···11+---
22+'@atcute/did-plc': minor
33+---
44+55+add `signOperation` and `signTombstone` functions for signing unsigned PLC operations
+7
.changeset/hungry-plc-client.md
···11+---
22+'@atcute/did-plc': minor
33+---
44+55+add `PlcClient` for interacting with plc.directory API
66+77+includes new types `PlcState` and `SequencedEntry` for API responses
+5
.changeset/smooth-dids-derive.md
···11+---
22+'@atcute/did-plc': minor
33+---
44+55+add `deriveDidFromGenesisOp` to derive did:plc identifier from genesis operation
+41-2
packages/identity/did-plc/README.md
···11# @atcute/did-plc
2233-validate and process did:plc operation logs.
33+validate, sign, and interact with did:plc operations.
4455```sh
66npm install @atcute/did-plc
77```
8899did:plc is a self-certifying DID method where the audit log serves as the source of truth. this
1010-package validates that operations are properly signed and chained.
1010+package validates that operations are properly signed and chained, and provides utilities for
1111+creating and submitting new operations.
11121213## usage
1414+1515+### using the client
1616+1717+```ts
1818+import { PlcClient } from '@atcute/did-plc';
1919+2020+const client = new PlcClient();
2121+2222+// fetch DID document
2323+const doc = await client.getDocument('did:plc:ragtjsm2j2vknwkz3zp4oxrd');
2424+2525+// fetch current identity state
2626+const state = await client.getState('did:plc:ragtjsm2j2vknwkz3zp4oxrd');
2727+2828+// fetch operation log
2929+const log = await client.getOperationLog('did:plc:ragtjsm2j2vknwkz3zp4oxrd');
3030+3131+// fetch last operation
3232+const lastOp = await client.getLastOperation('did:plc:ragtjsm2j2vknwkz3zp4oxrd');
3333+3434+// submit a signed operation
3535+await client.submitOperation('did:plc:ragtjsm2j2vknwkz3zp4oxrd', signedOperation);
3636+```
3737+3838+### signing operations
3939+4040+```ts
4141+import { signOperation, signTombstone, deriveDidFromGenesisOp } from '@atcute/did-plc';
4242+4343+// sign an unsigned operation
4444+const signedOp = await signOperation(unsignedOp, rotationKey);
4545+4646+// sign a tombstone
4747+const signedTombstone = await signTombstone(unsignedTombstone, rotationKey);
4848+4949+// derive did:plc from genesis operation
5050+const did = await deriveDidFromGenesisOp(signedGenesisOp);
5151+```
13521453### validating audit logs
1554
+195
packages/identity/did-plc/lib/client.ts
···11+import type { DidDocument } from '@atcute/identity';
22+import { defs as identityDefs } from '@atcute/identity';
33+import { FailedResponseError, isResponseOk, parseResponseAsJson, pipe, validateJsonWith } from '@atcute/util-fetch';
44+55+import * as defs from './typedefs.js';
66+import * as t from './types.js';
77+88+const MAX_RESPONSE_SIZE = 64 * 1024;
99+1010+const handleDocument = pipe(
1111+ isResponseOk,
1212+ parseResponseAsJson(/^application\/(did\+ld\+)?json$/, MAX_RESPONSE_SIZE),
1313+ validateJsonWith(identityDefs.didDocument, { mode: 'passthrough' }),
1414+);
1515+1616+const handlePlcState = pipe(
1717+ isResponseOk,
1818+ parseResponseAsJson(/^application\/json$/, MAX_RESPONSE_SIZE),
1919+ validateJsonWith(defs.plcState, { mode: 'passthrough' }),
2020+);
2121+2222+const handleOperationLog = pipe(
2323+ isResponseOk,
2424+ parseResponseAsJson(/^application\/json$/, MAX_RESPONSE_SIZE),
2525+ validateJsonWith(defs.operationLog, { mode: 'passthrough' }),
2626+);
2727+2828+const handleIndexedEntryLog = pipe(
2929+ isResponseOk,
3030+ parseResponseAsJson(/^application\/json$/, MAX_RESPONSE_SIZE),
3131+ validateJsonWith(defs.indexedEntryLog, { mode: 'passthrough' }),
3232+);
3333+3434+const handleLastOperation = pipe(
3535+ isResponseOk,
3636+ parseResponseAsJson(/^application\/json$/, MAX_RESPONSE_SIZE),
3737+ validateJsonWith(defs.compatibleOperationOrTombstone, { mode: 'passthrough' }),
3838+);
3939+4040+export interface PlcClientOptions {
4141+ /** plc directory URL, defaults to https://plc.directory */
4242+ serviceUrl?: string;
4343+ /** custom fetch function */
4444+ fetch?: typeof globalThis.fetch;
4545+}
4646+4747+export interface PlcRequestOptions {
4848+ signal?: AbortSignal;
4949+}
5050+5151+/**
5252+ * client for interacting with plc.directory
5353+ */
5454+export class PlcClient {
5555+ readonly serviceUrl: string;
5656+ #fetch: typeof globalThis.fetch;
5757+5858+ constructor({ serviceUrl = 'https://plc.directory', fetch: fetchFn = fetch }: PlcClientOptions = {}) {
5959+ this.serviceUrl = serviceUrl;
6060+ this.#fetch = fetchFn;
6161+ }
6262+6363+ /**
6464+ * fetches the DID document for a did:plc
6565+ * @param did the did:plc identifier
6666+ * @param options request options
6767+ * @returns the DID document
6868+ */
6969+ async getDocument(did: t.DidPlcString, options?: PlcRequestOptions): Promise<DidDocument> {
7070+ const url = new URL(`/${encodeURIComponent(did)}`, this.serviceUrl);
7171+7272+ const response = await this.#fetch(url, {
7373+ signal: options?.signal,
7474+ headers: { accept: 'application/did+ld+json,application/json' },
7575+ });
7676+7777+ const { json } = await handleDocument(response);
7878+ return json;
7979+ }
8080+8181+ /**
8282+ * fetches the current identity state for a did:plc
8383+ * @param did the did:plc identifier
8484+ * @param options request options
8585+ * @returns the current plc state
8686+ */
8787+ async getState(did: t.DidPlcString, options?: PlcRequestOptions): Promise<t.PlcState> {
8888+ const url = new URL(`/${encodeURIComponent(did)}/data`, this.serviceUrl);
8989+9090+ const response = await this.#fetch(url, {
9191+ signal: options?.signal,
9292+ headers: { accept: 'application/json' },
9393+ });
9494+9595+ const { json } = await handlePlcState(response);
9696+ return json;
9797+ }
9898+9999+ /**
100100+ * fetches the operation log for a did:plc
101101+ * @param did the did:plc identifier
102102+ * @param options request options
103103+ * @returns the operation log
104104+ */
105105+ async getOperationLog(did: t.DidPlcString, options?: PlcRequestOptions): Promise<t.OperationLog> {
106106+ const url = new URL(`/${encodeURIComponent(did)}/log`, this.serviceUrl);
107107+108108+ const response = await this.#fetch(url, {
109109+ signal: options?.signal,
110110+ headers: { accept: 'application/json' },
111111+ });
112112+113113+ const { json } = await handleOperationLog(response);
114114+ return json;
115115+ }
116116+117117+ /**
118118+ * fetches the auditable log for a did:plc (includes CIDs and timestamps)
119119+ * @param did the did:plc identifier
120120+ * @param options request options
121121+ * @returns the indexed entry log
122122+ */
123123+ async getAuditLog(did: t.DidPlcString, options?: PlcRequestOptions): Promise<t.IndexedEntryLog> {
124124+ const url = new URL(`/${encodeURIComponent(did)}/log/audit`, this.serviceUrl);
125125+126126+ const response = await this.#fetch(url, {
127127+ signal: options?.signal,
128128+ headers: { accept: 'application/json' },
129129+ });
130130+131131+ const { json } = await handleIndexedEntryLog(response);
132132+ return json;
133133+ }
134134+135135+ /**
136136+ * fetches the last operation for a did:plc
137137+ * @param did the did:plc identifier
138138+ * @param options request options
139139+ * @returns the last operation or tombstone
140140+ */
141141+ async getLastOperation(
142142+ did: t.DidPlcString,
143143+ options?: PlcRequestOptions,
144144+ ): Promise<t.CompatibleOperationOrTombstone> {
145145+ const url = new URL(`/${encodeURIComponent(did)}/log/last`, this.serviceUrl);
146146+147147+ const response = await this.#fetch(url, {
148148+ signal: options?.signal,
149149+ headers: { accept: 'application/json' },
150150+ });
151151+152152+ const { json } = await handleLastOperation(response);
153153+ return json;
154154+ }
155155+156156+ /**
157157+ * submits a signed operation to plc.directory
158158+ * @param did the did:plc identifier
159159+ * @param operation the signed operation to submit
160160+ * @param options request options
161161+ */
162162+ async submitOperation(
163163+ did: t.DidPlcString,
164164+ operation: t.OperationOrTombstone,
165165+ options?: PlcRequestOptions,
166166+ ): Promise<void> {
167167+ const url = new URL(`/${encodeURIComponent(did)}`, this.serviceUrl);
168168+169169+ const response = await this.#fetch(url, {
170170+ method: 'POST',
171171+ signal: options?.signal,
172172+ headers: { 'content-type': 'application/json' },
173173+ body: JSON.stringify(operation),
174174+ });
175175+176176+ if (!response.ok) {
177177+ throw new FailedResponseError(response);
178178+ }
179179+ }
180180+181181+ /**
182182+ * checks if the plc directory is reachable
183183+ * @param options request options
184184+ * @returns true if reachable
185185+ */
186186+ async ping(options?: PlcRequestOptions): Promise<boolean> {
187187+ const url = new URL('/_health', this.serviceUrl);
188188+189189+ const response = await this.#fetch(url, {
190190+ signal: options?.signal,
191191+ });
192192+193193+ return response.ok;
194194+ }
195195+}
+3-6
packages/identity/did-plc/lib/data.ts
···11import * as CBOR from '@atcute/cbor';
22import * as CID from '@atcute/cid';
33import { isKeyDid } from '@atcute/identity';
44-import { toBase32 } from '@atcute/multibase';
55-import { toSha256 } from '@atcute/uint8array';
6475import { DISPUTE_WINDOW } from './constants.js';
86import * as err from './errors.js';
97import * as t from './types.js';
1010-import { isSignedOperationValid, normalizeOp } from './utils.js';
88+import { deriveDidFromGenesisOp, isSignedOperationValid, normalizeOp } from './utils.js';
1191210// soft constraint limits for incoming operations
1311const MAX_OP_BYTES = 4000;
···148146149147 // Check if CID and DID matches
150148 {
151151- const opBytes = CBOR.encode(proposed.operation);
152152-153153- const expectedDid = `did:plc:${toBase32(await toSha256(opBytes)).slice(0, 24)}`;
149149+ const expectedDid = await deriveDidFromGenesisOp(proposed.operation);
154150 if (expectedDid !== did) {
155151 throw new err.GenesisHashError(proposed, did);
156152 }
157153154154+ const opBytes = CBOR.encode(proposed.operation);
158155 const expectedCid = CID.toString(await CID.create(CID.CODEC_DCBOR, opBytes));
159156 if (expectedCid !== proposed.cid) {
160157 throw new err.InvalidHashError(proposed, expectedCid);
+1
packages/identity/did-plc/lib/index.ts
···11export * as defs from './typedefs.js';
22export * from './types.js';
3344+export * from './client.js';
45export * from './constants.js';
56export * from './data.js';
67export * from './errors.js';