agent-passport-system 2.6.0 → 2.7.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.
Files changed (40) hide show
  1. package/README.md +22 -3
  2. package/dist/src/adapters/oauth-id-jag/index.d.ts +21 -0
  3. package/dist/src/adapters/oauth-id-jag/index.d.ts.map +1 -0
  4. package/dist/src/adapters/oauth-id-jag/index.js +224 -0
  5. package/dist/src/adapters/oauth-id-jag/index.js.map +1 -0
  6. package/dist/src/adapters/oauth-id-jag/types.d.ts +228 -0
  7. package/dist/src/adapters/oauth-id-jag/types.d.ts.map +1 -0
  8. package/dist/src/adapters/oauth-id-jag/types.js +18 -0
  9. package/dist/src/adapters/oauth-id-jag/types.js.map +1 -0
  10. package/dist/src/adapters/oauth-id-jag/verify.d.ts +12 -0
  11. package/dist/src/adapters/oauth-id-jag/verify.d.ts.map +1 -0
  12. package/dist/src/adapters/oauth-id-jag/verify.js +81 -0
  13. package/dist/src/adapters/oauth-id-jag/verify.js.map +1 -0
  14. package/dist/src/core/action-ref.d.ts +7 -3
  15. package/dist/src/core/action-ref.d.ts.map +1 -1
  16. package/dist/src/core/action-ref.js +7 -3
  17. package/dist/src/core/action-ref.js.map +1 -1
  18. package/dist/src/core/delegation.d.ts +5 -0
  19. package/dist/src/core/delegation.d.ts.map +1 -1
  20. package/dist/src/core/delegation.js +43 -13
  21. package/dist/src/core/delegation.js.map +1 -1
  22. package/dist/src/index.d.ts +4 -1
  23. package/dist/src/index.d.ts.map +1 -1
  24. package/dist/src/index.js +8 -1
  25. package/dist/src/index.js.map +1 -1
  26. package/dist/src/v2/key-resolution/did-cycles.d.ts +38 -25
  27. package/dist/src/v2/key-resolution/did-cycles.d.ts.map +1 -1
  28. package/dist/src/v2/key-resolution/did-cycles.js +147 -77
  29. package/dist/src/v2/key-resolution/did-cycles.js.map +1 -1
  30. package/dist/src/v2/key-resolution/index.d.ts +1 -1
  31. package/dist/src/v2/key-resolution/index.d.ts.map +1 -1
  32. package/dist/src/v2/key-resolution/index.js +1 -1
  33. package/dist/src/v2/key-resolution/index.js.map +1 -1
  34. package/dist/src/v2/key-resolution/resolver.d.ts.map +1 -1
  35. package/dist/src/v2/key-resolution/resolver.js +52 -13
  36. package/dist/src/v2/key-resolution/resolver.js.map +1 -1
  37. package/dist/src/v2/key-resolution/types.d.ts +27 -3
  38. package/dist/src/v2/key-resolution/types.d.ts.map +1 -1
  39. package/dist/src/v2/key-resolution/types.js.map +1 -1
  40. package/package.json +3 -3
@@ -1,23 +1,32 @@
1
1
  import type { Ed25519JWK, JWKS } from './types.js';
2
2
  export interface ParsedDIDCycles {
3
- /** https JWKS endpoint the DID maps to. */
4
- jwksUrl: string;
3
+ /** Lowercase hex sha256 of `server_id`, as carried in the DID subject.
4
+ * A BINDING value: the cycles verify layer checks it against
5
+ * sha256(envelope.server_id). Not a locator. */
6
+ serverIdHash: string;
5
7
  /** kid taken from the DID-URL fragment, if present. */
6
8
  kid?: string;
7
9
  }
8
10
  /**
9
11
  * Parse a did:cycles identifier (optionally with a #fragment) into its
10
- * JWKS URL and kid. Mirrors didWebToUrl: colon-separated segments,
11
- * each percent-decoded, segment[0] is the authority, the rest is the
12
- * path; a bare authority uses /.well-known/jwks.json.
12
+ * `server_id` hash and kid. The fragment is split off BEFORE the subject
13
+ * is read so a `#` never lands inside the hash.
13
14
  *
14
- * Throws on a structurally invalid did:cycles string. The fragment is
15
- * split off BEFORE segment parsing so a `#` never lands inside a path
16
- * segment.
15
+ * Throws on a structurally invalid did:cycles string (wrong method, or a
16
+ * subject that is not a 64-char lowercase hex sha256).
17
17
  */
18
18
  export declare function parseDIDCycles(did: string): ParsedDIDCycles;
19
19
  /** True if the value looks like a did:cycles identifier. */
20
20
  export declare function isDIDCycles(value: string): boolean;
21
+ /**
22
+ * The JWK Set URL for a Cycles server, API-base-relative: append
23
+ * `/.well-known/cycles-jwks.json` to the verbatim `server_id` (collapsing
24
+ * only a doubled `/` at the join). Deliberately NOT origin-rooted —
25
+ * `server_id` carries its path (e.g. `https://cycles.example.com/v1`), so
26
+ * the set lives at `…/v1/.well-known/cycles-jwks.json`, keeping key
27
+ * authority anchored to the base the DID hash commits to.
28
+ */
29
+ export declare function cyclesJwksUrl(serverId: string): string;
21
30
  export type JWKSelection = {
22
31
  ok: true;
23
32
  jwk: Ed25519JWK;
@@ -28,26 +37,30 @@ export type JWKSelection = {
28
37
  status: 'not_found' | 'ambiguous' | 'malformed';
29
38
  reason: string;
30
39
  };
40
+ /** Selection criteria. Legacy callers pass a bare kid string; the cycles
41
+ * path passes the window + raw-key form. */
42
+ export interface SelectKeyOptions {
43
+ /** Match the JWK whose kid strictly equals this (exact, case-sensitive). */
44
+ kid?: string;
45
+ /** Window gate: keep only keys whose [cycles_nbf_ms, cycles_exp_ms) covers
46
+ * this issuance time (epoch ms). Omitted ⇒ no window gate (legacy). */
47
+ issuedAtMs?: number;
48
+ /** Raw-hex signer match: keep only keys whose `x` decodes to these 32 bytes
49
+ * (64-char hex). Used when the envelope's signer_did is a raw key. */
50
+ publicKeyHex?: string;
51
+ }
52
+ /**
53
+ * Select exactly one Ed25519 verification key from a JWKS. Filters, in
54
+ * order: Ed25519 signing candidacy, optional raw-hex `x` match, optional
55
+ * kid match, optional validity-window gate; then requires the survivor to
56
+ * be unique. Never silently falls back to a different key. Legacy callers
57
+ * may pass a bare kid string.
58
+ */
59
+ export declare function selectKey(jwks: JWKS, sel?: string | SelectKeyOptions): JWKSelection;
31
60
  /**
32
61
  * Validate that a parsed object is a well-formed JWKS with a non-empty
33
62
  * `keys` array. Returns the JWKS on success or null on any structural
34
- * problem. Does NOT validate individual JWK key material; that happens
35
- * during candidate filtering / selection.
63
+ * problem.
36
64
  */
37
65
  export declare function asJWKS(body: unknown): JWKS | null;
38
- /**
39
- * Select exactly one Ed25519 verification key from a JWKS by kid.
40
- *
41
- * 1. Filter to Ed25519 signing candidates.
42
- * 2. If a kid is requested: the candidate whose kid strictly equals it
43
- * must be unique. Zero matches -> not_found. Duplicate kids ->
44
- * ambiguous. kid comparison is exact, case-sensitive.
45
- * 3. If no kid is requested: exactly one candidate -> use it; more than
46
- * one -> ambiguous; zero -> not_found.
47
- * 4. Decode `x` -> 32 bytes -> hex. A candidate that survives filtering
48
- * but whose `x` is not 32 bytes is malformed.
49
- *
50
- * Never silently falls back to a different kid than requested.
51
- */
52
- export declare function selectKey(jwks: JWKS, requestedKid?: string): JWKSelection;
53
66
  //# sourceMappingURL=did-cycles.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"did-cycles.d.ts","sourceRoot":"","sources":["../../../../src/v2/key-resolution/did-cycles.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAElD,MAAM,WAAW,eAAe;IAC9B,2CAA2C;IAC3C,OAAO,EAAE,MAAM,CAAA;IACf,uDAAuD;IACvD,GAAG,CAAC,EAAE,MAAM,CAAA;CACb;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,CAkC3D;AAED,4DAA4D;AAC5D,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAElD;AAED,MAAM,MAAM,YAAY,GACpB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,GAAG,EAAE,UAAU,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GACjE;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,WAAW,GAAG,WAAW,GAAG,WAAW,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAElF;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,GAAG,IAAI,CAKjD;AAmCD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,YAAY,CAgCzE"}
1
+ {"version":3,"file":"did-cycles.d.ts","sourceRoot":"","sources":["../../../../src/v2/key-resolution/did-cycles.ts"],"names":[],"mappings":"AAuCA,OAAO,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAElD,MAAM,WAAW,eAAe;IAC9B;;qDAEiD;IACjD,YAAY,EAAE,MAAM,CAAA;IACpB,uDAAuD;IACvD,GAAG,CAAC,EAAE,MAAM,CAAA;CACb;AAID;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,CAwB3D;AAED,4DAA4D;AAC5D,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAElD;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGtD;AAED,MAAM,MAAM,YAAY,GACpB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,GAAG,EAAE,UAAU,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GACjE;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,WAAW,GAAG,WAAW,GAAG,WAAW,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAElF;6CAC6C;AAC7C,MAAM,WAAW,gBAAgB;IAC/B,4EAA4E;IAC5E,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;4EACwE;IACxE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;2EACuE;IACvE,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAgDD;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,gBAAgB,GAAG,YAAY,CA6EnF;AAED;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,GAAG,IAAI,CAKjD"}
@@ -1,37 +1,54 @@
1
1
  // Copyright 2024-2026 Tymofii Pidlisnyi. Apache-2.0 license. See LICENSE.
2
2
  // ══════════════════════════════════════════════════════════════════
3
- // did:cycles: DID-to-JWKS mapping and Ed25519 JWK selection (M3)
3
+ // did:cycles: hash-bound DIDJWKS mapping and Ed25519 JWK selection
4
4
  // ══════════════════════════════════════════════════════════════════
5
- // did:cycles is an AEOESS-defined, did:web-style, HTTPS-anchored
6
- // method. There is no external registry to conform to; this file IS
7
- // its specification. It mirrors didWebToUrl() in core/did-interop.ts
8
- // exactly, except the method name is `cycles` and the resolved
9
- // document is a JWKS (RFC 7517) rather than a DID Document.
5
+ // did:cycles is an AEOESS-defined method for Cycles evidence signers.
6
+ // The design is settled with the Cycles maintainers on
7
+ // runcycles/cycles-protocol (issue #43, spec drafts/cycles-evidence-v0.1).
8
+ // There is no external registry to conform to; this file IS its
9
+ // specification on the APS side.
10
10
  //
11
- // did:cycles:example.com -> https://example.com/.well-known/jwks.json
12
- // did:cycles:example.com:agents:7 -> https://example.com/agents/7/jwks.json
13
- // did:cycles:example.com%3A8443 -> https://example.com:8443/.well-known/jwks.json
11
+ // SUBJECT IS A HASH, NOT A HOST (the load-bearing difference from a
12
+ // did:web-style method):
14
13
  //
15
- // A DID-URL fragment is the kid:
16
- // did:cycles:example.com#agent-7-2026
17
- // selects the JWK whose kid === "agent-7-2026" from the resolved set.
14
+ // did:cycles:<sha256(server_id)> (optionally with #<kid>)
15
+ //
16
+ // The subject is the lowercase hex sha256 of the envelope's `server_id`
17
+ // string — a BINDING, not a locator. The DID does NOT encode where the
18
+ // keys live; a hash cannot be reversed to a URL. The JWK Set is located
19
+ // from the envelope's own `server_id`:
20
+ //
21
+ // {server_id}/.well-known/cycles-jwks.json (API-base-relative)
22
+ //
23
+ // and the DID's hash is checked against `sha256(server_id)` so the key
24
+ // authority stays anchored to the exact `server_id` (path included) the
25
+ // envelope claims — closing the cross-tenant key-confusion path that an
26
+ // origin-rooted JWKS would open under a path-multi-tenant host. (The
27
+ // sha256 binding check itself is performed by the cycles verify layer,
28
+ // which holds the envelope's `server_id` and the sha256 primitive.)
29
+ //
30
+ // A DID-URL fragment is the kid: did:cycles:<hash>#2026-06 selects the
31
+ // JWK whose kid === "2026-06" from the resolved set.
32
+ //
33
+ // Key selection adds a validity-WINDOW gate over the M3 kid match: pick
34
+ // the key whose [cycles_nbf_ms, cycles_exp_ms) covers the envelope's
35
+ // issued_at_ms, never "the current key" — so a receipt verified after a
36
+ // rotation is checked against the key valid when it was signed.
18
37
  // ══════════════════════════════════════════════════════════════════
19
38
  import { decodeBase64Url, bytesToHex } from './base64url.js';
39
+ const SERVER_ID_HASH_RE = /^[0-9a-f]{64}$/;
20
40
  /**
21
41
  * Parse a did:cycles identifier (optionally with a #fragment) into its
22
- * JWKS URL and kid. Mirrors didWebToUrl: colon-separated segments,
23
- * each percent-decoded, segment[0] is the authority, the rest is the
24
- * path; a bare authority uses /.well-known/jwks.json.
42
+ * `server_id` hash and kid. The fragment is split off BEFORE the subject
43
+ * is read so a `#` never lands inside the hash.
25
44
  *
26
- * Throws on a structurally invalid did:cycles string. The fragment is
27
- * split off BEFORE segment parsing so a `#` never lands inside a path
28
- * segment.
45
+ * Throws on a structurally invalid did:cycles string (wrong method, or a
46
+ * subject that is not a 64-char lowercase hex sha256).
29
47
  */
30
48
  export function parseDIDCycles(did) {
31
49
  if (typeof did !== 'string') {
32
50
  throw new Error('did:cycles must be a string');
33
51
  }
34
- // Split the DID-URL fragment (the kid) off first.
35
52
  const hashIndex = did.indexOf('#');
36
53
  let kid;
37
54
  let core = did;
@@ -43,42 +60,30 @@ export function parseDIDCycles(did) {
43
60
  }
44
61
  }
45
62
  const parts = core.split(':');
46
- if (parts.length < 3 || parts[0] !== 'did' || parts[1] !== 'cycles') {
63
+ if (parts.length !== 3 || parts[0] !== 'did' || parts[1] !== 'cycles') {
47
64
  throw new Error(`Invalid did:cycles format: ${core}`);
48
65
  }
49
- // Everything after "did:cycles:" is authority-and-path, colon-separated.
50
- const segments = parts.slice(2).map(s => decodeURIComponent(s));
51
- const authority = segments[0];
52
- if (!authority) {
53
- throw new Error('did:cycles must include an authority');
54
- }
55
- let jwksUrl;
56
- if (segments.length === 1) {
57
- jwksUrl = `https://${authority}/.well-known/jwks.json`;
66
+ const serverIdHash = parts[2];
67
+ if (!SERVER_ID_HASH_RE.test(serverIdHash)) {
68
+ throw new Error('did:cycles subject must be a 64-char lowercase hex sha256(server_id)');
58
69
  }
59
- else {
60
- const path = segments.slice(1).join('/');
61
- jwksUrl = `https://${authority}/${path}/jwks.json`;
62
- }
63
- return { jwksUrl, kid };
70
+ return { serverIdHash, kid };
64
71
  }
65
72
  /** True if the value looks like a did:cycles identifier. */
66
73
  export function isDIDCycles(value) {
67
74
  return typeof value === 'string' && value.startsWith('did:cycles:');
68
75
  }
69
76
  /**
70
- * Validate that a parsed object is a well-formed JWKS with a non-empty
71
- * `keys` array. Returns the JWKS on success or null on any structural
72
- * problem. Does NOT validate individual JWK key material; that happens
73
- * during candidate filtering / selection.
77
+ * The JWK Set URL for a Cycles server, API-base-relative: append
78
+ * `/.well-known/cycles-jwks.json` to the verbatim `server_id` (collapsing
79
+ * only a doubled `/` at the join). Deliberately NOT origin-rooted —
80
+ * `server_id` carries its path (e.g. `https://cycles.example.com/v1`), so
81
+ * the set lives at `…/v1/.well-known/cycles-jwks.json`, keeping key
82
+ * authority anchored to the base the DID hash commits to.
74
83
  */
75
- export function asJWKS(body) {
76
- if (!body || typeof body !== 'object')
77
- return null;
78
- const keys = body.keys;
79
- if (!Array.isArray(keys) || keys.length === 0)
80
- return null;
81
- return { keys: keys };
84
+ export function cyclesJwksUrl(serverId) {
85
+ const base = serverId.endsWith('/') ? serverId.slice(0, -1) : serverId;
86
+ return `${base}/.well-known/cycles-jwks.json`;
82
87
  }
83
88
  /**
84
89
  * Is this JWK an admissible Ed25519 SIGNING candidate?
@@ -86,7 +91,6 @@ export function asJWKS(body) {
86
91
  * (use absent || use === "sig")
87
92
  * (alg absent || alg === "EdDSA")
88
93
  * (key_ops absent || includes "verify")
89
- * X-curves, enc keys, and non-EdDSA algs are excluded here.
90
94
  */
91
95
  function isEd25519SigningCandidate(jwk) {
92
96
  if (!jwk || typeof jwk !== 'object')
@@ -98,6 +102,11 @@ function isEd25519SigningCandidate(jwk) {
98
102
  return false;
99
103
  if (typeof k.x !== 'string' || k.x.length === 0)
100
104
  return false;
105
+ // A published verification key set must be public-only. A JWK carrying a
106
+ // private `d` member is leaked private material / a misconfiguration —
107
+ // reject it (fail-closed), never select it.
108
+ if (k.d !== undefined)
109
+ return false;
101
110
  if (k.use !== undefined && k.use !== 'sig')
102
111
  return false;
103
112
  if (k.alg !== undefined && k.alg !== 'EdDSA')
@@ -108,60 +117,121 @@ function isEd25519SigningCandidate(jwk) {
108
117
  }
109
118
  return true;
110
119
  }
111
- /**
112
- * Decode a candidate's `x` to a 32-byte Ed25519 public key, returned
113
- * as 64-char lowercase hex. Returns null if `x` does not base64url-
114
- * decode to exactly 32 bytes (malformed key material).
115
- */
120
+ /** Decode a candidate's `x` to a 32-byte key as 64-char lowercase hex, or
121
+ * null if `x` does not base64url-decode to exactly 32 bytes. */
116
122
  function candidateToHex(jwk) {
117
123
  const bytes = decodeBase64Url(jwk.x);
118
124
  if (!bytes || bytes.length !== 32)
119
125
  return null;
120
126
  return bytesToHex(bytes);
121
127
  }
128
+ /** `cycles_nbf_ms <= t AND (cycles_exp_ms absent/null OR t < cycles_exp_ms)`.
129
+ * A non-integer `cycles_nbf_ms` (or a non-integer present `cycles_exp_ms`)
130
+ * makes the window unusable → the key does not cover `t`. */
131
+ function windowCovers(jwk, t) {
132
+ const nbf = jwk.cycles_nbf_ms;
133
+ if (typeof nbf !== 'number' || !Number.isInteger(nbf))
134
+ return false;
135
+ if (t < nbf)
136
+ return false;
137
+ const exp = jwk.cycles_exp_ms;
138
+ if (exp === undefined || exp === null)
139
+ return true;
140
+ if (typeof exp !== 'number' || !Number.isInteger(exp))
141
+ return false;
142
+ return t < exp;
143
+ }
122
144
  /**
123
- * Select exactly one Ed25519 verification key from a JWKS by kid.
124
- *
125
- * 1. Filter to Ed25519 signing candidates.
126
- * 2. If a kid is requested: the candidate whose kid strictly equals it
127
- * must be unique. Zero matches -> not_found. Duplicate kids ->
128
- * ambiguous. kid comparison is exact, case-sensitive.
129
- * 3. If no kid is requested: exactly one candidate -> use it; more than
130
- * one -> ambiguous; zero -> not_found.
131
- * 4. Decode `x` -> 32 bytes -> hex. A candidate that survives filtering
132
- * but whose `x` is not 32 bytes is malformed.
133
- *
134
- * Never silently falls back to a different kid than requested.
145
+ * Select exactly one Ed25519 verification key from a JWKS. Filters, in
146
+ * order: Ed25519 signing candidacy, optional raw-hex `x` match, optional
147
+ * kid match, optional validity-window gate; then requires the survivor to
148
+ * be unique. Never silently falls back to a different key. Legacy callers
149
+ * may pass a bare kid string.
135
150
  */
136
- export function selectKey(jwks, requestedKid) {
137
- const candidates = jwks.keys.filter(isEd25519SigningCandidate);
151
+ export function selectKey(jwks, sel) {
152
+ const opts = typeof sel === 'string' ? { kid: sel } : (sel ?? {});
153
+ let candidates = jwks.keys.filter(isEd25519SigningCandidate);
138
154
  if (candidates.length === 0) {
139
155
  return { ok: false, status: 'not_found', reason: 'no Ed25519 signing key in JWKS' };
140
156
  }
141
- if (requestedKid !== undefined) {
142
- const matches = candidates.filter(c => c.kid === requestedKid);
157
+ // Cycles authority paths carry a window (issuedAtMs) and/or a raw-hex signer
158
+ // match (publicKeyHex). On those, `kid` MUST be unique across the whole set —
159
+ // a duplicate kid is an authority failure SET-WIDE, before any selection, so
160
+ // it can't be rescued by the window leaving one survivor or by selecting via
161
+ // the raw-hex path. (Generic non-cycles kid-only lookups keep RFC 7517's
162
+ // looser handling, enforced per-requested-kid below.)
163
+ if (opts.issuedAtMs !== undefined || opts.publicKeyHex !== undefined) {
164
+ const seen = new Set();
165
+ for (const c of candidates) {
166
+ if (typeof c.kid !== 'string')
167
+ continue;
168
+ if (seen.has(c.kid)) {
169
+ return { ok: false, status: 'ambiguous', reason: `duplicate kid='${c.kid}' in JWK Set` };
170
+ }
171
+ seen.add(c.kid);
172
+ }
173
+ }
174
+ // Raw-hex signer match (cycles): the published key must carry the signer's
175
+ // bytes — and the spec applies the SAME validity-window gate to raw-hex
176
+ // signers as to did:cycles, so a raw-hex selection REQUIRES an integer
177
+ // issued_at_ms (no window-less raw acceptance).
178
+ if (opts.publicKeyHex !== undefined) {
179
+ if (typeof opts.issuedAtMs !== 'number' || !Number.isInteger(opts.issuedAtMs)) {
180
+ return { ok: false, status: 'not_found', reason: 'raw-hex signer selection requires an integer issuedAtMs (window gate)' };
181
+ }
182
+ const want = opts.publicKeyHex.toLowerCase();
183
+ candidates = candidates.filter((c) => candidateToHex(c) === want);
184
+ if (candidates.length === 0) {
185
+ return { ok: false, status: 'not_found', reason: 'no JWK carries the raw signer key' };
186
+ }
187
+ }
188
+ // kid match (exact, case-sensitive). kid MUST be unique within the set: a
189
+ // duplicate kid is an authority failure BEFORE any window discretion (so two
190
+ // same-kid keys with disjoint windows can't sneak one survivor through).
191
+ if (opts.kid !== undefined) {
192
+ const matches = candidates.filter((c) => c.kid === opts.kid);
143
193
  if (matches.length === 0) {
144
- return { ok: false, status: 'not_found', reason: `no key with kid='${requestedKid}'` };
194
+ return { ok: false, status: 'not_found', reason: `no key with kid='${opts.kid}'` };
145
195
  }
146
196
  if (matches.length > 1) {
147
- return { ok: false, status: 'ambiguous', reason: `duplicate kid='${requestedKid}' in JWKS` };
197
+ return { ok: false, status: 'ambiguous', reason: `duplicate kid='${opts.kid}' in JWK Set` };
148
198
  }
149
- const jwk = matches[0];
150
- const hex = candidateToHex(jwk);
151
- if (!hex) {
152
- return { ok: false, status: 'malformed', reason: `kid='${requestedKid}' x is not a 32-byte key` };
199
+ candidates = matches;
200
+ }
201
+ // Validity-window gate (cycles): the key valid AT ISSUANCE, never the newest.
202
+ if (opts.issuedAtMs !== undefined) {
203
+ const t = opts.issuedAtMs;
204
+ // Fail-closed on a non-finite-integer issuance time: never coerce a string
205
+ // or fractional value into the [nbf, exp) comparison.
206
+ if (typeof t !== 'number' || !Number.isInteger(t)) {
207
+ return { ok: false, status: 'not_found', reason: 'issuedAtMs must be an integer epoch-ms value' };
208
+ }
209
+ candidates = candidates.filter((c) => windowCovers(c, t));
210
+ if (candidates.length === 0) {
211
+ return { ok: false, status: 'not_found', reason: 'no key whose validity window covers issued_at_ms' };
153
212
  }
154
- return { ok: true, jwk, publicKeyHex: hex, kid: jwk.kid };
155
213
  }
156
- // No kid requested: require an unambiguous single candidate.
157
214
  if (candidates.length > 1) {
158
- return { ok: false, status: 'ambiguous', reason: 'multiple signing keys and no kid requested' };
215
+ return { ok: false, status: 'ambiguous', reason: 'more than one key matches the selection criteria' };
159
216
  }
160
217
  const jwk = candidates[0];
161
218
  const hex = candidateToHex(jwk);
162
219
  if (!hex) {
163
- return { ok: false, status: 'malformed', reason: 'sole candidate x is not a 32-byte key' };
220
+ return { ok: false, status: 'malformed', reason: 'selected key x is not a 32-byte key' };
164
221
  }
165
222
  return { ok: true, jwk, publicKeyHex: hex, kid: jwk.kid };
166
223
  }
224
+ /**
225
+ * Validate that a parsed object is a well-formed JWKS with a non-empty
226
+ * `keys` array. Returns the JWKS on success or null on any structural
227
+ * problem.
228
+ */
229
+ export function asJWKS(body) {
230
+ if (!body || typeof body !== 'object')
231
+ return null;
232
+ const keys = body.keys;
233
+ if (!Array.isArray(keys) || keys.length === 0)
234
+ return null;
235
+ return { keys: keys };
236
+ }
167
237
  //# sourceMappingURL=did-cycles.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"did-cycles.js","sourceRoot":"","sources":["../../../../src/v2/key-resolution/did-cycles.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"did-cycles.js","sourceRoot":"","sources":["../../../../src/v2/key-resolution/did-cycles.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  export type { Ed25519JWK, JWKS, KeyResolutionStatus, KeyResolution, KeyLocator, FailurePolicy, CachePolicy, KeyResolverConfig, KeyResolver, } from './types.js';
2
2
  export { DEFAULT_TIMEOUT_MS, DEFAULT_CACHE_POLICY } from './types.js';
3
3
  export { decodeBase64Url, bytesToHex } from './base64url.js';
4
- export { parseDIDCycles, isDIDCycles, asJWKS, selectKey, type ParsedDIDCycles, type JWKSelection, } from './did-cycles.js';
4
+ export { parseDIDCycles, isDIDCycles, cyclesJwksUrl, asJWKS, selectKey, type ParsedDIDCycles, type SelectKeyOptions, type JWKSelection, } from './did-cycles.js';
5
5
  export { CyclesKeyResolver } from './resolver.js';
6
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/v2/key-resolution/index.ts"],"names":[],"mappings":"AAgCA,YAAY,EACV,UAAU,EACV,IAAI,EACJ,mBAAmB,EACnB,aAAa,EACb,UAAU,EACV,aAAa,EACb,WAAW,EACX,iBAAiB,EACjB,WAAW,GACZ,MAAM,YAAY,CAAA;AAEnB,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAErE,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAE5D,OAAO,EACL,cAAc,EACd,WAAW,EACX,MAAM,EACN,SAAS,EACT,KAAK,eAAe,EACpB,KAAK,YAAY,GAClB,MAAM,iBAAiB,CAAA;AAExB,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/v2/key-resolution/index.ts"],"names":[],"mappings":"AAgCA,YAAY,EACV,UAAU,EACV,IAAI,EACJ,mBAAmB,EACnB,aAAa,EACb,UAAU,EACV,aAAa,EACb,WAAW,EACX,iBAAiB,EACjB,WAAW,GACZ,MAAM,YAAY,CAAA;AAEnB,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAErE,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAE5D,OAAO,EACL,cAAc,EACd,WAAW,EACX,aAAa,EACb,MAAM,EACN,SAAS,EACT,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,YAAY,GAClB,MAAM,iBAAiB,CAAA;AAExB,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA"}
@@ -31,6 +31,6 @@
31
31
  // ══════════════════════════════════════════════════════════════════
32
32
  export { DEFAULT_TIMEOUT_MS, DEFAULT_CACHE_POLICY } from './types.js';
33
33
  export { decodeBase64Url, bytesToHex } from './base64url.js';
34
- export { parseDIDCycles, isDIDCycles, asJWKS, selectKey, } from './did-cycles.js';
34
+ export { parseDIDCycles, isDIDCycles, cyclesJwksUrl, asJWKS, selectKey, } from './did-cycles.js';
35
35
  export { CyclesKeyResolver } from './resolver.js';
36
36
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/v2/key-resolution/index.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,qEAAqE;AACrE,sCAAsC;AACtC,qEAAqE;AACrE,sEAAsE;AACtE,+DAA+D;AAC/D,sEAAsE;AACtE,+CAA+C;AAC/C,EAAE;AACF,YAAY;AACZ,YAAY;AACZ,kEAAkE;AAClE,uEAAuE;AACvE,gEAAgE;AAChE,yDAAyD;AACzD,EAAE;AACF,kBAAkB;AAClB,sEAAsE;AACtE,gCAAgC;AAChC,mEAAmE;AACnE,uEAAuE;AACvE,8DAA8D;AAC9D,+DAA+D;AAC/D,uEAAuE;AACvE,uEAAuE;AACvE,0DAA0D;AAC1D,EAAE;AACF,mEAAmE;AACnE,oEAAoE;AACpE,sDAAsD;AACtD,qEAAqE;AAcrE,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAErE,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAE5D,OAAO,EACL,cAAc,EACd,WAAW,EACX,MAAM,EACN,SAAS,GAGV,MAAM,iBAAiB,CAAA;AAExB,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/v2/key-resolution/index.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,qEAAqE;AACrE,sCAAsC;AACtC,qEAAqE;AACrE,sEAAsE;AACtE,+DAA+D;AAC/D,sEAAsE;AACtE,+CAA+C;AAC/C,EAAE;AACF,YAAY;AACZ,YAAY;AACZ,kEAAkE;AAClE,uEAAuE;AACvE,gEAAgE;AAChE,yDAAyD;AACzD,EAAE;AACF,kBAAkB;AAClB,sEAAsE;AACtE,gCAAgC;AAChC,mEAAmE;AACnE,uEAAuE;AACvE,8DAA8D;AAC9D,+DAA+D;AAC/D,uEAAuE;AACvE,uEAAuE;AACvE,0DAA0D;AAC1D,EAAE;AACF,mEAAmE;AACnE,oEAAoE;AACpE,sDAAsD;AACtD,qEAAqE;AAcrE,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAErE,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAE5D,OAAO,EACL,cAAc,EACd,WAAW,EACX,aAAa,EACb,MAAM,EACN,SAAS,GAIV,MAAM,iBAAiB,CAAA;AAExB,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"resolver.d.ts","sourceRoot":"","sources":["../../../../src/v2/key-resolution/resolver.ts"],"names":[],"mappings":"AAiCA,OAAO,EAKL,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACvB,MAAM,YAAY,CAAA;AAuCnB,qBAAa,iBAAkB,YAAW,WAAW;IACnD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAe;IAC7C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAQ;IAClC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAa;IACzC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAc;IACxC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAc;IAClC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgC;gBAE1C,MAAM,GAAE,iBAAsB;IAS1C,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO;IAWlC,OAAO,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,aAAa,CAAC;IAuB1D,OAAO,CAAC,aAAa;YAsBP,aAAa;IAwC3B,OAAO,CAAC,wBAAwB;YAsClB,WAAW;IA+EzB;;qEAEiE;IACjE,OAAO,CAAC,YAAY;IASpB,OAAO,CAAC,SAAS;IAajB,OAAO,CAAC,UAAU;IAQlB,OAAO,CAAC,aAAa;IAQrB,0DAA0D;IAC1D,SAAS,IAAI,MAAM;IAInB,mDAAmD;IACnD,UAAU,IAAI,IAAI;IAMlB,OAAO,CAAC,IAAI;IAIZ,OAAO,CAAC,SAAS;IAYjB;;;;;;;OAOG;IACH,OAAO,CAAC,WAAW;CAwBpB"}
1
+ {"version":3,"file":"resolver.d.ts","sourceRoot":"","sources":["../../../../src/v2/key-resolution/resolver.ts"],"names":[],"mappings":"AAmCA,OAAO,EAKL,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACvB,MAAM,YAAY,CAAA;AAuCnB,qBAAa,iBAAkB,YAAW,WAAW;IACnD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAe;IAC7C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAQ;IAClC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAa;IACzC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAc;IACxC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAc;IAClC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgC;gBAE1C,MAAM,GAAE,iBAAsB;IAS1C,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO;IAWlC,OAAO,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,aAAa,CAAC;IAuB1D,OAAO,CAAC,aAAa;YAsBP,aAAa;IAwC3B,OAAO,CAAC,wBAAwB;YAsClB,WAAW;IAyHzB;;qEAEiE;IACjE,OAAO,CAAC,YAAY;IASpB,OAAO,CAAC,SAAS;IAajB,OAAO,CAAC,UAAU;IAQlB,OAAO,CAAC,aAAa;IAQrB,0DAA0D;IAC1D,SAAS,IAAI,MAAM;IAInB,mDAAmD;IACnD,UAAU,IAAI,IAAI;IAMlB,OAAO,CAAC,IAAI;IAIZ,OAAO,CAAC,SAAS;IAYjB;;;;;;;OAOG;IACH,OAAO,CAAC,WAAW;CAwBpB"}
@@ -20,9 +20,10 @@
20
20
  // fetch timeout mirroring the did:web resolver. It does not aggregate
21
21
  // across tenants, expose an endpoint, or alert.
22
22
  // ══════════════════════════════════════════════════════════════════
23
+ import { createHash } from 'node:crypto';
23
24
  import { fromDIDKey, resolveDIDWeb } from '../../core/did-interop.js';
24
25
  import { multibaseToHex } from '../../core/did.js';
25
- import { asJWKS, isDIDCycles, parseDIDCycles, selectKey, } from './did-cycles.js';
26
+ import { asJWKS, cyclesJwksUrl, isDIDCycles, parseDIDCycles, selectKey, } from './did-cycles.js';
26
27
  import { decodeBase64Url, bytesToHex } from './base64url.js';
27
28
  import { DEFAULT_CACHE_POLICY, DEFAULT_TIMEOUT_MS, } from './types.js';
28
29
  /** Scope-of-claim for a successful key resolution. */
@@ -74,9 +75,11 @@ export class CyclesKeyResolver {
74
75
  const did = locator.did;
75
76
  if (!did)
76
77
  return false;
77
- return (did.startsWith('did:key:') ||
78
- did.startsWith('did:web:') ||
79
- did.startsWith('did:cycles:'));
78
+ // did:cycles is handled here, but is hash-bound: it resolves only with a
79
+ // serverId (the resolver checks sha256(serverId) against the DID subject and
80
+ // derives the JWKS URL). A did:cycles locator without serverId fails closed
81
+ // in resolve(); canResolve reports the method is handled.
82
+ return did.startsWith('did:key:') || did.startsWith('did:web:') || did.startsWith('did:cycles:');
80
83
  }
81
84
  async resolve(locator) {
82
85
  if (!locator || (!locator.did && !locator.jwksUrl)) {
@@ -190,23 +193,47 @@ export class CyclesKeyResolver {
190
193
  }
191
194
  // ── did:cycles / direct JWKS (this module's mapping) ─────────────
192
195
  async resolveJwks(locator) {
193
- let jwksUrl;
194
196
  let fragmentKid;
197
+ let jwksUrl;
195
198
  if (locator.did && isDIDCycles(locator.did)) {
199
+ // did:cycles is HASH-BOUND. The resolver enforces the binding itself and
200
+ // derives the JWKS URL — it never accepts an unbound did:cycles:
201
+ // 1. parse the subject hash + kid fragment;
202
+ // 2. require server_id, and check sha256(server_id) === subject hash
203
+ // (fail closed on mismatch) — anchors authority to that server_id;
204
+ // 3. derive the JWKS URL from server_id (cyclesJwksUrl), NOT from a
205
+ // caller-supplied URL (which could point at a different server);
206
+ // 4. the validity-window gate (issued_at_ms) is MANDATORY here.
207
+ let serverIdHash;
196
208
  try {
197
- const parsed = parseDIDCycles(locator.did);
198
- jwksUrl = parsed.jwksUrl;
199
- fragmentKid = parsed.kid;
209
+ ;
210
+ ({ serverIdHash, kid: fragmentKid } = parseDIDCycles(locator.did));
200
211
  }
201
212
  catch (err) {
202
213
  return this.fail('unsupported', `did:cycles parse failed: ${safeMsg(err)}`);
203
214
  }
215
+ // The canonical signer_did form is did:cycles:<hash>#<kid>; the fragment
216
+ // names the key in the set and is REQUIRED for resolution.
217
+ if (!fragmentKid) {
218
+ return this.fail('unsupported', 'did:cycles requires a #<kid> fragment');
219
+ }
220
+ if (!locator.serverId) {
221
+ return this.fail('unsupported', 'did:cycles requires serverId to establish the sha256 binding');
222
+ }
223
+ const computed = createHash('sha256').update(locator.serverId, 'utf8').digest('hex');
224
+ if (computed !== serverIdHash) {
225
+ return this.fail('not_found', 'did:cycles subject does not match sha256(server_id) — signer not bound to this server');
226
+ }
227
+ if (typeof locator.issuedAtMs !== 'number' || !Number.isInteger(locator.issuedAtMs)) {
228
+ return this.fail('not_found', 'did:cycles resolution requires an integer issuedAtMs (validity window)');
229
+ }
230
+ jwksUrl = cyclesJwksUrl(locator.serverId);
204
231
  }
205
- else if (locator.jwksUrl) {
232
+ else {
206
233
  jwksUrl = locator.jwksUrl;
207
234
  }
208
- else {
209
- return this.fail('unsupported', 'no did:cycles or jwksUrl present');
235
+ if (!jwksUrl) {
236
+ return this.fail('unsupported', 'no did:cycles (with serverId) or jwksUrl present');
210
237
  }
211
238
  // HTTPS only.
212
239
  if (!jwksUrl.startsWith('https://')) {
@@ -216,7 +243,15 @@ export class CyclesKeyResolver {
216
243
  if (wantKid === AMBIGUOUS) {
217
244
  return this.fail('ambiguous', 'did:cycles fragment and explicit kid disagree');
218
245
  }
219
- const cacheKey = `jwks:${jwksUrl}|kid:${wantKid ?? ''}`;
246
+ // Cache the SELECTED key. Selection now depends on the validity window
247
+ // (issuedAtMs) and the raw-hex signer match (publicKeyHex) too, so both
248
+ // MUST be in the cache key — otherwise a key selected for one receipt's
249
+ // issuance window could be wrongly reused for another across a rotation
250
+ // (the [nbf, exp) gate would be bypassed). (A JWKS-document cache keyed by
251
+ // URL, with selection re-run per call, would also dedup the fetch across
252
+ // distinct issuance times — a future optimization.)
253
+ const cacheKey = `jwks:${jwksUrl}|kid:${wantKid ?? ''}` +
254
+ `|iat:${locator.issuedAtMs ?? ''}|pk:${locator.publicKeyHex?.toLowerCase() ?? ''}`;
220
255
  const cached = this.readCache(cacheKey);
221
256
  if (cached)
222
257
  return cached;
@@ -249,7 +284,11 @@ export class CyclesKeyResolver {
249
284
  // Loaded but structurally wrong: fail-closed even under fail-open.
250
285
  return this.storeFail(cacheKey, 'malformed', 'JWKS missing non-empty keys[] array');
251
286
  }
252
- const selection = selectKey(jwks, wantKid);
287
+ const selection = selectKey(jwks, {
288
+ kid: wantKid,
289
+ issuedAtMs: locator.issuedAtMs,
290
+ publicKeyHex: locator.publicKeyHex,
291
+ });
253
292
  if (!selection.ok) {
254
293
  return this.storeFail(cacheKey, selection.status, selection.reason);
255
294
  }