@metalabel/dfos-protocol 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -17,18 +17,15 @@ import { verifyContentChain, verifyIdentityChain } from '@metalabel/dfos-protoco
17
17
  import { createAuthToken, createDFOSCredential } from '@metalabel/dfos-protocol/credentials';
18
18
  // Crypto primitives
19
19
  import { createJws, dagCborCanonicalEncode, verifyJws } from '@metalabel/dfos-protocol/crypto';
20
- // Merkle trees
21
- import { buildMerkleTree, verifyMerkleProof } from '@metalabel/dfos-protocol/merkle';
22
20
  ```
23
21
 
24
22
  ## Subpath Exports
25
23
 
26
- | Export | Description |
27
- | -------------------------------------- | ----------------------------------------------------------------------- |
28
- | `@metalabel/dfos-protocol/chain` | Identity and content chain signing, verification, beacons, countersigns |
29
- | `@metalabel/dfos-protocol/credentials` | Auth tokens (DID-signed JWT) and DFOS credentials for authorization |
30
- | `@metalabel/dfos-protocol/crypto` | Ed25519, JWS, JWT, dag-cbor, base64url, ID generation |
31
- | `@metalabel/dfos-protocol/merkle` | SHA-256 binary merkle tree, inclusion proofs |
24
+ | Export | Description |
25
+ | -------------------------------------- | ------------------------------------------------------------------------- |
26
+ | `@metalabel/dfos-protocol/chain` | Identity & content chains, services, artifacts, countersigns, revocations |
27
+ | `@metalabel/dfos-protocol/credentials` | Auth tokens (DID-signed JWT) and DFOS credentials for authorization |
28
+ | `@metalabel/dfos-protocol/crypto` | Ed25519, JWS, JWT, dag-cbor, base64url, ID generation |
32
29
 
33
30
  ## Specifications
34
31
 
@@ -36,7 +33,7 @@ import { buildMerkleTree, verifyMerkleProof } from '@metalabel/dfos-protocol/mer
36
33
  | ------------------------------------------------ | -------------------------------------------------------------- |
37
34
  | [PROTOCOL.md](../../specs/PROTOCOL.md) | Core protocol — chains, signatures, verification, test vectors |
38
35
  | [DID-METHOD.md](../../specs/DID-METHOD.md) | W3C DID method specification for `did:dfos` |
39
- | [CONTENT-MODEL.md](../../specs/CONTENT-MODEL.md) | Standard content schemas (post, profile, manifest) |
36
+ | [CONTENT-MODEL.md](../../specs/CONTENT-MODEL.md) | Standard content schemas (post, profile) |
40
37
 
41
38
  ## Examples
42
39
 
@@ -50,8 +47,7 @@ The `examples/` directory contains deterministic reference fixtures that can be
50
47
  - `content-delegated.json` — creator genesis + delegated update with DFOS write credential
51
48
  - `credential-write.json` — DFOS write credential (broad + content-narrowed)
52
49
  - `credential-read.json` — DFOS read credential
53
- - `merkle-tree.json` — 5 content IDs sorted tree root, with inclusion proof
54
- - `beacon.json` — signed manifest pointer announcement with witness countersignature
50
+ - `identity-services.json` — genesis publishing a services set (relay locator + content/artifact anchors)
55
51
 
56
52
  ## License
57
53
 
@@ -1,5 +1,5 @@
1
- import { I as IdentityOperation, S as Signer, V as VerifiedIdentity, C as ContentOperation, B as BeaconPayload, a as CountersignPayload, A as ArtifactPayload } from '../schemas-BEl38wrI.js';
2
- export { M as MAX_ARTIFACT_PAYLOAD_SIZE, b as MultikeyPublicKey, R as RevocationPayload } from '../schemas-BEl38wrI.js';
1
+ import { I as IdentityOperation, h as Signer, V as VerifiedIdentity, S as ServiceEntry, b as ContentOperation, c as CountersignPayload, a as ArtifactPayload } from '../schemas-Myod8ES9.js';
2
+ export { A as ARTIFACT_CID_ANCHOR_RE, C as CONTENT_ID_ANCHOR_RE, M as MAX_ARTIFACT_PAYLOAD_SIZE, d as MAX_SERVICES_ENTRIES, e as MAX_SERVICES_PAYLOAD_SIZE, f as MultikeyPublicKey, R as RevocationPayload, g as ServicesArray } from '../schemas-Myod8ES9.js';
3
3
  import 'zod';
4
4
 
5
5
  /** Ed25519 public key multicodec value */
@@ -32,7 +32,7 @@ declare const deriveChainIdentifier: (cidBytes: Uint8Array, prefix: string) => s
32
32
  /**
33
33
  * Derive a bare content identifier from CID bytes
34
34
  *
35
- * Returns the raw 22-char hash with no prefix. Applications may add
35
+ * Returns the raw 31-char hash with no prefix. Applications may add
36
36
  * their own prefix for routing (e.g., post_xxxx) — that's semantic sugar.
37
37
  */
38
38
  declare const deriveContentId: (cidBytes: Uint8Array) => string;
@@ -88,8 +88,31 @@ declare const verifyIdentityExtensionFromTrustedState: (input: {
88
88
  createdAt: string;
89
89
  }>;
90
90
 
91
+ type AnchorKind = 'chain' | 'artifact' | 'invalid';
92
+ /**
93
+ * Enforce the services byte cap on the CBOR-encoded array — same encoding the
94
+ * wire uses, so the bound is identical across implementations. Mirrors the
95
+ * artifact payload size check.
96
+ */
97
+ declare const assertServicesWithinCap: (services: ServiceEntry[]) => Promise<void>;
98
+ /**
99
+ * Classify a ContentAnchor target by structural form. Resolvers dispatch on the
100
+ * result: 'chain' → resolve a content chain by contentId; 'artifact' → fetch by
101
+ * CID and require type:"artifact"; 'invalid' → reject (e.g. a bare head CID is
102
+ * 'artifact'-shaped but fails the resolution-time type check).
103
+ */
104
+ declare const classifyAnchor: (anchor: string) => AnchorKind;
105
+ /** Recognized (core-blessed) service types. All other types are valid but opaque. */
106
+ declare const RECOGNIZED_SERVICE_TYPES: readonly ["DfosRelay", "ContentAnchor"];
107
+ /** Whether the core assigns structural semantics to this service type. */
108
+ declare const isRecognizedServiceType: (type: string) => boolean;
109
+ /** Select the DfosRelay transport endpoints from a services set, in entry order. */
110
+ declare const relayEndpoints: (services: ServiceEntry[]) => string[];
111
+ /** Select ContentAnchor entries matching a client label (e.g. "profile"). */
112
+ declare const anchorsByLabel: (services: ServiceEntry[], label: string) => ServiceEntry[];
113
+
91
114
  interface VerifiedContentChain {
92
- /** Content identifier — bare 22-char hash derived from genesis CID */
115
+ /** Content identifier — bare 31-char hash derived from genesis CID */
93
116
  contentId: string;
94
117
  /** CID of the genesis operation */
95
118
  genesisCID: string;
@@ -144,6 +167,11 @@ declare const verifyContentChain: (input: {
144
167
  * is true, as credential verification needs identity resolution.
145
168
  */
146
169
  resolveIdentity?: (did: string) => Promise<VerifiedIdentity | undefined>;
170
+ /**
171
+ * Check whether a credential (leaf or parent) has been revoked. Threaded onto
172
+ * the WRITE path so revoked credentials no longer authorize writes.
173
+ */
174
+ isRevoked?: (issuerDID: string, credentialCID: string) => Promise<boolean>;
147
175
  }) => Promise<VerifiedContentChain>;
148
176
  /**
149
177
  * Verify a single new content operation against already-verified chain state
@@ -165,40 +193,14 @@ declare const verifyContentExtensionFromTrustedState: (input: {
165
193
  enforceAuthorization?: boolean;
166
194
  /** Resolve a DID to a VerifiedIdentity. Required when enforceAuthorization is true. */
167
195
  resolveIdentity?: (did: string) => Promise<VerifiedIdentity | undefined>;
196
+ /** Check whether a credential (leaf or parent) has been revoked. */
197
+ isRevoked?: (issuerDID: string, credentialCID: string) => Promise<boolean>;
168
198
  }) => Promise<{
169
199
  state: VerifiedContentChain;
170
200
  operationCID: string;
171
201
  createdAt: string;
172
202
  }>;
173
203
 
174
- interface VerifiedBeacon {
175
- did: string;
176
- manifestContentId: string;
177
- createdAt: string;
178
- signerKeyId: string;
179
- beaconCID: string;
180
- }
181
- /**
182
- * Sign a beacon announcement as a JWS
183
- */
184
- declare const signBeacon: (input: {
185
- payload: BeaconPayload;
186
- signer: Signer;
187
- kid: string;
188
- }) => Promise<{
189
- jwsToken: string;
190
- beaconCID: string;
191
- }>;
192
- /**
193
- * Verify a beacon JWS — signature, CID, payload schema, clock skew
194
- */
195
- declare const verifyBeacon: (input: {
196
- jwsToken: string;
197
- resolveKey: (kid: string) => Promise<Uint8Array>;
198
- /** Current time for clock skew check (defaults to Date.now()) */
199
- now?: number;
200
- }) => Promise<VerifiedBeacon>;
201
-
202
204
  interface VerifiedCountersignature {
203
205
  /** CID of this countersign operation (distinct from the target) */
204
206
  countersignCID: string;
@@ -206,6 +208,8 @@ interface VerifiedCountersignature {
206
208
  witnessDID: string;
207
209
  /** The CID being attested to */
208
210
  targetCID: string;
211
+ /** Open-namespace relation tag, if present (e.g. "endorses", "coauthors") */
212
+ relation?: string;
209
213
  }
210
214
  /**
211
215
  * Sign a countersignature attesting to a target operation by CID
@@ -287,4 +291,4 @@ declare const verifyRevocation: (input: {
287
291
  resolveKey: (kid: string) => Promise<Uint8Array>;
288
292
  }) => Promise<VerifiedRevocation>;
289
293
 
290
- export { ArtifactPayload, BeaconPayload, ContentOperation, CountersignPayload, ED25519_PRIV_MULTICODEC, ED25519_PUB_MULTICODEC, IdentityOperation, Signer, type VerifiedArtifact, type VerifiedBeacon, type VerifiedContentChain, type VerifiedCountersignature, VerifiedIdentity, type VerifiedRevocation, decodeMultikey, deriveChainIdentifier, deriveContentId, encodeEd25519Multikey, signArtifact, signBeacon, signContentOperation, signCountersignature, signIdentityOperation, signRevocation, verifyArtifact, verifyBeacon, verifyContentChain, verifyContentExtensionFromTrustedState, verifyCountersignature, verifyIdentityChain, verifyIdentityExtensionFromTrustedState, verifyRevocation };
294
+ export { type AnchorKind, ArtifactPayload, ContentOperation, CountersignPayload, ED25519_PRIV_MULTICODEC, ED25519_PUB_MULTICODEC, IdentityOperation, RECOGNIZED_SERVICE_TYPES, ServiceEntry, Signer, type VerifiedArtifact, type VerifiedContentChain, type VerifiedCountersignature, VerifiedIdentity, type VerifiedRevocation, anchorsByLabel, assertServicesWithinCap, classifyAnchor, decodeMultikey, deriveChainIdentifier, deriveContentId, encodeEd25519Multikey, isRecognizedServiceType, relayEndpoints, signArtifact, signContentOperation, signCountersignature, signIdentityOperation, signRevocation, verifyArtifact, verifyContentChain, verifyContentExtensionFromTrustedState, verifyCountersignature, verifyIdentityChain, verifyIdentityExtensionFromTrustedState, verifyRevocation };
@@ -1,61 +1,79 @@
1
1
  import {
2
+ ARTIFACT_CID_ANCHOR_RE,
2
3
  ArtifactPayload,
3
- BeaconPayload,
4
+ CONTENT_ID_ANCHOR_RE,
4
5
  ContentOperation,
5
6
  CountersignPayload,
6
7
  IdentityOperation,
7
8
  MAX_ARTIFACT_PAYLOAD_SIZE,
9
+ MAX_SERVICES_ENTRIES,
10
+ MAX_SERVICES_PAYLOAD_SIZE,
8
11
  MultikeyPublicKey,
12
+ RECOGNIZED_SERVICE_TYPES,
9
13
  RevocationPayload,
14
+ ServiceEntry,
15
+ ServicesArray,
10
16
  VerifiedIdentity,
17
+ anchorsByLabel,
18
+ assertServicesWithinCap,
19
+ classifyAnchor,
11
20
  deriveChainIdentifier,
12
21
  deriveContentId,
22
+ isRecognizedServiceType,
23
+ relayEndpoints,
13
24
  signArtifact,
14
- signBeacon,
15
25
  signContentOperation,
16
26
  signCountersignature,
17
27
  signIdentityOperation,
18
28
  signRevocation,
19
29
  verifyArtifact,
20
- verifyBeacon,
21
30
  verifyContentChain,
22
31
  verifyContentExtensionFromTrustedState,
23
32
  verifyCountersignature,
24
33
  verifyIdentityChain,
25
34
  verifyIdentityExtensionFromTrustedState,
26
35
  verifyRevocation
27
- } from "../chunk-UEJ364OG.js";
36
+ } from "../chunk-SDUOUFTF.js";
28
37
  import {
29
38
  ED25519_PRIV_MULTICODEC,
30
39
  ED25519_PUB_MULTICODEC,
31
40
  decodeMultikey,
32
41
  encodeEd25519Multikey
33
- } from "../chunk-24VGJGUM.js";
34
- import "../chunk-ZXXP5W5N.js";
42
+ } from "../chunk-LQFOBE6X.js";
43
+ import "../chunk-GQOZJKKO.js";
35
44
  export {
45
+ ARTIFACT_CID_ANCHOR_RE,
36
46
  ArtifactPayload,
37
- BeaconPayload,
47
+ CONTENT_ID_ANCHOR_RE,
38
48
  ContentOperation,
39
49
  CountersignPayload,
40
50
  ED25519_PRIV_MULTICODEC,
41
51
  ED25519_PUB_MULTICODEC,
42
52
  IdentityOperation,
43
53
  MAX_ARTIFACT_PAYLOAD_SIZE,
54
+ MAX_SERVICES_ENTRIES,
55
+ MAX_SERVICES_PAYLOAD_SIZE,
44
56
  MultikeyPublicKey,
57
+ RECOGNIZED_SERVICE_TYPES,
45
58
  RevocationPayload,
59
+ ServiceEntry,
60
+ ServicesArray,
46
61
  VerifiedIdentity,
62
+ anchorsByLabel,
63
+ assertServicesWithinCap,
64
+ classifyAnchor,
47
65
  decodeMultikey,
48
66
  deriveChainIdentifier,
49
67
  deriveContentId,
50
68
  encodeEd25519Multikey,
69
+ isRecognizedServiceType,
70
+ relayEndpoints,
51
71
  signArtifact,
52
- signBeacon,
53
72
  signContentOperation,
54
73
  signCountersignature,
55
74
  signIdentityOperation,
56
75
  signRevocation,
57
76
  verifyArtifact,
58
- verifyBeacon,
59
77
  verifyContentChain,
60
78
  verifyContentExtensionFromTrustedState,
61
79
  verifyCountersignature,
@@ -40,7 +40,7 @@ var isValidEd25519Signature = (payload, signature, publicKey) => {
40
40
  // src/crypto/id.ts
41
41
  import { sha256 as sha256Hash } from "@noble/hashes/sha2.js";
42
42
  var alphabet = "2346789acdefhknrtvz";
43
- var idLength = 22;
43
+ var idLength = 31;
44
44
  var getRandomBytes = (length) => {
45
45
  const bytes = new Uint8Array(length);
46
46
  globalThis.crypto.getRandomValues(bytes);
@@ -75,6 +75,22 @@ var normalizedId = (prefix, id) => {
75
75
  return `${prefixLowered}_${idLowered}`;
76
76
  };
77
77
 
78
+ // src/crypto/jws-profile.ts
79
+ var assertJwsProfile = (header, makeError) => {
80
+ if (header.alg !== "EdDSA") {
81
+ throw makeError(`Unsupported algorithm: ${String(header.alg)}`);
82
+ }
83
+ if ("crit" in header) {
84
+ throw makeError("crit header is not supported");
85
+ }
86
+ if ("jwk" in header) {
87
+ throw makeError("jwk header is not allowed (key is resolved from kid)");
88
+ }
89
+ if ("x5c" in header) {
90
+ throw makeError("x5c header is not allowed (key is resolved from kid)");
91
+ }
92
+ };
93
+
78
94
  // src/crypto/jws.ts
79
95
  var createJws = async (options) => {
80
96
  const headerB64 = base64urlEncode(JSON.stringify(options.header));
@@ -99,9 +115,10 @@ var verifyJws = (options) => {
99
115
  } catch {
100
116
  throw new JwsVerificationError("Failed to decode token");
101
117
  }
102
- if (header.alg !== "EdDSA") {
103
- throw new JwsVerificationError(`Unsupported algorithm: ${header.alg}`);
104
- }
118
+ assertJwsProfile(
119
+ header,
120
+ (m) => new JwsVerificationError(m)
121
+ );
105
122
  const signingInput = `${headerB64}.${payloadB64}`;
106
123
  const signingInputBytes = new TextEncoder().encode(signingInput);
107
124
  const signatureBytes = base64urlDecode(signatureB64);
@@ -172,12 +189,13 @@ var verifyJwt = (options) => {
172
189
  } catch {
173
190
  throw new JwtVerificationError("Failed to decode token");
174
191
  }
192
+ assertJwsProfile(
193
+ header,
194
+ (m) => new JwtVerificationError(m)
195
+ );
175
196
  const signingInput = `${headerB64}.${payloadB64}`;
176
197
  const signingInputBytes = new TextEncoder().encode(signingInput);
177
198
  const signatureBytes = base64urlDecode(signatureB64);
178
- if (header.alg !== "EdDSA") {
179
- throw new JwtVerificationError(`Unsupported algorithm: ${header.alg}`);
180
- }
181
199
  const isValid = isValidEd25519Signature(signingInputBytes, signatureBytes, options.publicKey);
182
200
  if (!isValid) throw new JwtVerificationError("Invalid signature");
183
201
  const currentTime = options.currentTime ?? Math.floor(Date.now() / 1e3);
@@ -209,14 +227,43 @@ import * as Block from "multiformats/block";
209
227
  import { CID } from "multiformats/cid";
210
228
  import { sha256 } from "multiformats/hashes/sha2";
211
229
  var dagCborCanonicalEncode = async (value) => {
230
+ assertCanonicalNumbers(value);
231
+ const serialized = JSON.parse(JSON.stringify(value));
232
+ assertCanonicalNumbers(serialized);
212
233
  return await Block.encode({
213
- // removes any undefineds or other non-serializable values, kinda whack but
214
- // it works for now
215
- value: JSON.parse(JSON.stringify(value)),
234
+ // removes any undefineds or other non-serializable values (and normalizes
235
+ // -0 to 0)
236
+ value: serialized,
216
237
  codec: dagCborCodec,
217
238
  hasher: sha256
218
239
  });
219
240
  };
241
+ var MAX_SAFE_CANONICAL_INTEGER = 9007199254740991;
242
+ var assertCanonicalNumbers = (value) => {
243
+ if (typeof value === "number") {
244
+ if (!Number.isFinite(value)) {
245
+ throw new Error(`non-finite number is not canonicalizable: ${value}`);
246
+ }
247
+ if (!Number.isInteger(value)) {
248
+ throw new Error(
249
+ `non-integer number is not canonicalizable: ${value} (encode it as a string)`
250
+ );
251
+ }
252
+ if (value > MAX_SAFE_CANONICAL_INTEGER || value < -MAX_SAFE_CANONICAL_INTEGER) {
253
+ throw new Error(
254
+ `integer out of safe range is not canonicalizable: ${value} (encode it as a string)`
255
+ );
256
+ }
257
+ return;
258
+ }
259
+ if (Array.isArray(value)) {
260
+ for (const entry of value) assertCanonicalNumbers(entry);
261
+ return;
262
+ }
263
+ if (value !== null && typeof value === "object") {
264
+ for (const entry of Object.values(value)) assertCanonicalNumbers(entry);
265
+ }
266
+ };
220
267
  var parseDagCborCID = (cid) => {
221
268
  return CID.parse(cid);
222
269
  };
@@ -237,6 +284,7 @@ export {
237
284
  generateId,
238
285
  isValidId,
239
286
  normalizedId,
287
+ assertJwsProfile,
240
288
  createJws,
241
289
  verifyJws,
242
290
  decodeJwsUnsafe,
@@ -5,7 +5,7 @@ import {
5
5
  decodeJwsUnsafe,
6
6
  verifyJws,
7
7
  verifyJwt
8
- } from "./chunk-ZXXP5W5N.js";
8
+ } from "./chunk-GQOZJKKO.js";
9
9
 
10
10
  // src/credentials/schemas.ts
11
11
  import { z } from "zod";
@@ -14,7 +14,7 @@ var MAX_AUD = 512;
14
14
  var MAX_RESOURCE = 512;
15
15
  var MAX_ACTION = 64;
16
16
  var MAX_ATT = 32;
17
- var MAX_PRF = 8;
17
+ var MAX_PRF = 1;
18
18
  var Attenuation = z.strictObject({
19
19
  resource: z.string().min(1).max(MAX_RESOURCE),
20
20
  action: z.string().min(1).max(MAX_ACTION)
@@ -96,6 +96,7 @@ var verifyAuthToken = (options) => {
96
96
  iss: result.data.iss,
97
97
  aud: result.data.aud,
98
98
  exp: result.data.exp,
99
+ iat: result.data.iat,
99
100
  kid
100
101
  };
101
102
  };
@@ -256,41 +257,38 @@ var verifyDelegationChain = async (credential, options) => {
256
257
  }
257
258
  return { credential, chain, rootDID: options.rootDID };
258
259
  }
259
- const parents = [];
260
- for (const parentJws of current.prf) {
261
- const parent = await verifyDFOSCredential(parentJws, {
262
- resolveIdentity: options.resolveIdentity,
263
- ...options.now !== void 0 ? { now: options.now } : {}
264
- });
265
- if (options.isRevoked) {
266
- const revoked = await options.isRevoked(parent.iss, parent.credentialCID);
267
- if (revoked) {
268
- throw new CredentialVerificationError("parent credential in delegation chain is revoked");
269
- }
260
+ if (current.prf.length > 1) {
261
+ throw new CredentialVerificationError(
262
+ "delegation chain: multi-parent credentials are not supported (prf must have at most one entry)"
263
+ );
264
+ }
265
+ const parent = await verifyDFOSCredential(current.prf[0], {
266
+ resolveIdentity: options.resolveIdentity,
267
+ ...options.now !== void 0 ? { now: options.now } : {}
268
+ });
269
+ if (options.isRevoked) {
270
+ const revoked = await options.isRevoked(parent.iss, parent.credentialCID);
271
+ if (revoked) {
272
+ throw new CredentialVerificationError("parent credential in delegation chain is revoked");
270
273
  }
271
- parents.push(parent);
272
274
  }
273
- const matchingParent = parents.find((p) => p.aud === "*" || p.aud === current.iss);
274
- if (!matchingParent) {
275
+ if (parent.aud !== "*" && parent.aud !== current.iss) {
275
276
  throw new CredentialVerificationError(
276
- `delegation gap: no parent credential has audience matching child issuer ${current.iss}`
277
+ `delegation gap: parent credential audience ${parent.aud} does not match child issuer ${current.iss}`
277
278
  );
278
279
  }
279
- for (const parent of parents) {
280
- if (current.exp > parent.exp) {
281
- throw new CredentialVerificationError(
282
- "delegation chain: child credential expiry exceeds parent expiry"
283
- );
284
- }
280
+ if (current.exp > parent.exp) {
281
+ throw new CredentialVerificationError(
282
+ "delegation chain: child credential expiry exceeds parent expiry"
283
+ );
285
284
  }
286
- const parentAttUnion = parents.flatMap((p) => p.att);
287
- if (!isAttenuated(parentAttUnion, current.att)) {
285
+ if (!isAttenuated(parent.att, current.att)) {
288
286
  throw new CredentialVerificationError(
289
287
  "delegation chain: child credential scope exceeds parent scope"
290
288
  );
291
289
  }
292
- chain.push(...parents);
293
- current = parents[0];
290
+ chain.push(parent);
291
+ current = parent;
294
292
  }
295
293
  throw new CredentialVerificationError("delegation chain too deep (max 16 hops)");
296
294
  };