@metalabel/dfos-protocol 0.53.0 → 0.54.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
@@ -36,6 +36,12 @@ import { signKeyProof, verifyKeyProof } from '@metalabel/dfos-protocol/key-proof
36
36
  | `@metalabel/dfos-protocol/key-proof` | KEY-PROOF envelopes — compose/sign and verify a challenge-bound proof that a candidate key is held |
37
37
  | `@metalabel/dfos-protocol/fold` | Canonical linearization and LWW-map folds for index documents |
38
38
 
39
+ ## `api:` resources and the `jti` member
40
+
41
+ An `api:` resource takes one of two forms: `api:<host>`, the whole API surface, and `api:<host>/spaces/<id>`, one space on it. A bare host is the ancestor of every space at that host, so a grant naming the host covers a space-addressed request while a space-scoped grant covers only its own space. `parseApiResource` and `apiResourceCovers` are that rule on its own; `isAttenuated` and `matchesResource` apply it. Every other resource type is exact byte equality, and a malformed `api:` id covers nothing — itself included.
42
+
43
+ `jti` is the envelope's one registered additive member: a per-request unique string of at most `MAX_JTI_BYTES` UTF-8 bytes, emitted after the canonical members. Pass it typed to `signApiRequest` or `signApiIdentityRequest` (`generateJti()` mints one), set `requireJti` on a verifier to refuse a proof without it, and read the verified value back off the envelope to key a replay cache. A proof carrying none has the bytes it always had.
44
+
39
45
  ## Specifications
40
46
 
41
47
  | Document | Description |
@@ -62,6 +68,7 @@ The `examples/` directory contains deterministic reference fixtures that can be
62
68
  - `credential-write.json` — DFOS write credential (broad + content-narrowed)
63
69
  - `credential-read.json` — DFOS read credential
64
70
  - `identity-services.json` — genesis publishing a services set (relay locator + content/artifact anchors)
71
+ - `api-resource-coverage.json` — the `api:` hierarchy's parse, coverage, attenuation, and matching rows, read by the TypeScript and Go suites alike
65
72
 
66
73
  ## License
67
74
 
@@ -1,5 +1,5 @@
1
- import { I as IdentityOperation, v as Signer, x as VerifiedIdentity, S as ServiceEntry, c as ContentOperation, R as ResolvedIdentity, r as RevocationChecker, d as CountersignPayload, a as ArtifactPayload } from '../dfos-credential-j-kHtFvG.js';
2
- export { A as ARTIFACT_CID_ANCHOR_RE, C as CONTENT_ID_ANCHOR_RE, f as CreditClaimPayload, g as DeclaredKeyState, h as Iso8601, M as MAX_ARTIFACT_PAYLOAD_SIZE, j as MAX_CREDIT_CLAIM_SIZE, k as MAX_KEY_PROOFS, l as MAX_OPERATION_SIZE, m as MAX_SERVICES_ENTRIES, n as MAX_SERVICES_PAYLOAD_SIZE, o as MAX_SIGN_REQUEST_PAYLOAD_SIZE, p as MAX_SIGN_REQUEST_SIZE, q as MultikeyPublicKey, s as RevocationPayload, t as ServicesArray, u as SignRequestPayload, y as VoidKeyMembership, G as parseProtocolTimestampUnix } from '../dfos-credential-j-kHtFvG.js';
1
+ import { I as IdentityOperation, v as Signer, x as VerifiedIdentity, S as ServiceEntry, c as ContentOperation, R as ResolvedIdentity, r as RevocationChecker, d as CountersignPayload, a as ArtifactPayload } from '../dfos-credential-DgT8XVTm.js';
2
+ export { A as ARTIFACT_CID_ANCHOR_RE, C as CONTENT_ID_ANCHOR_RE, f as CreditClaimPayload, g as DeclaredKeyState, h as Iso8601, M as MAX_ARTIFACT_PAYLOAD_SIZE, j as MAX_CREDIT_CLAIM_SIZE, k as MAX_KEY_PROOFS, l as MAX_OPERATION_SIZE, m as MAX_SERVICES_ENTRIES, n as MAX_SERVICES_PAYLOAD_SIZE, o as MAX_SIGN_REQUEST_PAYLOAD_SIZE, p as MAX_SIGN_REQUEST_SIZE, q as MultikeyPublicKey, s as RevocationPayload, t as ServicesArray, u as SignRequestPayload, y as VoidKeyMembership, J as parseProtocolTimestampUnix } from '../dfos-credential-DgT8XVTm.js';
3
3
  import 'zod';
4
4
 
5
5
  /** Ed25519 public key multicodec value */
@@ -53,8 +53,8 @@ import {
53
53
  verifyIdentityExtensionFromTrustedState,
54
54
  verifyRevocation,
55
55
  verifySignRequest
56
- } from "../chunk-WBFWGQTS.js";
57
- import "../chunk-VEKRLXZ5.js";
56
+ } from "../chunk-QIM3KZXZ.js";
57
+ import "../chunk-RPA44ND4.js";
58
58
  import "../chunk-ISHAX5WN.js";
59
59
  import {
60
60
  ED25519_PRIV_MULTICODEC,
@@ -6,7 +6,7 @@ import {
6
6
  matchesResource,
7
7
  verifyDFOSCredential,
8
8
  verifyDelegationChain
9
- } from "./chunk-VEKRLXZ5.js";
9
+ } from "./chunk-RPA44ND4.js";
10
10
  import {
11
11
  KEY_ADD_JWS_TYP,
12
12
  KEY_ROLES,
@@ -43,6 +43,7 @@ var REQUEST_PROOF_JWS_TYP = "did:dfos:request-proof";
43
43
  var IDENTITY_PROOF_JWS_TYP = "did:dfos:identity-proof";
44
44
  var EMPTY_BODY_SHA256 = "47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU";
45
45
  var MAX_REQUEST_PROOF_SIZE = 4096;
46
+ var MAX_JTI_BYTES = 256;
46
47
  var DEFAULT_PROOF_WINDOW_SECONDS = 60;
47
48
  var DEFAULT_PROOF_SKEW_SECONDS = 60;
48
49
  var MAX_PROOF_FRESHNESS_SPAN_SECONDS = 300;
@@ -66,6 +67,27 @@ var CTL_OR_SPACE = /[\u0000-\u0020\u007f]/;
66
67
  var BASE64URL_32 = /^[A-Za-z0-9_-]{43}$/;
67
68
  var EXTRA_MEMBER_NAME = /^[A-Za-z0-9_.-]+$/;
68
69
  var CANONICAL_MEMBERS = /* @__PURE__ */ new Set(["method", "host", "path", "bodyHash", "credentialCID", "iat"]);
70
+ var assertJtiBound = (jti, label) => {
71
+ if (jti === "" || encoder.encode(jti).length > MAX_JTI_BYTES) {
72
+ throw new Error(
73
+ `invalid ${label}: jti must be a non-empty string of at most ${MAX_JTI_BYTES} bytes`
74
+ );
75
+ }
76
+ };
77
+ var withJti = (extra, jti, label) => {
78
+ if (jti === void 0) return extra;
79
+ if (extra && "jti" in extra) {
80
+ throw new Error(`invalid ${label}: jti is named twice \u2014 pass it typed or in extraMembers`);
81
+ }
82
+ assertJtiBound(jti, label);
83
+ return { ...extra, jti };
84
+ };
85
+ var emittedExtraMembers = (parsed, extra, label) => canonicalExtraMembers(withJti(extra, parsed.jti, label), label);
86
+ var generateJti = () => {
87
+ const bytes = new Uint8Array(16);
88
+ globalThis.crypto.getRandomValues(bytes);
89
+ return base64urlEncode(bytes);
90
+ };
69
91
  var assertNoLoneSurrogate = (value, field, label) => {
70
92
  if (LONE_SURROGATE.test(value)) {
71
93
  throw new Error(`invalid ${label}: ${field} must be well-formed Unicode`);
@@ -113,7 +135,26 @@ var validateProofPayload = (value, shape) => {
113
135
  if (typeof iat !== "number" || !Number.isSafeInteger(iat) || iat <= 0) {
114
136
  throw new Error(`invalid ${label}: iat must be a positive integer`);
115
137
  }
116
- return shape.credentialed ? { method, host, path, bodyHash, credentialCID: raw["credentialCID"], iat } : { method, host, path, bodyHash, iat };
138
+ const rawJti = raw["jti"];
139
+ let jti;
140
+ if (rawJti !== void 0) {
141
+ if (typeof rawJti !== "string") {
142
+ throw new Error(
143
+ `invalid ${label}: jti must be a non-empty string of at most ${MAX_JTI_BYTES} bytes`
144
+ );
145
+ }
146
+ assertJtiBound(rawJti, label);
147
+ jti = rawJti;
148
+ }
149
+ return {
150
+ method,
151
+ host,
152
+ path,
153
+ bodyHash,
154
+ ...shape.credentialed ? { credentialCID: raw["credentialCID"] } : {},
155
+ iat,
156
+ ...jti !== void 0 ? { jti } : {}
157
+ };
117
158
  };
118
159
  var validateRequestProofPayload = (value) => {
119
160
  const parsed = validateProofPayload(value, REQUEST_PROOF_SHAPE);
@@ -123,7 +164,8 @@ var validateRequestProofPayload = (value) => {
123
164
  path: parsed.path,
124
165
  bodyHash: parsed.bodyHash,
125
166
  credentialCID: parsed.credentialCID,
126
- iat: parsed.iat
167
+ iat: parsed.iat,
168
+ ...parsed.jti !== void 0 ? { jti: parsed.jti } : {}
127
169
  };
128
170
  };
129
171
  var validateIdentityProofPayload = (value) => {
@@ -133,7 +175,8 @@ var validateIdentityProofPayload = (value) => {
133
175
  host: parsed.host,
134
176
  path: parsed.path,
135
177
  bodyHash: parsed.bodyHash,
136
- iat: parsed.iat
178
+ iat: parsed.iat,
179
+ ...parsed.jti !== void 0 ? { jti: parsed.jti } : {}
137
180
  };
138
181
  };
139
182
  var canonicalExtraMembers = (extra, label) => {
@@ -171,16 +214,22 @@ var proofPayloadObject = (parsed, shape, extra) => {
171
214
  for (const [name, value] of extra) out[name] = value;
172
215
  return out;
173
216
  };
174
- var apiRequestSigningInput = (payload, extraMembers) => proofSigningInput(
175
- validateProofPayload(payload, REQUEST_PROOF_SHAPE),
176
- REQUEST_PROOF_SHAPE,
177
- canonicalExtraMembers(extraMembers, REQUEST_PROOF_SHAPE.label)
178
- );
179
- var apiIdentitySigningInput = (payload, extraMembers) => proofSigningInput(
180
- validateProofPayload(payload, IDENTITY_PROOF_SHAPE),
181
- IDENTITY_PROOF_SHAPE,
182
- canonicalExtraMembers(extraMembers, IDENTITY_PROOF_SHAPE.label)
183
- );
217
+ var apiRequestSigningInput = (payload, extraMembers) => {
218
+ const parsed = validateProofPayload(payload, REQUEST_PROOF_SHAPE);
219
+ return proofSigningInput(
220
+ parsed,
221
+ REQUEST_PROOF_SHAPE,
222
+ emittedExtraMembers(parsed, extraMembers, REQUEST_PROOF_SHAPE.label)
223
+ );
224
+ };
225
+ var apiIdentitySigningInput = (payload, extraMembers) => {
226
+ const parsed = validateProofPayload(payload, IDENTITY_PROOF_SHAPE);
227
+ return proofSigningInput(
228
+ parsed,
229
+ IDENTITY_PROOF_SHAPE,
230
+ emittedExtraMembers(parsed, extraMembers, IDENTITY_PROOF_SHAPE.label)
231
+ );
232
+ };
184
233
  var sha256BodyHash = (body) => base64urlEncode(sha256(body));
185
234
  var parseDfosAuthorization = (header) => {
186
235
  if (!header) return null;
@@ -199,12 +248,13 @@ var signApiRequest = async (input) => {
199
248
  path: input.path,
200
249
  bodyHash: sha256BodyHash(input.body ?? EMPTY_BODY),
201
250
  credentialCID: input.credentialCID,
202
- iat: input.iat ?? Math.floor(Date.now() / 1e3)
251
+ iat: input.iat ?? Math.floor(Date.now() / 1e3),
252
+ ...input.jti !== void 0 ? { jti: input.jti } : {}
203
253
  });
204
254
  if (!input.kid.includes("#")) {
205
255
  throw new Error("invalid request proof: kid must be a DID URL");
206
256
  }
207
- const extra = canonicalExtraMembers(input.extraMembers, REQUEST_PROOF_SHAPE.label);
257
+ const extra = emittedExtraMembers(payload, input.extraMembers, REQUEST_PROOF_SHAPE.label);
208
258
  const proof = await createJws({
209
259
  header: { alg: "EdDSA", typ: REQUEST_PROOF_JWS_TYP, kid: input.kid },
210
260
  payload: proofPayloadObject(payload, REQUEST_PROOF_SHAPE, extra),
@@ -221,12 +271,13 @@ var signApiIdentityRequest = async (input) => {
221
271
  host: input.host,
222
272
  path: input.path,
223
273
  bodyHash: sha256BodyHash(input.body ?? EMPTY_BODY),
224
- iat: input.iat ?? Math.floor(Date.now() / 1e3)
274
+ iat: input.iat ?? Math.floor(Date.now() / 1e3),
275
+ ...input.jti !== void 0 ? { jti: input.jti } : {}
225
276
  });
226
277
  if (!input.kid.includes("#")) {
227
278
  throw new Error("invalid identity proof: kid must be a DID URL");
228
279
  }
229
- const extra = canonicalExtraMembers(input.extraMembers, IDENTITY_PROOF_SHAPE.label);
280
+ const extra = emittedExtraMembers(payload, input.extraMembers, IDENTITY_PROOF_SHAPE.label);
230
281
  const proof = await createJws({
231
282
  header: { alg: "EdDSA", typ: IDENTITY_PROOF_JWS_TYP, kid: input.kid },
232
283
  payload: proofPayloadObject(payload, IDENTITY_PROOF_SHAPE, extra),
@@ -247,7 +298,7 @@ var buildApiIdentityHeaders = (input) => ({
247
298
  var ApiRequestVerifyError = class extends Error {
248
299
  reason;
249
300
  phase;
250
- /** Recommended HTTP status: 401 proof-invalid, 403 credential-invalid, 503 unverifiable, 500 config. */
301
+ /** Recommended HTTP status: 401 proof-invalid, 403 credential-invalid or uncovered, 409 replayed, 503 unverifiable, 500 config. */
251
302
  status;
252
303
  constructor(reason, phase, status, message) {
253
304
  super(message);
@@ -258,6 +309,8 @@ var ApiRequestVerifyError = class extends Error {
258
309
  }
259
310
  };
260
311
  var invalidProof = (message) => new ApiRequestVerifyError("invalid", "proof", 401, message);
312
+ var replayedProof = (message) => new ApiRequestVerifyError("replayed", "proof", 409, message);
313
+ var uncoveredProof = (message) => new ApiRequestVerifyError("uncovered", "credential", 403, message);
261
314
  var unverifiableProof = (message) => new ApiRequestVerifyError("unverifiable", "proof", 503, message);
262
315
  var misconfiguredProof = (message) => new ApiRequestVerifyError("config", "config", 500, message);
263
316
  var assertProofVerifierConfig = (input) => {
@@ -319,6 +372,9 @@ var verifyProofEnvelope = async (input, shape, resolvePresenter) => {
319
372
  } catch (err) {
320
373
  throw invalidProof(err instanceof Error ? err.message : `invalid ${shape.label} payload`);
321
374
  }
375
+ if (input.requireJti === true && payload.jti === void 0) {
376
+ throw invalidProof(`invalid ${shape.label}: jti is required on this route`);
377
+ }
322
378
  const now = Math.floor((input.now ? input.now() : Date.now()) / 1e3);
323
379
  if (now - payload.iat > window) throw invalidProof(`${shape.label} is stale`);
324
380
  if (payload.iat - now > skew) {
@@ -362,7 +418,15 @@ var verifyProofEnvelope = async (input, shape, resolvePresenter) => {
362
418
  } catch (err) {
363
419
  throw invalidProof(err instanceof Error ? err.message : `invalid ${shape.label} signature`);
364
420
  }
365
- return { payload, rawPayload, presenterDID, kid, now };
421
+ return {
422
+ payload,
423
+ rawPayload,
424
+ presenterDID,
425
+ kid,
426
+ now,
427
+ ...payload.jti !== void 0 ? { jti: payload.jti } : {},
428
+ ...key.roles !== void 0 ? { keyRoles: key.roles } : {}
429
+ };
366
430
  };
367
431
  var verifyIdentityProofEnvelope = (input, resolvePresenter) => verifyProofEnvelope(input, IDENTITY_PROOF_SHAPE, resolvePresenter);
368
432
  var verifyRequestProofEnvelope = (input, resolvePresenter) => verifyProofEnvelope(input, REQUEST_PROOF_SHAPE, resolvePresenter);
@@ -538,6 +602,26 @@ var parseResource = (resource) => {
538
602
  if (colonIdx < 0) return null;
539
603
  return { type: resource.substring(0, colonIdx), id: resource.substring(colonIdx + 1) };
540
604
  };
605
+ var SPACE_ID_RE = /^[2346789acdefhknrtvz]{31}$/;
606
+ var parseApiResource = (resource) => {
607
+ const parsed = parseResource(resource);
608
+ if (!parsed || parsed.type !== "api") return null;
609
+ const slash = parsed.id.indexOf("/");
610
+ if (slash < 0) return parsed.id === "" ? null : { host: parsed.id };
611
+ const host = parsed.id.substring(0, slash);
612
+ if (host === "") return null;
613
+ const child = parsed.id.substring(slash + 1);
614
+ if (!child.startsWith("spaces/")) return null;
615
+ const spaceId = child.substring("spaces/".length);
616
+ if (!SPACE_ID_RE.test(spaceId)) return null;
617
+ return { host, spaceId };
618
+ };
619
+ var apiResourceCovers = (entryResource, requiredResource) => {
620
+ const entry = parseApiResource(entryResource);
621
+ const required = parseApiResource(requiredResource);
622
+ if (!entry || !required || entry.host !== required.host) return false;
623
+ return entry.spaceId === void 0 || entry.spaceId === required.spaceId;
624
+ };
541
625
  var ASCII_TRIM_RE = /^[\t\n\v\f\r ]+|[\t\n\v\f\r ]+$/g;
542
626
  var parseActions = (action) => new Set(
543
627
  action.split(",").map((a) => a.replace(ASCII_TRIM_RE, "")).filter((a) => a !== "")
@@ -563,6 +647,9 @@ var isAttenuated = (parentAtt, childAtt) => {
563
647
  if (childRes.type === "chain" && parentRes.type === "chain") {
564
648
  return childRes.id === parentRes.id;
565
649
  }
650
+ if (childRes.type === "api" && parentRes.type === "api") {
651
+ return apiResourceCovers(parentEntry.resource, childEntry.resource) || childEntry.resource === parentEntry.resource;
652
+ }
566
653
  if (childRes.type !== "chain" && parentRes.type !== "chain") {
567
654
  return childEntry.resource === parentEntry.resource;
568
655
  }
@@ -589,6 +676,10 @@ var matchesResource = async (att, resource, action) => {
589
676
  if (entryRes.type === "chain" && entryRes.id === "*" && requestedRes.type === "chain") {
590
677
  return true;
591
678
  }
679
+ if (entryRes.type === "api" || requestedRes.type === "api") {
680
+ if (apiResourceCovers(entry.resource, resource)) return true;
681
+ continue;
682
+ }
592
683
  if (entryRes.type === requestedRes.type && entryRes.id === requestedRes.id) {
593
684
  return true;
594
685
  }
@@ -623,11 +714,13 @@ export {
623
714
  IDENTITY_PROOF_JWS_TYP,
624
715
  EMPTY_BODY_SHA256,
625
716
  MAX_REQUEST_PROOF_SIZE,
717
+ MAX_JTI_BYTES,
626
718
  DEFAULT_PROOF_WINDOW_SECONDS,
627
719
  DEFAULT_PROOF_SKEW_SECONDS,
628
720
  MAX_PROOF_FRESHNESS_SPAN_SECONDS,
629
721
  MAX_BODY_BYTES,
630
722
  DFOS_AUTH_SCHEME,
723
+ generateJti,
631
724
  canonicalExtraMembers,
632
725
  apiRequestSigningInput,
633
726
  apiIdentitySigningInput,
@@ -639,6 +732,8 @@ export {
639
732
  buildApiIdentityHeaders,
640
733
  ApiRequestVerifyError,
641
734
  invalidProof,
735
+ replayedProof,
736
+ uncoveredProof,
642
737
  unverifiableProof,
643
738
  misconfiguredProof,
644
739
  assertProofVerifierConfig,
@@ -647,6 +742,8 @@ export {
647
742
  createDFOSCredential,
648
743
  verifyDFOSCredential,
649
744
  verifyDelegationChain,
745
+ parseApiResource,
746
+ apiResourceCovers,
650
747
  isAttenuated,
651
748
  matchesResource,
652
749
  decodeDFOSCredentialUnsafe,
@@ -1,4 +1,4 @@
1
- export { b as Attenuation, e as CredentialVerificationError, D as DFOSCredentialPayload, i as MAX_CREDENTIAL_SIZE, R as ResolvedIdentity, r as RevocationChecker, V as VerifiedDFOSCredential, w as VerifiedDelegationChain, z as createDFOSCredential, B as decodeDFOSCredentialUnsafe, E as isAttenuated, F as matchesResource, H as verifyDFOSCredential, J as verifyDelegationChain } from '../dfos-credential-j-kHtFvG.js';
1
+ export { b as Attenuation, e as CredentialVerificationError, D as DFOSCredentialPayload, i as MAX_CREDENTIAL_SIZE, R as ResolvedIdentity, r as RevocationChecker, V as VerifiedDFOSCredential, w as VerifiedDelegationChain, z as apiResourceCovers, B as createDFOSCredential, E as decodeDFOSCredentialUnsafe, F as isAttenuated, G as matchesResource, H as parseApiResource, K as verifyDFOSCredential, L as verifyDelegationChain } from '../dfos-credential-DgT8XVTm.js';
2
2
  import 'zod';
3
3
 
4
4
  /**
@@ -27,6 +27,11 @@ declare const IDENTITY_PROOF_JWS_TYP = "did:dfos:identity-proof";
27
27
  declare const EMPTY_BODY_SHA256 = "47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU";
28
28
  /** Size cap on the serialized proof token, checked BEFORE any decode. */
29
29
  declare const MAX_REQUEST_PROOF_SIZE = 4096;
30
+ /**
31
+ * Cap on the `jti` member, in UTF-8 bytes. There is no floor: uniqueness is the
32
+ * member's job, and a verifier's replay cache is what enforces it.
33
+ */
34
+ declare const MAX_JTI_BYTES = 256;
30
35
  /** RECOMMENDED acceptance window `W` — how old a proof may be, in seconds. */
31
36
  declare const DEFAULT_PROOF_WINDOW_SECONDS = 60;
32
37
  /** RECOMMENDED clock-skew allowance `S` — how forward-dated a proof may be. */
@@ -64,6 +69,11 @@ interface RequestProofPayload {
64
69
  credentialCID: string;
65
70
  /** Issued-at — unix seconds (positive integer). */
66
71
  iat: number;
72
+ /**
73
+ * The registered additive member: a per-request unique value, at most
74
+ * `MAX_JTI_BYTES` UTF-8 bytes. Emitted after `iat`, in the additive block.
75
+ */
76
+ jti?: string;
67
77
  }
68
78
  /**
69
79
  * The identity proof's payload: the request proof's five members MINUS
@@ -81,6 +91,11 @@ interface IdentityProofPayload {
81
91
  bodyHash: string;
82
92
  /** Issued-at — unix seconds (positive integer). */
83
93
  iat: number;
94
+ /**
95
+ * The registered additive member: a per-request unique value, at most
96
+ * `MAX_JTI_BYTES` UTF-8 bytes. Emitted after `iat`, in the additive block.
97
+ */
98
+ jti?: string;
84
99
  }
85
100
  /**
86
101
  * ADDITIVE MEMBERS, appended AFTER the canonical order.
@@ -108,7 +123,15 @@ interface ParsedProofPayload {
108
123
  bodyHash: string;
109
124
  credentialCID?: string;
110
125
  iat: number;
126
+ /** The registered additive member, when the proof carries one. */
127
+ jti?: string;
111
128
  }
129
+ /**
130
+ * Mint a `jti`: 128 bits from the platform CSPRNG, unpadded base64url (22
131
+ * characters). Uniqueness is per (presenter DID, freshness window), which this
132
+ * much randomness makes a collision no deployment will observe.
133
+ */
134
+ declare const generateJti: () => string;
112
135
  /**
113
136
  * Validate and normalize additive members into their canonical emission order:
114
137
  * lexicographic by name, after every canonical member. Returns `[]` for the
@@ -171,10 +194,17 @@ interface SignApiRequestInput {
171
194
  sign: (message: Uint8Array) => Promise<Uint8Array>;
172
195
  /** Issued-at override — unix seconds. Default `Math.floor(Date.now() / 1000)`. */
173
196
  iat?: number;
197
+ /**
198
+ * The registered additive member, per-request unique and at most
199
+ * `MAX_JTI_BYTES` bytes — required by a deployment that gates WRITES with this
200
+ * envelope (INTEGRATIONS.md, API security notes). `generateJti()` mints one.
201
+ * It emits the same bytes as `extraMembers: { jti }`, and naming it both ways
202
+ * throws.
203
+ */
204
+ jti?: string;
174
205
  /**
175
206
  * ADDITIVE members, appended after the canonical order in lexicographic name
176
- * order. `{ jti }` is the registered one required by a deployment that gates
177
- * WRITES with this envelope (INTEGRATIONS.md, API security notes).
207
+ * order. `{ jti }` is the registered one, and has the typed field above.
178
208
  */
179
209
  extraMembers?: ProofExtraMembers;
180
210
  }
@@ -209,10 +239,17 @@ interface SignApiIdentityRequestInput {
209
239
  sign: (message: Uint8Array) => Promise<Uint8Array>;
210
240
  /** Issued-at override — unix seconds. Default `Math.floor(Date.now() / 1000)`. */
211
241
  iat?: number;
242
+ /**
243
+ * The registered additive member, per-request unique and at most
244
+ * `MAX_JTI_BYTES` bytes. A WRITE-SHAPED surface — relay ingestion, blob
245
+ * upload — REQUIRES it (INTEGRATIONS.md, API security notes). `generateJti()`
246
+ * mints one. It emits the same bytes as `extraMembers: { jti }`, and naming it
247
+ * both ways throws.
248
+ */
249
+ jti?: string;
212
250
  /**
213
251
  * ADDITIVE members, appended after the canonical order in lexicographic name
214
- * order. `{ jti }` is the registered one, and a WRITE-SHAPED surface relay
215
- * ingestion, blob upload — REQUIRES it (INTEGRATIONS.md, API security notes).
252
+ * order. `{ jti }` is the registered one, and has the typed field above.
216
253
  */
217
254
  extraMembers?: ProofExtraMembers;
218
255
  }
@@ -261,13 +298,21 @@ declare const buildApiIdentityHeaders: (input: {
261
298
  * The verdict class. Branch on `reason`, never on message text.
262
299
  *
263
300
  * - `invalid` — checked and failed.
301
+ * - `replayed` — checked, VALID, and already spent: this `jti` was seen inside
302
+ * the proof's own freshness window. Its own verdict rather than an `invalid`
303
+ * one because the caller's answer differs — re-read state, do not re-sign the
304
+ * same request.
305
+ * - `uncovered` — the credential is VALID and simply does not reach this route's
306
+ * resource and action. Distinct from `invalid` because nothing is wrong with
307
+ * the artifact: a route offering optional authentication serves its anonymous
308
+ * projection on an `uncovered` verdict, and still refuses an `invalid` one.
264
309
  * - `unverifiable` — could not check (an unresolvable presenter, an unreachable
265
310
  * revocation source). A transient resolution failure is the server's
266
311
  * condition, not the caller's.
267
312
  * - `config` — the DEPLOYMENT is misconfigured (a `W + S` over the 300-second
268
313
  * ceiling, or an empty required action). Not a judgment about the artifact.
269
314
  */
270
- type RequestProofFailureReason = 'invalid' | 'unverifiable' | 'config';
315
+ type RequestProofFailureReason = 'invalid' | 'replayed' | 'uncovered' | 'unverifiable' | 'config';
271
316
  /**
272
317
  * The verification phase a failure arose in. Load-bearing for HTTP mapping: an
273
318
  * `invalid` proof-layer failure is a 401 (with a `WWW-Authenticate: DFOS`
@@ -279,12 +324,26 @@ type RequestProofFailurePhase = 'proof' | 'credential' | 'config';
279
324
  declare class ApiRequestVerifyError extends Error {
280
325
  readonly reason: RequestProofFailureReason;
281
326
  readonly phase: RequestProofFailurePhase;
282
- /** Recommended HTTP status: 401 proof-invalid, 403 credential-invalid, 503 unverifiable, 500 config. */
327
+ /** Recommended HTTP status: 401 proof-invalid, 403 credential-invalid or uncovered, 409 replayed, 503 unverifiable, 500 config. */
283
328
  readonly status: number;
284
329
  constructor(reason: RequestProofFailureReason, phase: RequestProofFailurePhase, status: number, message: string);
285
330
  }
286
331
  /** invalid, proof phase → 401. */
287
332
  declare const invalidProof: (message: string) => ApiRequestVerifyError;
333
+ /**
334
+ * replayed, proof phase → 409. The proof verified; its `jti` was already spent.
335
+ * The cache belongs to the deployment, so this is the constructor every consumer
336
+ * classifies a replay with rather than a status each one picks.
337
+ */
338
+ declare const replayedProof: (message: string) => ApiRequestVerifyError;
339
+ /**
340
+ * uncovered, credential phase → 403. The credential verified and simply does not
341
+ * reach this route's resource and action. Its own verdict because a route
342
+ * offering optional authentication answers it with the anonymous projection a
343
+ * caller presenting nothing would get — a credential only ever ADDS — while an
344
+ * `invalid` credential is refused outright.
345
+ */
346
+ declare const uncoveredProof: (message: string) => ApiRequestVerifyError;
288
347
  /** unverifiable, proof phase → 503. */
289
348
  declare const unverifiableProof: (message: string) => ApiRequestVerifyError;
290
349
  /** the DEPLOYMENT is misconfigured → 500. Never a judgment about the artifact. */
@@ -301,10 +360,16 @@ declare const misconfiguredProof: (message: string) => ApiRequestVerifyError;
301
360
  interface ProofPresenterState {
302
361
  /** Current-state deletion. A deleted presenter's proofs are INVALID (401). */
303
362
  isDeleted: boolean;
304
- /** Current keys, any role. */
363
+ /**
364
+ * Current keys, any role. `roles` is OPTIONAL and the resolver's to supply: a
365
+ * key may hold several, and a deployment gating writes decides for itself
366
+ * which role may sign. The verifier never reads it — it hands the matched
367
+ * key's roles back on the verified envelope.
368
+ */
305
369
  keys: readonly {
306
370
  id: string;
307
371
  publicKeyMultibase: string;
372
+ roles?: readonly ('auth' | 'assert' | 'controller')[];
308
373
  }[];
309
374
  }
310
375
  /**
@@ -345,6 +410,12 @@ interface ProofEnvelopeInput {
345
410
  skewSeconds?: number;
346
411
  /** Clock injection (unix ms). Default `Date.now()`. */
347
412
  now?: () => number;
413
+ /**
414
+ * Require the registered `jti` member. Default false. A verifier gating WRITES
415
+ * sets it on every write-shaped route and records the value in its own replay
416
+ * cache; a read-shaped route may ignore the member entirely.
417
+ */
418
+ requireJti?: boolean;
348
419
  }
349
420
  /**
350
421
  * INTEGRATIONS.md, Verification algorithm step 4's freshness configuration,
@@ -372,11 +443,10 @@ interface VerifiedProofEnvelope {
372
443
  /**
373
444
  * The DECODED payload object, unknown members included.
374
445
  *
375
- * ADDITIVE MEMBERS ARE READ FROM HERE, at the consuming layer, AFTER
376
- * verification — the signature already covers them, and the canonical member
377
- * set stays closed. `jti` is the case this exists for: the envelope verifier
378
- * ignores it per MUST-ignore-unknown, and a write-gating deployment reads it
379
- * off this object and applies its own replay discipline.
446
+ * UNREGISTERED ADDITIVE MEMBERS ARE READ FROM HERE, at the consuming layer,
447
+ * AFTER verification — the signature already covers them, and the canonical
448
+ * member set stays closed. The registered `jti` has its own field above; every
449
+ * other additive member is ignored per MUST-ignore-unknown and survives here.
380
450
  */
381
451
  rawPayload: Record<string, unknown>;
382
452
  /** THE PRINCIPAL — the `kid`'s DID. */
@@ -385,6 +455,17 @@ interface VerifiedProofEnvelope {
385
455
  kid: string;
386
456
  /** The integer unix seconds the freshness check used. */
387
457
  now: number;
458
+ /**
459
+ * The registered `jti`, when the proof carried one — the value a write-gating
460
+ * deployment records in its replay cache, keyed with `presenterDID`.
461
+ */
462
+ jti?: string;
463
+ /**
464
+ * The matched key's roles, when the resolver supplied them. A deployment
465
+ * gating writes with an identity proof applies its own rule here: a key whose
466
+ * only effective role is `controller` should not stand for a write.
467
+ */
468
+ keyRoles?: readonly ('auth' | 'assert' | 'controller')[];
388
469
  }
389
470
  /**
390
471
  * Verify an IDENTITY proof's envelope — for this artifact steps 1–7 ARE the whole
@@ -406,4 +487,4 @@ declare const verifyIdentityProofEnvelope: (input: ProofEnvelopeInput, resolvePr
406
487
  */
407
488
  declare const verifyRequestProofEnvelope: (input: ProofEnvelopeInput, resolvePresenter: ResolveProofPresenter) => Promise<VerifiedProofEnvelope>;
408
489
 
409
- export { ApiRequestVerifyError, DEFAULT_PROOF_SKEW_SECONDS, DEFAULT_PROOF_WINDOW_SECONDS, DFOS_AUTH_SCHEME, EMPTY_BODY_SHA256, IDENTITY_PROOF_JWS_TYP, type IdentityProofPayload, MAX_BODY_BYTES, MAX_PROOF_FRESHNESS_SPAN_SECONDS, MAX_REQUEST_PROOF_SIZE, type ParsedProofPayload, type ProofEnvelopeInput, type ProofExtraMembers, type ProofPresenterState, REQUEST_PROOF_JWS_TYP, type RequestProofFailurePhase, type RequestProofFailureReason, type RequestProofPayload, type ResolveProofPresenter, type SignApiIdentityRequestInput, type SignApiRequestInput, type VerifiedProofEnvelope, apiIdentitySigningInput, apiRequestSigningInput, assertProofVerifierConfig, buildApiAuthHeaders, buildApiIdentityHeaders, canonicalExtraMembers, invalidProof, misconfiguredProof, parseDfosAuthorization, sha256BodyHash, signApiIdentityRequest, signApiRequest, unverifiableProof, verifyIdentityProofEnvelope, verifyRequestProofEnvelope };
490
+ export { ApiRequestVerifyError, DEFAULT_PROOF_SKEW_SECONDS, DEFAULT_PROOF_WINDOW_SECONDS, DFOS_AUTH_SCHEME, EMPTY_BODY_SHA256, IDENTITY_PROOF_JWS_TYP, type IdentityProofPayload, MAX_BODY_BYTES, MAX_JTI_BYTES, MAX_PROOF_FRESHNESS_SPAN_SECONDS, MAX_REQUEST_PROOF_SIZE, type ParsedProofPayload, type ProofEnvelopeInput, type ProofExtraMembers, type ProofPresenterState, REQUEST_PROOF_JWS_TYP, type RequestProofFailurePhase, type RequestProofFailureReason, type RequestProofPayload, type ResolveProofPresenter, type SignApiIdentityRequestInput, type SignApiRequestInput, type VerifiedProofEnvelope, apiIdentitySigningInput, apiRequestSigningInput, assertProofVerifierConfig, buildApiAuthHeaders, buildApiIdentityHeaders, canonicalExtraMembers, generateJti, invalidProof, misconfiguredProof, parseDfosAuthorization, replayedProof, sha256BodyHash, signApiIdentityRequest, signApiRequest, uncoveredProof, unverifiableProof, verifyIdentityProofEnvelope, verifyRequestProofEnvelope };
@@ -10,31 +10,37 @@ import {
10
10
  IDENTITY_PROOF_JWS_TYP,
11
11
  MAX_BODY_BYTES,
12
12
  MAX_CREDENTIAL_SIZE,
13
+ MAX_JTI_BYTES,
13
14
  MAX_PROOF_FRESHNESS_SPAN_SECONDS,
14
15
  MAX_REQUEST_PROOF_SIZE,
15
16
  REQUEST_PROOF_JWS_TYP,
16
17
  apiIdentitySigningInput,
17
18
  apiRequestSigningInput,
19
+ apiResourceCovers,
18
20
  assertProofVerifierConfig,
19
21
  buildApiAuthHeaders,
20
22
  buildApiIdentityHeaders,
21
23
  canonicalExtraMembers,
22
24
  createDFOSCredential,
23
25
  decodeDFOSCredentialUnsafe,
26
+ generateJti,
24
27
  invalidProof,
25
28
  isAttenuated,
26
29
  matchesResource,
27
30
  misconfiguredProof,
31
+ parseApiResource,
28
32
  parseDfosAuthorization,
33
+ replayedProof,
29
34
  sha256BodyHash,
30
35
  signApiIdentityRequest,
31
36
  signApiRequest,
37
+ uncoveredProof,
32
38
  unverifiableProof,
33
39
  verifyDFOSCredential,
34
40
  verifyDelegationChain,
35
41
  verifyIdentityProofEnvelope,
36
42
  verifyRequestProofEnvelope
37
- } from "../chunk-VEKRLXZ5.js";
43
+ } from "../chunk-RPA44ND4.js";
38
44
  import "../chunk-F7RHI2CK.js";
39
45
  import "../chunk-NUIIVAZ3.js";
40
46
  export {
@@ -49,25 +55,31 @@ export {
49
55
  IDENTITY_PROOF_JWS_TYP,
50
56
  MAX_BODY_BYTES,
51
57
  MAX_CREDENTIAL_SIZE,
58
+ MAX_JTI_BYTES,
52
59
  MAX_PROOF_FRESHNESS_SPAN_SECONDS,
53
60
  MAX_REQUEST_PROOF_SIZE,
54
61
  REQUEST_PROOF_JWS_TYP,
55
62
  apiIdentitySigningInput,
56
63
  apiRequestSigningInput,
64
+ apiResourceCovers,
57
65
  assertProofVerifierConfig,
58
66
  buildApiAuthHeaders,
59
67
  buildApiIdentityHeaders,
60
68
  canonicalExtraMembers,
61
69
  createDFOSCredential,
62
70
  decodeDFOSCredentialUnsafe,
71
+ generateJti,
63
72
  invalidProof,
64
73
  isAttenuated,
65
74
  matchesResource,
66
75
  misconfiguredProof,
76
+ parseApiResource,
67
77
  parseDfosAuthorization,
78
+ replayedProof,
68
79
  sha256BodyHash,
69
80
  signApiIdentityRequest,
70
81
  signApiRequest,
82
+ uncoveredProof,
71
83
  unverifiableProof,
72
84
  verifyDFOSCredential,
73
85
  verifyDelegationChain,
@@ -648,6 +648,26 @@ declare const verifyDelegationChain: (credential: VerifiedDFOSCredential, option
648
648
  */
649
649
  nowUnix?: number;
650
650
  }) => Promise<VerifiedDelegationChain>;
651
+ /**
652
+ * Parse an `api:` resource into its host and, when present, its space id.
653
+ * Returns `null` for any other resource type and for a malformed `api:` id —
654
+ * `null` IS the malformed verdict, and a malformed resource covers nothing.
655
+ *
656
+ * Two forms are well-formed and no others: `api:<host>` with a non-empty host
657
+ * carrying no `/`, and `api:<host>/spaces/<id>`. The id half splits on the FIRST
658
+ * `/`, so a host keeps its port and anything past `spaces/<id>` is malformed.
659
+ */
660
+ declare const parseApiResource: (resource: string) => {
661
+ host: string;
662
+ spaceId?: string;
663
+ } | null;
664
+ /**
665
+ * Whether one `api:` resource covers another: same host, and either the entry is
666
+ * the bare host (the registered ancestor of every space on it) or the two name
667
+ * the same space. False when either side is malformed or is not `api:` —
668
+ * coverage never crosses hosts and never crosses resource types.
669
+ */
670
+ declare const apiResourceCovers: (entryResource: string, requiredResource: string) => boolean;
651
671
  /**
652
672
  * Check if `childAtt` is a valid attenuation of `parentAtt`
653
673
  *
@@ -658,11 +678,14 @@ declare const verifyDelegationChain: (credential: VerifiedDFOSCredential, option
658
678
  * - `chain:X` covered by `chain:*` (narrowing from wildcard — valid)
659
679
  * - `chain:*` covered by `chain:*` (exact match)
660
680
  * - `chain:*` NOT covered by `chain:X` (widening — invalid)
661
- * - Non-`chain` types (`mailbox:<id>`, and any form a future capability
662
- * registers): exact byte equality of the full resource string, nothing else.
663
- * The wildcard is a `chain:`-only concept — a literal `*` id in any other
664
- * type is an ordinary id covering only itself and coverage never crosses
665
- * resource types. See CREDENTIALS.md "Resource Types".
681
+ * - `api:<host>/spaces/<id>` covered by `api:<host>` (narrowing onto one space)
682
+ * or by the same child. A MALFORMED `api:` id narrows only from a
683
+ * byte-identical parent, under the rule below — a bare host does not carry it.
684
+ * - Other types (`mailbox:<id>`, and any form a future capability registers):
685
+ * exact byte equality of the full resource string, nothing else. The wildcard
686
+ * is a `chain:`-only concept — a literal `*` id in any other type is an
687
+ * ordinary id covering only itself — and coverage never crosses resource
688
+ * types. See CREDENTIALS.md "Resource Types".
666
689
  * - Actions: child action set must be a subset of parent action set
667
690
  */
668
691
  declare const isAttenuated: (parentAtt: Attenuation[], childAtt: Attenuation[]) => boolean;
@@ -670,7 +693,14 @@ declare const isAttenuated: (parentAtt: Attenuation[], childAtt: Attenuation[])
670
693
  * Check if an `att` array covers a requested resource
671
694
  *
672
695
  * Used at the relay to determine if a credential authorizes access to a
673
- * specific content chain.
696
+ * specific content chain, and by an API verifier to decide whether a grant
697
+ * reaches the route's resource.
698
+ *
699
+ * `chain:*` covers any `chain:` request; an `api:` entry covers an `api:`
700
+ * request under the host/space hierarchy; every other form is exact byte
701
+ * equality. A malformed `api:` resource covers nothing and is covered by
702
+ * nothing, itself included — a resource a verifier cannot parse must not
703
+ * authorize a request. Every entry is scanned, so any one of them may cover.
674
704
  */
675
705
  declare const matchesResource: (att: Attenuation[], resource: string, action: string) => Promise<boolean>;
676
706
  /**
@@ -691,4 +721,4 @@ declare class CredentialVerificationError extends Error {
691
721
  constructor(message: string);
692
722
  }
693
723
 
694
- export { ARTIFACT_CID_ANCHOR_RE as A, decodeDFOSCredentialUnsafe as B, CONTENT_ID_ANCHOR_RE as C, DFOSCredentialPayload as D, isAttenuated as E, matchesResource as F, parseProtocolTimestampUnix as G, verifyDFOSCredential as H, IdentityOperation as I, verifyDelegationChain as J, MAX_ARTIFACT_PAYLOAD_SIZE as M, type ResolvedIdentity as R, ServiceEntry as S, type VerifiedDFOSCredential as V, ArtifactPayload as a, Attenuation as b, ContentOperation as c, CountersignPayload as d, CredentialVerificationError as e, CreditClaimPayload as f, DeclaredKeyState as g, Iso8601 as h, MAX_CREDENTIAL_SIZE as i, MAX_CREDIT_CLAIM_SIZE as j, MAX_KEY_PROOFS as k, MAX_OPERATION_SIZE as l, MAX_SERVICES_ENTRIES as m, MAX_SERVICES_PAYLOAD_SIZE as n, MAX_SIGN_REQUEST_PAYLOAD_SIZE as o, MAX_SIGN_REQUEST_SIZE as p, MultikeyPublicKey as q, type RevocationChecker as r, RevocationPayload as s, ServicesArray as t, SignRequestPayload as u, type Signer as v, type VerifiedDelegationChain as w, VerifiedIdentity as x, VoidKeyMembership as y, createDFOSCredential as z };
724
+ export { ARTIFACT_CID_ANCHOR_RE as A, createDFOSCredential as B, CONTENT_ID_ANCHOR_RE as C, DFOSCredentialPayload as D, decodeDFOSCredentialUnsafe as E, isAttenuated as F, matchesResource as G, parseApiResource as H, IdentityOperation as I, parseProtocolTimestampUnix as J, verifyDFOSCredential as K, verifyDelegationChain as L, MAX_ARTIFACT_PAYLOAD_SIZE as M, type ResolvedIdentity as R, ServiceEntry as S, type VerifiedDFOSCredential as V, ArtifactPayload as a, Attenuation as b, ContentOperation as c, CountersignPayload as d, CredentialVerificationError as e, CreditClaimPayload as f, DeclaredKeyState as g, Iso8601 as h, MAX_CREDENTIAL_SIZE as i, MAX_CREDIT_CLAIM_SIZE as j, MAX_KEY_PROOFS as k, MAX_OPERATION_SIZE as l, MAX_SERVICES_ENTRIES as m, MAX_SERVICES_PAYLOAD_SIZE as n, MAX_SIGN_REQUEST_PAYLOAD_SIZE as o, MAX_SIGN_REQUEST_SIZE as p, MultikeyPublicKey as q, type RevocationChecker as r, RevocationPayload as s, ServicesArray as t, SignRequestPayload as u, type Signer as v, type VerifiedDelegationChain as w, VerifiedIdentity as x, VoidKeyMembership as y, apiResourceCovers as z };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { JwsHeader, JwsVerificationError, JwtClaims, JwtCreateOptions, JwtHeader, JwtVerificationError, JwtVerifyOptions, PrefixedID, assertCanonicalJsonText, assertJwsProfile, base64urlDecode, base64urlEncode, createJws, createJwt, createNewEd25519Keypair, dagCborCanonicalEncode, decodeJwsUnsafe, decodeJwtUnsafe, generateId, generateIdNoPrefix, importEd25519Keypair, isCanonicallyEqual, isValidEd25519Signature, isValidId, normalizedId, parseDagCborCID, sha256, signPayloadEd25519, verifyJws, verifyJwt } from './crypto/index.js';
2
- export { A as ARTIFACT_CID_ANCHOR_RE, a as ArtifactPayload, b as Attenuation, C as CONTENT_ID_ANCHOR_RE, c as ContentOperation, d as CountersignPayload, e as CredentialVerificationError, f as CreditClaimPayload, D as DFOSCredentialPayload, g as DeclaredKeyState, I as IdentityOperation, h as Iso8601, M as MAX_ARTIFACT_PAYLOAD_SIZE, i as MAX_CREDENTIAL_SIZE, j as MAX_CREDIT_CLAIM_SIZE, k as MAX_KEY_PROOFS, l as MAX_OPERATION_SIZE, m as MAX_SERVICES_ENTRIES, n as MAX_SERVICES_PAYLOAD_SIZE, o as MAX_SIGN_REQUEST_PAYLOAD_SIZE, p as MAX_SIGN_REQUEST_SIZE, q as MultikeyPublicKey, R as ResolvedIdentity, r as RevocationChecker, s as RevocationPayload, S as ServiceEntry, t as ServicesArray, u as SignRequestPayload, v as Signer, V as VerifiedDFOSCredential, w as VerifiedDelegationChain, x as VerifiedIdentity, y as VoidKeyMembership, z as createDFOSCredential, B as decodeDFOSCredentialUnsafe, E as isAttenuated, F as matchesResource, G as parseProtocolTimestampUnix, H as verifyDFOSCredential, J as verifyDelegationChain } from './dfos-credential-j-kHtFvG.js';
2
+ export { A as ARTIFACT_CID_ANCHOR_RE, a as ArtifactPayload, b as Attenuation, C as CONTENT_ID_ANCHOR_RE, c as ContentOperation, d as CountersignPayload, e as CredentialVerificationError, f as CreditClaimPayload, D as DFOSCredentialPayload, g as DeclaredKeyState, I as IdentityOperation, h as Iso8601, M as MAX_ARTIFACT_PAYLOAD_SIZE, i as MAX_CREDENTIAL_SIZE, j as MAX_CREDIT_CLAIM_SIZE, k as MAX_KEY_PROOFS, l as MAX_OPERATION_SIZE, m as MAX_SERVICES_ENTRIES, n as MAX_SERVICES_PAYLOAD_SIZE, o as MAX_SIGN_REQUEST_PAYLOAD_SIZE, p as MAX_SIGN_REQUEST_SIZE, q as MultikeyPublicKey, R as ResolvedIdentity, r as RevocationChecker, s as RevocationPayload, S as ServiceEntry, t as ServicesArray, u as SignRequestPayload, v as Signer, V as VerifiedDFOSCredential, w as VerifiedDelegationChain, x as VerifiedIdentity, y as VoidKeyMembership, z as apiResourceCovers, B as createDFOSCredential, E as decodeDFOSCredentialUnsafe, F as isAttenuated, G as matchesResource, H as parseApiResource, J as parseProtocolTimestampUnix, K as verifyDFOSCredential, L as verifyDelegationChain } from './dfos-credential-DgT8XVTm.js';
3
3
  export { AnchorKind, CreditClaimFailureReason, CreditClaimVerifyError, CreditEntry, CreditEntryState, ED25519_PRIV_MULTICODEC, ED25519_PUB_MULTICODEC, IdentityStateNoSeenKeysError, RECOGNIZED_SERVICE_TYPES, SignRequestFailureReason, SignRequestVerifyError, VerifiedArtifact, VerifiedContentChain, VerifiedCountersignature, VerifiedCreditClaim, VerifiedCreditEntry, VerifiedRevocation, VerifiedSignRequest, anchorsByLabel, assertCanonicalSignRequestPayload, assertServicesWithinCap, buildSignRequest, classifyAnchor, decodeEd25519PublicMultikey, decodeMultikey, deriveChainIdentifier, deriveContentId, encodeEd25519Multikey, isRecognizedServiceType, relayEndpoints, signArtifact, signContentOperation, signCountersignature, signCreditClaim, signIdentityOperation, signRevocation, verifyArtifact, verifyContentChain, verifyContentExtensionFromTrustedState, verifyCountersignature, verifyCreditClaim, verifyCreditEntry, verifyIdentityChain, verifyIdentityExtensionFromTrustedState, verifyRevocation, verifySignRequest } from './chain/index.js';
4
- export { ApiRequestVerifyError, DEFAULT_PROOF_SKEW_SECONDS, DEFAULT_PROOF_WINDOW_SECONDS, DFOS_AUTH_SCHEME, EMPTY_BODY_SHA256, IDENTITY_PROOF_JWS_TYP, IdentityProofPayload, MAX_BODY_BYTES, MAX_PROOF_FRESHNESS_SPAN_SECONDS, MAX_REQUEST_PROOF_SIZE, ParsedProofPayload, ProofEnvelopeInput, ProofExtraMembers, ProofPresenterState, REQUEST_PROOF_JWS_TYP, RequestProofFailurePhase, RequestProofFailureReason, RequestProofPayload, ResolveProofPresenter, SignApiIdentityRequestInput, SignApiRequestInput, VerifiedProofEnvelope, apiIdentitySigningInput, apiRequestSigningInput, assertProofVerifierConfig, buildApiAuthHeaders, buildApiIdentityHeaders, canonicalExtraMembers, invalidProof, misconfiguredProof, parseDfosAuthorization, sha256BodyHash, signApiIdentityRequest, signApiRequest, unverifiableProof, verifyIdentityProofEnvelope, verifyRequestProofEnvelope } from './credentials/index.js';
4
+ export { ApiRequestVerifyError, DEFAULT_PROOF_SKEW_SECONDS, DEFAULT_PROOF_WINDOW_SECONDS, DFOS_AUTH_SCHEME, EMPTY_BODY_SHA256, IDENTITY_PROOF_JWS_TYP, IdentityProofPayload, MAX_BODY_BYTES, MAX_JTI_BYTES, MAX_PROOF_FRESHNESS_SPAN_SECONDS, MAX_REQUEST_PROOF_SIZE, ParsedProofPayload, ProofEnvelopeInput, ProofExtraMembers, ProofPresenterState, REQUEST_PROOF_JWS_TYP, RequestProofFailurePhase, RequestProofFailureReason, RequestProofPayload, ResolveProofPresenter, SignApiIdentityRequestInput, SignApiRequestInput, VerifiedProofEnvelope, apiIdentitySigningInput, apiRequestSigningInput, assertProofVerifierConfig, buildApiAuthHeaders, buildApiIdentityHeaders, canonicalExtraMembers, generateJti, invalidProof, misconfiguredProof, parseDfosAuthorization, replayedProof, sha256BodyHash, signApiIdentityRequest, signApiRequest, uncoveredProof, unverifiableProof, verifyIdentityProofEnvelope, verifyRequestProofEnvelope } from './credentials/index.js';
5
5
  export { DEFAULT_KEY_PROOF_SKEW_SECONDS, KEY_ADD_JWS_TYP, KEY_ROLES, KeyProofFailureReason, KeyProofPayload, KeyProofVerifyError, KeyRole, MAX_KEY_PROOF_SIZE, SignKeyProofInput, VerifiedKeyProof, VerifyChainKeyProofOptions, VerifyKeyProofOptions, isCanonicalRoleSet, keyProofSigningInput, keyWordFingerprint, parseRoleSet, roleSetCovers, serializeRoleSet, signKeyProof, unsafeKeyProofSubject, verifyChainKeyProof, verifyKeyProof } from './key-proof/index.js';
6
6
  export { FoldOperation, INDEX_V1_SCHEMA, IndexDelta, IndexDocument, IndexEntry, LwwDelta, OrderKey, byteCompare, compareHeadPreference, compareLinear, foldIndexV1, foldLwwMap, linearize } from './fold/index.js';
7
7
  import 'multiformats';
package/dist/index.js CHANGED
@@ -53,7 +53,7 @@ import {
53
53
  verifyIdentityExtensionFromTrustedState,
54
54
  verifyRevocation,
55
55
  verifySignRequest
56
- } from "./chunk-WBFWGQTS.js";
56
+ } from "./chunk-QIM3KZXZ.js";
57
57
  import {
58
58
  ApiRequestVerifyError,
59
59
  Attenuation,
@@ -66,11 +66,13 @@ import {
66
66
  IDENTITY_PROOF_JWS_TYP,
67
67
  MAX_BODY_BYTES,
68
68
  MAX_CREDENTIAL_SIZE,
69
+ MAX_JTI_BYTES,
69
70
  MAX_PROOF_FRESHNESS_SPAN_SECONDS,
70
71
  MAX_REQUEST_PROOF_SIZE,
71
72
  REQUEST_PROOF_JWS_TYP,
72
73
  apiIdentitySigningInput,
73
74
  apiRequestSigningInput,
75
+ apiResourceCovers,
74
76
  assertProofVerifierConfig,
75
77
  buildApiAuthHeaders,
76
78
  buildApiIdentityHeaders,
@@ -78,22 +80,26 @@ import {
78
80
  carryDependencyMissing,
79
81
  createDFOSCredential,
80
82
  decodeDFOSCredentialUnsafe,
83
+ generateJti,
81
84
  invalidProof,
82
85
  isAttenuated,
83
86
  isDependencyMissing,
84
87
  markDependencyMissing,
85
88
  matchesResource,
86
89
  misconfiguredProof,
90
+ parseApiResource,
87
91
  parseDfosAuthorization,
92
+ replayedProof,
88
93
  sha256BodyHash,
89
94
  signApiIdentityRequest,
90
95
  signApiRequest,
96
+ uncoveredProof,
91
97
  unverifiableProof,
92
98
  verifyDFOSCredential,
93
99
  verifyDelegationChain,
94
100
  verifyIdentityProofEnvelope,
95
101
  verifyRequestProofEnvelope
96
- } from "./chunk-VEKRLXZ5.js";
102
+ } from "./chunk-RPA44ND4.js";
97
103
  import {
98
104
  DEFAULT_KEY_PROOF_SKEW_SECONDS,
99
105
  KEY_ADD_JWS_TYP,
@@ -187,6 +193,7 @@ export {
187
193
  MAX_BODY_BYTES,
188
194
  MAX_CREDENTIAL_SIZE,
189
195
  MAX_CREDIT_CLAIM_SIZE,
196
+ MAX_JTI_BYTES,
190
197
  MAX_KEY_PROOFS,
191
198
  MAX_KEY_PROOF_SIZE,
192
199
  MAX_OPERATION_SIZE,
@@ -209,6 +216,7 @@ export {
209
216
  anchorsByLabel,
210
217
  apiIdentitySigningInput,
211
218
  apiRequestSigningInput,
219
+ apiResourceCovers,
212
220
  assertCanonicalJsonText,
213
221
  assertCanonicalSignRequestPayload,
214
222
  assertJwsProfile,
@@ -242,6 +250,7 @@ export {
242
250
  foldLwwMap,
243
251
  generateId,
244
252
  generateIdNoPrefix,
253
+ generateJti,
245
254
  importEd25519Keypair,
246
255
  invalidProof,
247
256
  isAttenuated,
@@ -258,11 +267,13 @@ export {
258
267
  matchesResource,
259
268
  misconfiguredProof,
260
269
  normalizedId,
270
+ parseApiResource,
261
271
  parseDagCborCID,
262
272
  parseDfosAuthorization,
263
273
  parseProtocolTimestampUnix,
264
274
  parseRoleSet,
265
275
  relayEndpoints,
276
+ replayedProof,
266
277
  roleSetCovers,
267
278
  serializeRoleSet,
268
279
  sha256,
@@ -277,6 +288,7 @@ export {
277
288
  signKeyProof,
278
289
  signPayloadEd25519,
279
290
  signRevocation,
291
+ uncoveredProof,
280
292
  unsafeKeyProofSubject,
281
293
  unverifiableProof,
282
294
  verifyArtifact,
@@ -0,0 +1,413 @@
1
+ {
2
+ "description": "Reference vectors for the api: resource family — parse, coverage, attenuation, and request matching. Consumed by the TS credentials tests and the Go delegation tests (both twins read this file; neither inlines a row). Space ids are 31 characters of the did:dfos identifier alphabet [2346789acdefhknrtvz]; hosts are lowercase authorities, port included when non-default.",
3
+ "ids": {
4
+ "spaceA": "9ctvrdn9vedda7efetrhcdakfh4cr2k",
5
+ "spaceB": "cv7n8vkvr64cctf3294h9k4eanhff8z"
6
+ },
7
+ "parse": [
8
+ { "resource": "api:api.dfos.com", "valid": true, "host": "api.dfos.com", "spaceId": null },
9
+ {
10
+ "resource": "api:api.example.org:8443",
11
+ "valid": true,
12
+ "host": "api.example.org:8443",
13
+ "spaceId": null
14
+ },
15
+ {
16
+ "resource": "api:api.dfos.com/spaces/9ctvrdn9vedda7efetrhcdakfh4cr2k",
17
+ "valid": true,
18
+ "host": "api.dfos.com",
19
+ "spaceId": "9ctvrdn9vedda7efetrhcdakfh4cr2k"
20
+ },
21
+ {
22
+ "resource": "api:api.example.org:8443/spaces/9ctvrdn9vedda7efetrhcdakfh4cr2k",
23
+ "valid": true,
24
+ "host": "api.example.org:8443",
25
+ "spaceId": "9ctvrdn9vedda7efetrhcdakfh4cr2k"
26
+ },
27
+ {
28
+ "resource": "api:*",
29
+ "valid": true,
30
+ "host": "*",
31
+ "spaceId": null,
32
+ "note": "an ordinary id covering only itself; never a served host"
33
+ },
34
+ { "resource": "api:", "valid": false, "note": "empty host" },
35
+ {
36
+ "resource": "api:/spaces/9ctvrdn9vedda7efetrhcdakfh4cr2k",
37
+ "valid": false,
38
+ "note": "empty host"
39
+ },
40
+ { "resource": "api:api.dfos.com/", "valid": false, "note": "trailing separator, no child" },
41
+ {
42
+ "resource": "api:api.dfos.com/spaces",
43
+ "valid": false,
44
+ "note": "child segment without an id"
45
+ },
46
+ { "resource": "api:api.dfos.com/spaces/", "valid": false, "note": "empty id" },
47
+ {
48
+ "resource": "api:api.dfos.com/spaces/9ctvrdn9vedda7efetrhcdakfh4cr2",
49
+ "valid": false,
50
+ "note": "30-character id"
51
+ },
52
+ {
53
+ "resource": "api:api.dfos.com/spaces/9ctvrdn9vedda7efetrhcdakfh4cr2k2",
54
+ "valid": false,
55
+ "note": "32-character id"
56
+ },
57
+ {
58
+ "resource": "api:api.dfos.com/spaces/9ctvrdn9vedda7efetrhcdakfh4cr2K",
59
+ "valid": false,
60
+ "note": "uppercase is outside the identifier alphabet"
61
+ },
62
+ {
63
+ "resource": "api:api.dfos.com/spaces/9ctvrdn9vedda7efetrhcdakfh4cr1b",
64
+ "valid": false,
65
+ "note": "1 and b are outside the identifier alphabet"
66
+ },
67
+ {
68
+ "resource": "api:api.dfos.com/spaces/9ctvrdn9vedda7efetrhcdakfh4cr2k/extra",
69
+ "valid": false,
70
+ "note": "the hierarchy is one level deep"
71
+ },
72
+ {
73
+ "resource": "api:api.dfos.com/topics/9ctvrdn9vedda7efetrhcdakfh4cr2k",
74
+ "valid": false,
75
+ "note": "topics is not a registered child"
76
+ },
77
+ {
78
+ "resource": "api:api.dfos.com/SPACES/9ctvrdn9vedda7efetrhcdakfh4cr2k",
79
+ "valid": false,
80
+ "note": "the child segment is lowercase"
81
+ },
82
+ {
83
+ "resource": "api:api.dfos.com/spaces/did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k",
84
+ "valid": false,
85
+ "note": "the id is prefix-stripped, never a full DID"
86
+ }
87
+ ],
88
+ "covers": [
89
+ {
90
+ "entry": "api:api.dfos.com",
91
+ "required": "api:api.dfos.com",
92
+ "covers": true,
93
+ "note": "bare covers bare"
94
+ },
95
+ {
96
+ "entry": "api:api.dfos.com",
97
+ "required": "api:api.dfos.com/spaces/9ctvrdn9vedda7efetrhcdakfh4cr2k",
98
+ "covers": true,
99
+ "note": "bare host is the registered ancestor of every space at that host"
100
+ },
101
+ {
102
+ "entry": "api:api.dfos.com/spaces/9ctvrdn9vedda7efetrhcdakfh4cr2k",
103
+ "required": "api:api.dfos.com/spaces/9ctvrdn9vedda7efetrhcdakfh4cr2k",
104
+ "covers": true,
105
+ "note": "child covers itself"
106
+ },
107
+ {
108
+ "entry": "api:api.dfos.com/spaces/9ctvrdn9vedda7efetrhcdakfh4cr2k",
109
+ "required": "api:api.dfos.com",
110
+ "covers": false,
111
+ "note": "child never covers the bare host"
112
+ },
113
+ {
114
+ "entry": "api:api.dfos.com/spaces/9ctvrdn9vedda7efetrhcdakfh4cr2k",
115
+ "required": "api:api.dfos.com/spaces/cv7n8vkvr64cctf3294h9k4eanhff8z",
116
+ "covers": false,
117
+ "note": "sibling space"
118
+ },
119
+ {
120
+ "entry": "api:api.dfos.com",
121
+ "required": "api:api.example.org/spaces/9ctvrdn9vedda7efetrhcdakfh4cr2k",
122
+ "covers": false,
123
+ "note": "coverage never crosses hosts"
124
+ },
125
+ {
126
+ "entry": "api:api.dfos.com",
127
+ "required": "api:api.dfos.com:8443",
128
+ "covers": false,
129
+ "note": "a non-default port is part of the host"
130
+ },
131
+ {
132
+ "entry": "api:api.dfos.com:8443",
133
+ "required": "api:api.dfos.com:8443/spaces/9ctvrdn9vedda7efetrhcdakfh4cr2k",
134
+ "covers": true,
135
+ "note": "the port travels with the host into the child"
136
+ },
137
+ {
138
+ "entry": "api:*",
139
+ "required": "api:api.dfos.com",
140
+ "covers": false,
141
+ "note": "api:* is an ordinary id, not a wildcard"
142
+ },
143
+ {
144
+ "entry": "api:*",
145
+ "required": "api:*",
146
+ "covers": true,
147
+ "note": "an ordinary id covers itself"
148
+ },
149
+ {
150
+ "entry": "api:api.dfos.com/topics/9ctvrdn9vedda7efetrhcdakfh4cr2k",
151
+ "required": "api:api.dfos.com/topics/9ctvrdn9vedda7efetrhcdakfh4cr2k",
152
+ "covers": false,
153
+ "note": "a malformed api: resource never covers a request, even itself"
154
+ },
155
+ {
156
+ "entry": "api:api.dfos.com",
157
+ "required": "api:api.dfos.com/topics/9ctvrdn9vedda7efetrhcdakfh4cr2k",
158
+ "covers": false,
159
+ "note": "a malformed required resource is never covered"
160
+ },
161
+ {
162
+ "entry": "chain:*",
163
+ "required": "api:api.dfos.com",
164
+ "covers": false,
165
+ "note": "coverage never crosses resource types"
166
+ },
167
+ {
168
+ "entry": "api:api.dfos.com",
169
+ "required": "chain:9ctvrdn9vedda7efetrhcdakfh4cr2k",
170
+ "covers": false,
171
+ "note": "coverage never crosses resource types"
172
+ }
173
+ ],
174
+ "attenuation": [
175
+ {
176
+ "note": "bare host narrows to one space",
177
+ "parent": [{ "resource": "api:api.dfos.com", "action": "read:posts,write:comments" }],
178
+ "child": [
179
+ {
180
+ "resource": "api:api.dfos.com/spaces/9ctvrdn9vedda7efetrhcdakfh4cr2k",
181
+ "action": "read:posts"
182
+ }
183
+ ],
184
+ "valid": true
185
+ },
186
+ {
187
+ "note": "bare host narrows to two spaces",
188
+ "parent": [
189
+ { "resource": "api:api.dfos.com", "action": "read:posts,write:upvotes,write:comments" }
190
+ ],
191
+ "child": [
192
+ {
193
+ "resource": "api:api.dfos.com/spaces/9ctvrdn9vedda7efetrhcdakfh4cr2k",
194
+ "action": "read:posts,write:upvotes"
195
+ },
196
+ {
197
+ "resource": "api:api.dfos.com/spaces/cv7n8vkvr64cctf3294h9k4eanhff8z",
198
+ "action": "read:posts"
199
+ }
200
+ ],
201
+ "valid": true
202
+ },
203
+ {
204
+ "note": "bare host to bare host, fewer actions",
205
+ "parent": [{ "resource": "api:api.dfos.com", "action": "read:profile,read:email" }],
206
+ "child": [{ "resource": "api:api.dfos.com", "action": "read:profile" }],
207
+ "valid": true
208
+ },
209
+ {
210
+ "note": "child widening to the bare host is refused",
211
+ "parent": [
212
+ {
213
+ "resource": "api:api.dfos.com/spaces/9ctvrdn9vedda7efetrhcdakfh4cr2k",
214
+ "action": "read:posts"
215
+ }
216
+ ],
217
+ "child": [{ "resource": "api:api.dfos.com", "action": "read:posts" }],
218
+ "valid": false
219
+ },
220
+ {
221
+ "note": "sibling space is refused",
222
+ "parent": [
223
+ {
224
+ "resource": "api:api.dfos.com/spaces/9ctvrdn9vedda7efetrhcdakfh4cr2k",
225
+ "action": "read:posts"
226
+ }
227
+ ],
228
+ "child": [
229
+ {
230
+ "resource": "api:api.dfos.com/spaces/cv7n8vkvr64cctf3294h9k4eanhff8z",
231
+ "action": "read:posts"
232
+ }
233
+ ],
234
+ "valid": false
235
+ },
236
+ {
237
+ "note": "child to the same child is exact",
238
+ "parent": [
239
+ {
240
+ "resource": "api:api.dfos.com/spaces/9ctvrdn9vedda7efetrhcdakfh4cr2k",
241
+ "action": "read:posts,write:comments"
242
+ }
243
+ ],
244
+ "child": [
245
+ {
246
+ "resource": "api:api.dfos.com/spaces/9ctvrdn9vedda7efetrhcdakfh4cr2k",
247
+ "action": "write:comments"
248
+ }
249
+ ],
250
+ "valid": true
251
+ },
252
+ {
253
+ "note": "resource narrows but the action widens",
254
+ "parent": [{ "resource": "api:api.dfos.com", "action": "read:posts" }],
255
+ "child": [
256
+ {
257
+ "resource": "api:api.dfos.com/spaces/9ctvrdn9vedda7efetrhcdakfh4cr2k",
258
+ "action": "read:posts,write:posts"
259
+ }
260
+ ],
261
+ "valid": false
262
+ },
263
+ {
264
+ "note": "another host is refused",
265
+ "parent": [{ "resource": "api:api.dfos.com", "action": "read:posts" }],
266
+ "child": [
267
+ {
268
+ "resource": "api:api.example.org/spaces/9ctvrdn9vedda7efetrhcdakfh4cr2k",
269
+ "action": "read:posts"
270
+ }
271
+ ],
272
+ "valid": false
273
+ },
274
+ {
275
+ "note": "an account-level token narrows onto a child; the entry is inert, not refused",
276
+ "parent": [{ "resource": "api:api.dfos.com", "action": "read:profile,read:posts" }],
277
+ "child": [
278
+ {
279
+ "resource": "api:api.dfos.com/spaces/9ctvrdn9vedda7efetrhcdakfh4cr2k",
280
+ "action": "read:profile"
281
+ }
282
+ ],
283
+ "valid": true
284
+ },
285
+ {
286
+ "note": "a malformed api: id is carried only by byte-identical parent",
287
+ "parent": [
288
+ {
289
+ "resource": "api:api.dfos.com/topics/9ctvrdn9vedda7efetrhcdakfh4cr2k",
290
+ "action": "read:posts"
291
+ }
292
+ ],
293
+ "child": [
294
+ {
295
+ "resource": "api:api.dfos.com/topics/9ctvrdn9vedda7efetrhcdakfh4cr2k",
296
+ "action": "read:posts"
297
+ }
298
+ ],
299
+ "valid": true
300
+ },
301
+ {
302
+ "note": "a bare host does not carry a malformed api: id",
303
+ "parent": [{ "resource": "api:api.dfos.com", "action": "read:posts" }],
304
+ "child": [
305
+ {
306
+ "resource": "api:api.dfos.com/topics/9ctvrdn9vedda7efetrhcdakfh4cr2k",
307
+ "action": "read:posts"
308
+ }
309
+ ],
310
+ "valid": false
311
+ },
312
+ {
313
+ "note": "mailbox: stays exact — the hierarchy is api:-only",
314
+ "parent": [{ "resource": "mailbox:9ctvrdn9vedda7efetrhcdakfh4cr2k", "action": "deposit" }],
315
+ "child": [
316
+ {
317
+ "resource": "mailbox:9ctvrdn9vedda7efetrhcdakfh4cr2k/spaces/cv7n8vkvr64cctf3294h9k4eanhff8z",
318
+ "action": "deposit"
319
+ }
320
+ ],
321
+ "valid": false
322
+ }
323
+ ],
324
+ "matches": [
325
+ {
326
+ "note": "bare host entry satisfies a space-addressed demand",
327
+ "att": [{ "resource": "api:api.dfos.com", "action": "read:posts,write:comments" }],
328
+ "resource": "api:api.dfos.com/spaces/9ctvrdn9vedda7efetrhcdakfh4cr2k",
329
+ "action": "write:comments",
330
+ "covers": true
331
+ },
332
+ {
333
+ "note": "child entry satisfies its own space",
334
+ "att": [
335
+ {
336
+ "resource": "api:api.dfos.com/spaces/9ctvrdn9vedda7efetrhcdakfh4cr2k",
337
+ "action": "read:posts"
338
+ }
339
+ ],
340
+ "resource": "api:api.dfos.com/spaces/9ctvrdn9vedda7efetrhcdakfh4cr2k",
341
+ "action": "read:posts",
342
+ "covers": true
343
+ },
344
+ {
345
+ "note": "child entry does not satisfy the bare host",
346
+ "att": [
347
+ {
348
+ "resource": "api:api.dfos.com/spaces/9ctvrdn9vedda7efetrhcdakfh4cr2k",
349
+ "action": "read:profile"
350
+ }
351
+ ],
352
+ "resource": "api:api.dfos.com",
353
+ "action": "read:profile",
354
+ "covers": false
355
+ },
356
+ {
357
+ "note": "child entry does not satisfy a sibling space",
358
+ "att": [
359
+ {
360
+ "resource": "api:api.dfos.com/spaces/9ctvrdn9vedda7efetrhcdakfh4cr2k",
361
+ "action": "read:posts"
362
+ }
363
+ ],
364
+ "resource": "api:api.dfos.com/spaces/cv7n8vkvr64cctf3294h9k4eanhff8z",
365
+ "action": "read:posts",
366
+ "covers": false
367
+ },
368
+ {
369
+ "note": "the action must be in the covering entry's set",
370
+ "att": [{ "resource": "api:api.dfos.com", "action": "read:posts" }],
371
+ "resource": "api:api.dfos.com/spaces/9ctvrdn9vedda7efetrhcdakfh4cr2k",
372
+ "action": "write:comments",
373
+ "covers": false
374
+ },
375
+ {
376
+ "note": "any one entry may cover; the scan does not stop at the first api: entry",
377
+ "att": [
378
+ {
379
+ "resource": "api:api.dfos.com/spaces/cv7n8vkvr64cctf3294h9k4eanhff8z",
380
+ "action": "read:posts"
381
+ },
382
+ {
383
+ "resource": "api:api.dfos.com/spaces/9ctvrdn9vedda7efetrhcdakfh4cr2k",
384
+ "action": "read:posts,write:upvotes"
385
+ }
386
+ ],
387
+ "resource": "api:api.dfos.com/spaces/9ctvrdn9vedda7efetrhcdakfh4cr2k",
388
+ "action": "write:upvotes",
389
+ "covers": true
390
+ },
391
+ {
392
+ "note": "host mismatch",
393
+ "att": [{ "resource": "api:api.dfos.com", "action": "read:posts" }],
394
+ "resource": "api:api.example.org/spaces/9ctvrdn9vedda7efetrhcdakfh4cr2k",
395
+ "action": "read:posts",
396
+ "covers": false
397
+ },
398
+ {
399
+ "note": "existing bare-host behavior is unchanged: exact host, exact action",
400
+ "att": [{ "resource": "api:api.dfos.com", "action": "read:profile,read:email" }],
401
+ "resource": "api:api.dfos.com",
402
+ "action": "read:email",
403
+ "covers": true
404
+ },
405
+ {
406
+ "note": "read:* is a literal token and matches no real route",
407
+ "att": [{ "resource": "api:api.dfos.com", "action": "read:*" }],
408
+ "resource": "api:api.dfos.com/spaces/9ctvrdn9vedda7efetrhcdakfh4cr2k",
409
+ "action": "read:posts",
410
+ "covers": false
411
+ }
412
+ ]
413
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metalabel/dfos-protocol",
3
- "version": "0.53.0",
3
+ "version": "0.54.0",
4
4
  "type": "module",
5
5
  "description": "DFOS Protocol — Ed25519 signed chain primitives, services, credentials, and verification",
6
6
  "license": "MIT",