@metalabel/dfos-protocol 0.52.1 → 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 */
@@ -86,6 +86,25 @@ declare const verifyIdentityChain: (input: {
86
86
  */
87
87
  asOf?: string;
88
88
  }) => Promise<VerifiedIdentity>;
89
+ /**
90
+ * The trusted state cannot be extended incrementally: it does not carry the
91
+ * chain-wide id-to-material binding. The caller replays the log.
92
+ *
93
+ * THERE IS NO RECONSTRUCTION, and that is the whole point. `seenKeys` is a
94
+ * monotonic index that never forgets a key id once the chain declared it — even
95
+ * after the id is dropped from the declared arrays — and nothing else in the
96
+ * state remembers it: an id introduced without a proof and then removed is in
97
+ * neither `declared` (removed) nor `provedKeys` (never proved). Reading the
98
+ * binding off those two arrays therefore treats a re-declaration of that id,
99
+ * bound to DIFFERENT material, as a fresh introduction and accepts it — while a
100
+ * full replay of the same operations rejects it. A fast path that accepts what
101
+ * its own re-verification refuses is worse than one that says it cannot answer.
102
+ *
103
+ * The Go twin is `ErrIdentityStateNoSeenKeys`, with this exact message.
104
+ */
105
+ declare class IdentityStateNoSeenKeysError extends Error {
106
+ constructor();
107
+ }
89
108
  /**
90
109
  * Verify a single new operation against already-verified identity state
91
110
  *
@@ -100,7 +119,8 @@ declare const verifyIdentityChain: (input: {
100
119
  * is REJECTED on this path exactly as a replay of the same two operations would
101
120
  * reject it. Without that the fast path would accept a chain its own
102
121
  * re-verification refuses, and a relay's linear path is the path almost every
103
- * operation takes.
122
+ * operation takes. A state that does not carry `seenKeys` is therefore not
123
+ * extensible at all — see `IdentityStateNoSeenKeysError`.
104
124
  *
105
125
  * THE POSSESSION FOLD RUNS HERE TOO, and it needs both halves of the trusted
106
126
  * state: the EFFECTIVE arrays (to know what an introduction is a transition out
@@ -130,8 +150,16 @@ type AnchorKind = 'chain' | 'artifact' | 'invalid';
130
150
  * Enforce the services byte cap on the CBOR-encoded array — same encoding the
131
151
  * wire uses, so the bound is identical across implementations. Mirrors the
132
152
  * artifact payload size check.
153
+ *
154
+ * MEASURE THE RAW DECODED PAYLOAD'S ARRAY, never zod's output. The parameter is
155
+ * `unknown` to make that hard to get wrong: the schema is a validator, not a
156
+ * canonicalizer, and its output object is freshly built — a `__proto__` data
157
+ * member (constructible only by `JSON.parse`) does not survive the copy. Handing
158
+ * this the parsed `op.services` measured 19 bytes for a 33,032-byte signed
159
+ * array, which Go's `parseServices` — reading the wire map directly — rejects.
160
+ * Same rule as the operation CID, and for the same reason.
133
161
  */
134
- declare const assertServicesWithinCap: (services: ServiceEntry[]) => Promise<void>;
162
+ declare const assertServicesWithinCap: (services: unknown) => Promise<void>;
135
163
  /**
136
164
  * Classify a ContentAnchor target by structural form. Resolvers dispatch on the
137
165
  * result: 'chain' → resolve a content chain by contentId; 'artifact' → fetch by
@@ -560,4 +588,4 @@ declare const assertCanonicalSignRequestPayload: (payloadTyp: string, payloadByt
560
588
  subject: string;
561
589
  }) => void;
562
590
 
563
- export { type AnchorKind, ArtifactPayload, ContentOperation, CountersignPayload, type CreditClaimFailureReason, CreditClaimVerifyError, type CreditEntry, type CreditEntryState, ED25519_PRIV_MULTICODEC, ED25519_PUB_MULTICODEC, IdentityOperation, RECOGNIZED_SERVICE_TYPES, ServiceEntry, type SignRequestFailureReason, SignRequestVerifyError, Signer, type VerifiedArtifact, type VerifiedContentChain, type VerifiedCountersignature, type VerifiedCreditClaim, type VerifiedCreditEntry, VerifiedIdentity, type VerifiedRevocation, type 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 };
591
+ export { type AnchorKind, ArtifactPayload, ContentOperation, CountersignPayload, type CreditClaimFailureReason, CreditClaimVerifyError, type CreditEntry, type CreditEntryState, ED25519_PRIV_MULTICODEC, ED25519_PUB_MULTICODEC, IdentityOperation, IdentityStateNoSeenKeysError, RECOGNIZED_SERVICE_TYPES, ServiceEntry, type SignRequestFailureReason, SignRequestVerifyError, Signer, type VerifiedArtifact, type VerifiedContentChain, type VerifiedCountersignature, type VerifiedCreditClaim, type VerifiedCreditEntry, VerifiedIdentity, type VerifiedRevocation, type 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 };
@@ -8,6 +8,7 @@ import {
8
8
  CreditClaimVerifyError,
9
9
  DeclaredKeyState,
10
10
  IdentityOperation,
11
+ IdentityStateNoSeenKeysError,
11
12
  Iso8601,
12
13
  MAX_ARTIFACT_PAYLOAD_SIZE,
13
14
  MAX_CREDIT_CLAIM_SIZE,
@@ -52,9 +53,9 @@ import {
52
53
  verifyIdentityExtensionFromTrustedState,
53
54
  verifyRevocation,
54
55
  verifySignRequest
55
- } from "../chunk-Q7FREWMY.js";
56
- import "../chunk-FZHHSC5N.js";
57
- import "../chunk-BMIOY5W5.js";
56
+ } from "../chunk-QIM3KZXZ.js";
57
+ import "../chunk-RPA44ND4.js";
58
+ import "../chunk-ISHAX5WN.js";
58
59
  import {
59
60
  ED25519_PRIV_MULTICODEC,
60
61
  ED25519_PUB_MULTICODEC,
@@ -62,7 +63,7 @@ import {
62
63
  decodeMultikey,
63
64
  encodeEd25519Multikey
64
65
  } from "../chunk-F7RHI2CK.js";
65
- import "../chunk-KAW4MT4P.js";
66
+ import "../chunk-NUIIVAZ3.js";
66
67
  export {
67
68
  ARTIFACT_CID_ANCHOR_RE,
68
69
  ArtifactPayload,
@@ -75,6 +76,7 @@ export {
75
76
  ED25519_PRIV_MULTICODEC,
76
77
  ED25519_PUB_MULTICODEC,
77
78
  IdentityOperation,
79
+ IdentityStateNoSeenKeysError,
78
80
  Iso8601,
79
81
  MAX_ARTIFACT_PAYLOAD_SIZE,
80
82
  MAX_CREDIT_CLAIM_SIZE,
@@ -9,7 +9,7 @@ import {
9
9
  isValidEd25519Signature,
10
10
  sha256,
11
11
  signPayloadEd25519
12
- } from "./chunk-KAW4MT4P.js";
12
+ } from "./chunk-NUIIVAZ3.js";
13
13
 
14
14
  // src/key-proof/role-set.ts
15
15
  var KEY_ROLES = ["auth", "assert", "controller"];
@@ -161,10 +161,15 @@ var assertJwsProfile = (header, makeError) => {
161
161
  var decodeJwsSegment = (segmentB64) => {
162
162
  let text;
163
163
  try {
164
- text = new TextDecoder("utf-8", { fatal: true }).decode(base64urlDecode(segmentB64));
164
+ text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(
165
+ base64urlDecode(segmentB64)
166
+ );
165
167
  } catch {
166
168
  return null;
167
169
  }
170
+ if (text.charCodeAt(0) === 65279) {
171
+ throw new Error("leading byte order mark is not canonicalizable");
172
+ }
168
173
  assertCanonicalJsonText(text);
169
174
  let value;
170
175
  try {
@@ -6,13 +6,13 @@ import {
6
6
  matchesResource,
7
7
  verifyDFOSCredential,
8
8
  verifyDelegationChain
9
- } from "./chunk-FZHHSC5N.js";
9
+ } from "./chunk-RPA44ND4.js";
10
10
  import {
11
11
  KEY_ADD_JWS_TYP,
12
12
  KEY_ROLES,
13
13
  unsafeKeyProofSubject,
14
14
  verifyChainKeyProof
15
- } from "./chunk-BMIOY5W5.js";
15
+ } from "./chunk-ISHAX5WN.js";
16
16
  import {
17
17
  decodeEd25519PublicMultikey
18
18
  } from "./chunk-F7RHI2CK.js";
@@ -25,7 +25,7 @@ import {
25
25
  decodeJwsUnsafe,
26
26
  generateIdNoPrefix,
27
27
  verifyJws
28
- } from "./chunk-KAW4MT4P.js";
28
+ } from "./chunk-NUIIVAZ3.js";
29
29
 
30
30
  // src/chain/schemas.ts
31
31
  import { z } from "zod";
@@ -540,7 +540,7 @@ var verifyIdentityChain = async (input) => {
540
540
  });
541
541
  if (op.services) {
542
542
  try {
543
- await assertServicesWithinCap(op.services);
543
+ await assertServicesWithinCap(decoded.payload["services"]);
544
544
  } catch (e) {
545
545
  throw new Error(`log[${idx}]: ${e.message}`);
546
546
  }
@@ -661,14 +661,19 @@ var verifyIdentityChain = async (input) => {
661
661
  seenKeys: [...state.seenKeys.values()]
662
662
  };
663
663
  };
664
+ var IdentityStateNoSeenKeysError = class extends Error {
665
+ constructor() {
666
+ super("identity state has no seenKeys; replay the chain");
667
+ this.name = "IdentityStateNoSeenKeysError";
668
+ }
669
+ };
664
670
  var verifyIdentityExtensionFromTrustedState = async (input) => {
665
671
  const { currentState, headCID, lastCreatedAt, newOp } = input;
666
672
  const priorEffective = keyStateOf(currentState);
667
673
  const priorDeclared = currentState.declared ?? priorEffective;
668
674
  const priorProved = currentState.provedKeys ?? priorEffective;
669
- const seenKeys = keyMaterialIndex(
670
- currentState.seenKeys ?? [...flatKeys(priorDeclared), ...flatKeys(priorProved)]
671
- );
675
+ if (!currentState.seenKeys?.length) throw new IdentityStateNoSeenKeysError();
676
+ const seenKeys = keyMaterialIndex(currentState.seenKeys);
672
677
  const priorSeenKeys = [...seenKeys.values()];
673
678
  const decoded = decodeJwsUnsafe(newOp);
674
679
  if (!decoded) throw new Error("failed to decode JWS");
@@ -733,7 +738,7 @@ var verifyIdentityExtensionFromTrustedState = async (input) => {
733
738
  throw new Error("cannot repeat key ids in same usage");
734
739
  }
735
740
  });
736
- if (op.services) await assertServicesWithinCap(op.services);
741
+ if (op.services) await assertServicesWithinCap(decoded.payload["services"]);
737
742
  }
738
743
  const newState = (() => {
739
744
  switch (op.type) {
@@ -1679,6 +1684,7 @@ export {
1679
1684
  anchorsByLabel,
1680
1685
  signIdentityOperation,
1681
1686
  verifyIdentityChain,
1687
+ IdentityStateNoSeenKeysError,
1682
1688
  verifyIdentityExtensionFromTrustedState,
1683
1689
  signContentOperation,
1684
1690
  verifyContentChain,
@@ -10,7 +10,7 @@ import {
10
10
  decodeJwsUnsafe,
11
11
  sha256,
12
12
  verifyJws
13
- } from "./chunk-KAW4MT4P.js";
13
+ } from "./chunk-NUIIVAZ3.js";
14
14
 
15
15
  // src/credentials/schemas.ts
16
16
  import { z } from "zod";
@@ -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,