@request-seal/core 0.1.0 → 0.3.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 (128) hide show
  1. package/CHANGELOG.md +49 -1
  2. package/README.md +342 -114
  3. package/dist/accept-signature.d.ts +11 -0
  4. package/dist/accept-signature.js +7 -0
  5. package/dist/adapters/fetch/index.d.ts +10 -0
  6. package/dist/adapters/fetch/index.js +21 -4
  7. package/dist/adapters/fetch/runtime.d.ts +1 -0
  8. package/dist/adapters/fetch/runtime.js +1 -0
  9. package/dist/adapters/node-http/index.d.ts +23 -0
  10. package/dist/adapters/node-http/index.js +28 -8
  11. package/dist/brand.d.ts +5 -0
  12. package/dist/brand.js +12 -0
  13. package/dist/bytes.d.ts +4 -0
  14. package/dist/bytes.js +5 -0
  15. package/dist/constants.d.ts +64 -4
  16. package/dist/constants.js +62 -2
  17. package/dist/context.d.ts +4 -0
  18. package/dist/context.js +3 -0
  19. package/dist/crypto/algorithm.d.ts +9 -0
  20. package/dist/crypto/algorithm.js +12 -0
  21. package/dist/crypto/index.d.ts +12 -2
  22. package/dist/crypto/index.js +34 -11
  23. package/dist/crypto/options.d.ts +3 -0
  24. package/dist/crypto/options.js +15 -0
  25. package/dist/crypto/primitive.d.ts +7 -3
  26. package/dist/crypto/primitive.js +36 -10
  27. package/dist/custody/content_key.d.ts +4 -0
  28. package/dist/custody/content_key.js +4 -2
  29. package/dist/custody/index.d.ts +2 -0
  30. package/dist/custody/index.js +5 -1
  31. package/dist/custody/local.d.ts +17 -1
  32. package/dist/custody/local.js +10 -5
  33. package/dist/custody/ssh-agent/index.d.ts +2 -0
  34. package/dist/custody/ssh-agent/index.js +11 -181
  35. package/dist/custody/ssh-agent/port.d.ts +19 -0
  36. package/dist/custody/ssh-agent/port.js +183 -0
  37. package/dist/digest.d.ts +22 -0
  38. package/dist/digest.js +15 -0
  39. package/dist/discovery/body.d.ts +19 -2
  40. package/dist/discovery/body.js +36 -11
  41. package/dist/discovery/cache.d.ts +21 -2
  42. package/dist/discovery/cache.js +105 -39
  43. package/dist/discovery/headers.d.ts +3 -0
  44. package/dist/discovery/headers.js +27 -1
  45. package/dist/discovery/index.d.ts +13 -1
  46. package/dist/discovery/index.js +31 -7
  47. package/dist/discovery/node-connection.d.ts +1 -1
  48. package/dist/discovery/node-connection.js +2 -2
  49. package/dist/discovery/node-trust.d.ts +2 -0
  50. package/dist/discovery/node-trust.js +9 -0
  51. package/dist/discovery/node.d.ts +9 -1
  52. package/dist/discovery/node.js +150 -26
  53. package/dist/discovery/proof.js +2 -2
  54. package/dist/discovery/snapshot.js +3 -0
  55. package/dist/discovery/source.d.ts +7 -0
  56. package/dist/discovery/source.js +5 -0
  57. package/dist/discovery/support.js +2 -3
  58. package/dist/errors/parity.d.ts +44 -3
  59. package/dist/errors/parity.js +45 -4
  60. package/dist/errors/types.d.ts +17 -3
  61. package/dist/errors/types.js +16 -2
  62. package/dist/index.d.ts +1 -0
  63. package/dist/index.js +1 -0
  64. package/dist/jose/header.d.ts +1 -0
  65. package/dist/jose/jwe.d.ts +9 -0
  66. package/dist/jose/jwe.js +17 -3
  67. package/dist/jose/jws.d.ts +11 -0
  68. package/dist/jose/jws.js +14 -2
  69. package/dist/jose/key-management.d.ts +11 -0
  70. package/dist/jose/key-management.js +13 -2
  71. package/dist/jose/nested.d.ts +6 -0
  72. package/dist/jose/nested.js +32 -2
  73. package/dist/json.d.ts +7 -0
  74. package/dist/key_handle.d.ts +12 -3
  75. package/dist/key_handle.js +32 -25
  76. package/dist/key_identity.d.ts +7 -0
  77. package/dist/key_identity.js +10 -2
  78. package/dist/message/index.d.ts +24 -0
  79. package/dist/message/index.js +22 -1
  80. package/dist/numbers.d.ts +6 -0
  81. package/dist/numbers.js +5 -0
  82. package/dist/policy.d.ts +27 -0
  83. package/dist/policy.js +42 -21
  84. package/dist/profile.d.ts +36 -0
  85. package/dist/profile.js +215 -0
  86. package/dist/public_key.d.ts +11 -2
  87. package/dist/public_key.js +31 -8
  88. package/dist/quorum/evaluation.d.ts +1 -1
  89. package/dist/quorum/evaluation.js +18 -4
  90. package/dist/quorum/verification.d.ts +2 -0
  91. package/dist/quorum/verification.js +1 -0
  92. package/dist/quorum.d.ts +9 -0
  93. package/dist/quorum.js +7 -1
  94. package/dist/replay/claim.d.ts +5 -0
  95. package/dist/replay/claim.js +9 -1
  96. package/dist/replay/index.d.ts +6 -0
  97. package/dist/replay/index.js +13 -1
  98. package/dist/replay/postgres.d.ts +3 -0
  99. package/dist/replay/postgres.js +7 -0
  100. package/dist/replay/receipt.d.ts +1 -0
  101. package/dist/replay/receipt.js +1 -0
  102. package/dist/replay/store.d.ts +1 -0
  103. package/dist/replay/store.js +1 -0
  104. package/dist/replay-port.d.ts +6 -1
  105. package/dist/result.d.ts +3 -0
  106. package/dist/result.js +2 -0
  107. package/dist/sign.d.ts +26 -0
  108. package/dist/sign.js +25 -3
  109. package/dist/signature-base/index.d.ts +2 -0
  110. package/dist/signature-base/index.js +5 -1
  111. package/dist/signature-fields.d.ts +5 -1
  112. package/dist/signature-fields.js +9 -1
  113. package/dist/structured-fields/index.d.ts +21 -0
  114. package/dist/structured-fields/index.js +14 -1
  115. package/dist/verification.d.ts +5 -1
  116. package/dist/verification.js +9 -6
  117. package/dist/verify-quorum.d.ts +1 -0
  118. package/dist/verify-quorum.js +30 -8
  119. package/dist/verify.d.ts +23 -0
  120. package/dist/verify.js +46 -14
  121. package/dist/web-bot-auth/index.d.ts +3 -0
  122. package/dist/web-bot-auth/index.js +3 -0
  123. package/dist/web-bot-auth/policy.d.ts +8 -0
  124. package/dist/web-bot-auth/policy.js +8 -4
  125. package/dist/web-bot-auth/verification.d.ts +8 -0
  126. package/dist/web-bot-auth/verification.js +4 -2
  127. package/dist/web-bot-auth/verify.js +2 -2
  128. package/package.json +29 -6
@@ -1,16 +1,18 @@
1
1
  import { remainingMs } from "../context.js";
2
2
  import { decodeJson } from "../json.js";
3
3
  import { PublicKey } from "../public_key.js";
4
- import { resolveAlgorithm, algorithms, selection, } from "../crypto/algorithm.js";
4
+ import { resolveAlgorithm, assertedJose, joseId, selection, } from "../crypto/algorithm.js";
5
5
  import { digest } from "../crypto/subtle.js";
6
6
  import { encodeBase64Url } from "../bytes.js";
7
7
  import { ok } from "../result.js";
8
8
  import { isDiscoverySource, keyIdValid, sourceOrigin, sourceUrl, } from "./source.js";
9
9
  import { ensure, unwrap, failure, clockSeconds, inspectable, systemClock, list, } from "./support.js";
10
10
  const resolutions = new WeakSet(), sets = new WeakSet();
11
+ /** Check the private brand of a discovery resolution. */
11
12
  export function isResolution(v) {
12
13
  return !!v && typeof v === "object" && resolutions.has(v);
13
14
  }
15
+ /** Check the private brand of a discovery key set. */
14
16
  export function isKeySet(v) {
15
17
  return !!v && typeof v === "object" && sets.has(v);
16
18
  }
@@ -45,6 +47,7 @@ export function keySet(data) {
45
47
  proof: data.proof,
46
48
  });
47
49
  }
50
+ /** Resolve an eligible key by identifier, allowed algorithms, and current time. */
48
51
  export async function lookup(set, kid, allowed, now = systemClock(), transient = false) {
49
52
  try {
50
53
  ensure(isKeySet(set), "invalid_options");
@@ -56,7 +59,8 @@ export async function lookup(set, kid, allowed, now = systemClock(), transient =
56
59
  isResolution(entry) &&
57
60
  now < entry.expiresAt &&
58
61
  !set.source.revoked.includes(entry.thumbprint), "unknown_key");
59
- const algorithm = choices.find((a) => (entry.assertedAlgorithm === null || entry.assertedAlgorithm === a) &&
62
+ const algorithm = choices.find((a) => (entry.assertedAlgorithm === null ||
63
+ entry.assertedAlgorithm === joseId(a)) &&
60
64
  entry.key.bind(a).ok);
61
65
  ensure(algorithm, "unknown_key");
62
66
  return ok(resolution({ ...entry, algorithm, revision: set.revision }));
@@ -122,14 +126,34 @@ export async function bodyKeys(doc, source, now, location = source.location, con
122
126
  !Array.isArray(input) &&
123
127
  Object.keys(input).length <= 32, "invalid_key_set");
124
128
  const j = input, asserted = j.alg;
129
+ // Leaked private members are a publisher blunder the whole set rejects
130
+ // loudly, before any skip decision: skipping must never hide them.
131
+ ensure(!["d", "p", "q", "dp", "dq", "qi", "oth", "k"].some((k) => Object.hasOwn(j, k)), "invalid_key_set");
132
+ // A key whose alg names an algorithm this verifier does not use — an
133
+ // encryption key, or a signing name outside both registries — cannot
134
+ // satisfy any signature policy: skip it and keep the rest of the set.
135
+ if (typeof asserted === "string" && assertedJose(asserted) === undefined)
136
+ continue;
125
137
  ensure(asserted === undefined ||
126
138
  asserted === null ||
127
- (typeof asserted === "string" &&
128
- unwrap(algorithms(), "invalid_key_set").includes(asserted)), "invalid_key_set");
139
+ typeof asserted === "string", "invalid_key_set");
140
+ // RFC 7517 sources assert standard JOSE names; either spelling normalizes
141
+ // once, here, to the JOSE name, which exists for every registry entry.
142
+ const normalized = typeof asserted === "string" ? assertedJose(asserted) : undefined;
129
143
  const imported = Object.assign(Object.create(null), j);
130
- if (typeof asserted === "string")
131
- imported.alg = unwrap(resolveAlgorithm(asserted), "invalid_key_set").jose;
132
- const key = unwrap(await PublicKey.import(imported), "invalid_key_set");
144
+ if (normalized !== undefined)
145
+ imported.alg = normalized;
146
+ const attempt = await PublicKey.import(imported);
147
+ if (!attempt.ok) {
148
+ // A well-formed key this verifier cannot import for signature work (an
149
+ // unknown kty or curve, RFC 7517 Section 5) is skipped like an
150
+ // out-of-registry alg; other import failures stay set rejections.
151
+ if (attempt.error.kind !== "crypto" || attempt.error.reason !== "invalid_key")
152
+ ensure(false, "invalid_key_set");
153
+ ensure(!context || remainingMs(context) > 0, "deadline_exceeded");
154
+ continue;
155
+ }
156
+ const key = attempt.value;
133
157
  ensure(!context || remainingMs(context) > 0, "deadline_exceeded");
134
158
  const thumbprint = unwrap(await key.thumbprint(), "invalid_key_set");
135
159
  ensure(!context || remainingMs(context) > 0, "deadline_exceeded");
@@ -140,14 +164,15 @@ export async function bodyKeys(doc, source, now, location = source.location, con
140
164
  j.kid === thumbprint, "invalid_key_set");
141
165
  const nbf = Object.hasOwn(j, "nbf") ? keyTime(j.nbf) : null, exp = Object.hasOwn(j, "exp") ? keyTime(j.exp) : null;
142
166
  const publicJwk = unwrap(key.jwk(), "invalid_key_set");
143
- const algorithm = (asserted ??
144
- (publicJwk.kty === "OKP"
167
+ const algorithm = normalized === undefined
168
+ ? (publicJwk.kty === "OKP"
145
169
  ? "ed25519"
146
170
  : publicJwk.kty === "EC"
147
171
  ? publicJwk.crv === "P-256"
148
172
  ? "ecdsa-p256-sha256"
149
173
  : "ecdsa-p384-sha384"
150
- : "rsa-pss-sha512"));
174
+ : "rsa-pss-sha512")
175
+ : { jws: normalized };
151
176
  if (key.bind(algorithm).ok &&
152
177
  (nbf === null || now >= nbf) &&
153
178
  (exp === null || now < exp) &&
@@ -158,7 +183,7 @@ export async function bodyKeys(doc, source, now, location = source.location, con
158
183
  keyId: kid,
159
184
  thumbprint,
160
185
  algorithm,
161
- assertedAlgorithm: (asserted ?? null),
186
+ assertedAlgorithm: normalized ?? null,
162
187
  origin: sourceOrigin(sourceUrl(location)),
163
188
  sourceType: source.type,
164
189
  location,
@@ -1,17 +1,36 @@
1
1
  import { type Result } from "../result.ts";
2
2
  import { type Context } from "../context.ts";
3
3
  import { type DiscoverySource } from "./source.ts";
4
- import { type KeySet, type Resolution } from "./body.ts";
4
+ import { type KeySet, type NotModified, type Resolution } from "./body.ts";
5
+ /** A caller-owned discovery transport callback. With `validators`, an unchanged source answers 304 and the callback returns a `NotModified` outcome instead of a key set (RFC 9111 Section 4.3.4). */
5
6
  export type DiscoveryFetch = (source: DiscoverySource, options: {
6
7
  clock: () => number;
7
8
  context: Context;
8
- }) => Promise<Result<KeySet>>;
9
+ validators?: Readonly<{
10
+ etag: string;
11
+ lifetime: number;
12
+ }>;
13
+ }) => Promise<Result<KeySet | NotModified>>;
14
+ /** A caller-owned bounded cache of discovery key sets and failures. */
9
15
  export type DiscoveryCache = Readonly<{
16
+ /** Refresh a source from an explicit snapshot or configured fetch callback. */
10
17
  refresh: (source: unknown, snapshot?: unknown) => Promise<Result<KeySet>>;
18
+ /** Resolve an eligible key through the bounded discovery cache. */
11
19
  resolve: (source: unknown, keyId: unknown, options?: unknown) => Promise<Result<Resolution>>;
20
+ /** Remove a key by thumbprint from a cached source. */
12
21
  remove: (source: unknown, thumbprint: unknown) => Result<true>;
22
+ /** Invalidate a source's cached key set. */
13
23
  invalidate: (source: unknown) => Result<true>;
24
+ /** Close this capability and release its retained state. */
14
25
  close: () => Result<true>;
15
26
  }>;
27
+ /**
28
+ * Internal to the resolver bridge, not exported from the package: resolve with the
29
+ * cache's own clock for fetching, storing, negative caching, and entry freshness, and
30
+ * decide the key's freshness on `decision`.
31
+ */
32
+ export declare function resolveOnClock(cache: DiscoveryCache, s: unknown, kid: unknown, input: unknown, decision: () => number): Promise<Result<Resolution>>;
33
+ /** Check the private brand of a discovery cache. */
16
34
  export declare function isDiscoveryCache(v: unknown): v is DiscoveryCache;
35
+ /** Validate cache options and create a branded discovery cache. */
17
36
  export declare function createCache(input?: unknown): Result<DiscoveryCache>;
@@ -6,9 +6,20 @@ import { algorithmList, isKeySet, keySet, lookup, } from "./body.js";
6
6
  import { algorithms } from "../crypto/algorithm.js";
7
7
  import { ensure, unwrap, failure, options, clockValue, systemClock, inspectable, } from "./support.js";
8
8
  const caches = new WeakSet();
9
+ const decided = new WeakMap();
10
+ /**
11
+ * Internal to the resolver bridge, not exported from the package: resolve with the
12
+ * cache's own clock for fetching, storing, negative caching, and entry freshness, and
13
+ * decide the key's freshness on `decision`.
14
+ */
15
+ export function resolveOnClock(cache, s, kid, input, decision) {
16
+ return decided.get(cache)(s, kid, input, decision);
17
+ }
18
+ /** Check the private brand of a discovery cache. */
9
19
  export function isDiscoveryCache(v) {
10
20
  return !!v && typeof v === "object" && caches.has(v);
11
21
  }
22
+ /** Validate cache options and create a branded discovery cache. */
12
23
  export function createCache(input = {}) {
13
24
  try {
14
25
  const a = options(input, ["clock", "maxSources", "maxInFlight", "fetch"]);
@@ -47,6 +58,31 @@ export function createCache(input = {}) {
47
58
  }
48
59
  function store(e, result, clock, snapshot = false) {
49
60
  const now = clockValue(clock);
61
+ if (result.ok && "notModified" in result.value && result.value.notModified === true) {
62
+ // RFC 9111 Section 4.3.4: the stored representation stays; the 304's
63
+ // own freshness directives and validator replace the stored ones. The
64
+ // set never claims freshness beyond its earliest still-live key.
65
+ const current = e.set;
66
+ if (!current ||
67
+ sourceIdentity(current.source) !== sourceIdentity(e.source))
68
+ return err(discoveryError("source_unavailable"));
69
+ const fresh = result.value;
70
+ const set = filter(current, e);
71
+ const earliest = Object.values(set.keys).reduce((bound, key) => Math.min(bound, key.expiresAt), Infinity);
72
+ const merged = keySet({
73
+ ...set,
74
+ etag: fresh.etag,
75
+ fetchedAt: fresh.fetchedAt,
76
+ expiresAt: Math.min(fresh.expiresAt, earliest),
77
+ });
78
+ e.fetchedAtHighWater = Math.max(e.fetchedAtHighWater ?? merged.fetchedAt, merged.fetchedAt);
79
+ delete e.failureUntil;
80
+ if (merged.expiresAt > now)
81
+ e.set = merged;
82
+ else
83
+ delete e.set;
84
+ return ok(merged);
85
+ }
50
86
  if (result.ok) {
51
87
  ensure(isKeySet(result.value) &&
52
88
  sourceIdentity(result.value.source) === sourceIdentity(e.source), "source_unavailable");
@@ -82,13 +118,36 @@ export function createCache(input = {}) {
82
118
  signal: controller.signal,
83
119
  }), "invalid_options");
84
120
  const generation = e.generation;
121
+ // A stored validator makes the revalidation conditional: an unchanged
122
+ // source answers 304 and its key set stays put. A CIMD document is not
123
+ // the key set itself — its 304 says nothing about the nested JWKS it
124
+ // names — so CIMD sources always revalidate unconditionally.
125
+ let validators;
126
+ const stored = e.set;
127
+ if (stored && stored.etag !== null && e.source.type !== "cimd") {
128
+ const now = clockValue(clock);
129
+ // A 304 keeps the stored keys, so it can only renew a set whose
130
+ // every key is still inside its own expiry; any dead key forces a
131
+ // full fetch.
132
+ const alive = Object.keys(stored.keys).length > 0 &&
133
+ Object.values(stored.keys).every((key) => now < key.expiresAt);
134
+ if (alive)
135
+ validators = Object.freeze({
136
+ etag: stored.etag,
137
+ lifetime: Math.max(stored.expiresAt - stored.fetchedAt, 0),
138
+ });
139
+ }
85
140
  flight = {
86
141
  controller,
87
142
  waiters: 0,
88
143
  work: Promise.resolve().then(async () => {
89
144
  let result;
90
145
  try {
91
- const r = await run(c, () => fetcher(e.source, { clock, context: c }));
146
+ const r = await run(c, () => fetcher(e.source, {
147
+ clock,
148
+ context: c,
149
+ ...(validators ? { validators } : {}),
150
+ }));
92
151
  result =
93
152
  !r.ok && r.error.kind !== "discovery"
94
153
  ? err(discoveryError(r.error.reason === "deadline_exceeded"
@@ -134,6 +193,48 @@ export function createCache(input = {}) {
134
193
  }
135
194
  }
136
195
  }
196
+ async function resolveWith(s, kid, input, decision) {
197
+ try {
198
+ ensure(isDiscoverySource(s), "invalid_source");
199
+ const a = options(input, ["algorithms", "clock", "context", "timeoutMs"]);
200
+ const choices = algorithmList(Object.hasOwn(a, "algorithms")
201
+ ? a.algorithms
202
+ : unwrap(algorithms(), "invalid_options"));
203
+ ensure(keyIdValid(s, kid), "unknown_key");
204
+ ensure(s.keyId === "directory" || !s.revoked.includes(kid), "revoked_key");
205
+ // With a decision clock (the resolver bridge), the cache's own clock fetches, stores,
206
+ // and judges entry freshness; the decision clock judges only the key returned.
207
+ const selectedClock = decision === undefined && Object.hasOwn(a, "clock") ? a.clock : clockFn;
208
+ const now = clockValue(selectedClock);
209
+ const decisionNow = decision === undefined ? now : clockValue(decision);
210
+ ensure(!Object.hasOwn(a, "context") || isContext(a.context), "invalid_options");
211
+ let context = a.context;
212
+ if (Object.hasOwn(a, "timeoutMs")) {
213
+ ensure(Number.isSafeInteger(a.timeoutMs) &&
214
+ a.timeoutMs >= 1 &&
215
+ a.timeoutMs <= 300000, "invalid_options");
216
+ context = unwrap(withDeadline({
217
+ timeoutMs: Math.min(a.timeoutMs, s.timeoutMs),
218
+ ...(context ? { parent: context } : {}),
219
+ }), "invalid_options");
220
+ }
221
+ ensure(!context || remainingMs(context) > 0, "deadline_exceeded");
222
+ const e = enroll(s);
223
+ ensure(e.source.keyId === "directory" ||
224
+ (!e.removed.has(kid) && !e.source.revoked.includes(kid)), "revoked_key");
225
+ if (e.set && now < e.set.expiresAt)
226
+ return await lookup(e.set, kid, choices, decisionNow);
227
+ ensure(e.failureUntil === undefined || now >= e.failureUntil, "source_unavailable");
228
+ const fetched = await load(e, selectedClock, context);
229
+ if (!fetched.ok)
230
+ return fetched;
231
+ ensure(e.source.keyId === "directory" || !e.removed.has(kid), "revoked_key");
232
+ return await lookup(filter(fetched.value, e), kid, choices, decision === undefined ? clockValue(selectedClock) : clockValue(decision), fetched.value.expiresAt === fetched.value.fetchedAt);
233
+ }
234
+ catch (e) {
235
+ return failure(e, "source_unavailable");
236
+ }
237
+ }
137
238
  const out = {
138
239
  async refresh(s, snapshot) {
139
240
  try {
@@ -149,44 +250,8 @@ export function createCache(input = {}) {
149
250
  return failure(e, "source_unavailable");
150
251
  }
151
252
  },
152
- async resolve(s, kid, input = {}) {
153
- try {
154
- ensure(isDiscoverySource(s), "invalid_source");
155
- const a = options(input, ["algorithms", "clock", "context", "timeoutMs"]);
156
- const choices = algorithmList(Object.hasOwn(a, "algorithms")
157
- ? a.algorithms
158
- : unwrap(algorithms(), "invalid_options"));
159
- ensure(keyIdValid(s, kid), "unknown_key");
160
- ensure(s.keyId === "directory" || !s.revoked.includes(kid), "revoked_key");
161
- const selectedClock = Object.hasOwn(a, "clock") ? a.clock : clockFn;
162
- const now = clockValue(selectedClock);
163
- ensure(!Object.hasOwn(a, "context") || isContext(a.context), "invalid_options");
164
- let context = a.context;
165
- if (Object.hasOwn(a, "timeoutMs")) {
166
- ensure(Number.isSafeInteger(a.timeoutMs) &&
167
- a.timeoutMs >= 1 &&
168
- a.timeoutMs <= 300000, "invalid_options");
169
- context = unwrap(withDeadline({
170
- timeoutMs: Math.min(a.timeoutMs, s.timeoutMs),
171
- ...(context ? { parent: context } : {}),
172
- }), "invalid_options");
173
- }
174
- ensure(!context || remainingMs(context) > 0, "deadline_exceeded");
175
- const e = enroll(s);
176
- ensure(e.source.keyId === "directory" ||
177
- (!e.removed.has(kid) && !e.source.revoked.includes(kid)), "revoked_key");
178
- if (e.set && now < e.set.expiresAt)
179
- return await lookup(e.set, kid, choices, now);
180
- ensure(e.failureUntil === undefined || now >= e.failureUntil, "source_unavailable");
181
- const fetched = await load(e, selectedClock, context);
182
- if (!fetched.ok)
183
- return fetched;
184
- ensure(e.source.keyId === "directory" || !e.removed.has(kid), "revoked_key");
185
- return await lookup(filter(fetched.value, e), kid, choices, clockValue(selectedClock), fetched.value.expiresAt === fetched.value.fetchedAt);
186
- }
187
- catch (e) {
188
- return failure(e, "source_unavailable");
189
- }
253
+ resolve(s, kid, input = {}) {
254
+ return resolveWith(s, kid, input);
190
255
  },
191
256
  remove(s, kid) {
192
257
  try {
@@ -227,6 +292,7 @@ export function createCache(input = {}) {
227
292
  },
228
293
  };
229
294
  caches.add(out);
295
+ decided.set(out, (src, kid, input, decision) => resolveWith(src, kid, input, decision));
230
296
  return ok(inspectable(out, { kind: "DiscoveryCache" }));
231
297
  }
232
298
  catch (e) {
@@ -1,9 +1,12 @@
1
1
  import { type FieldOccurrence as Field } from "../message/index.ts";
2
2
  import type { DiscoverySource } from "./source.ts";
3
+ /** Ordered discovery response header names and string or byte values. */
3
4
  export type Headers = readonly (readonly [string, string | Uint8Array])[];
4
5
  export declare function fields(headers: unknown): readonly Field[];
5
6
  export declare function values(fs: readonly Field[], name: string): string[];
6
7
  export declare function header(fs: readonly Field[], name: string): string | null;
7
8
  export declare function httpDate(input: string | null, now: number): number | null;
8
9
  export declare function expiry(fs: readonly Field[], source: DiscoverySource, now: number): number;
10
+ /** RFC 9111 Section 4.3.4: whether a response carries freshness directives of its own. */
11
+ export declare function hasFreshness(fs: readonly Field[]): boolean;
9
12
  export declare function mediaType(fs: readonly Field[], type: DiscoverySource["type"]): void;
@@ -85,6 +85,25 @@ export function expiry(fs, source, now) {
85
85
  .toLowerCase()
86
86
  .split(",")
87
87
  .map((v) => v.trim());
88
+ return expiryFromControls(fs, source, now, controls);
89
+ }
90
+ /** RFC 9111 Section 4.3.4: whether a response carries freshness directives of its own. */
91
+ export function hasFreshness(fs) {
92
+ if (header(fs, "expires") !== null)
93
+ return true;
94
+ const controls = values(fs, "cache-control")
95
+ .join(",")
96
+ .toLowerCase()
97
+ .split(",")
98
+ .map((v) => v.trim());
99
+ // Only freshness-changing names count: max-age, no-cache, no-store. A
100
+ // directive like `public` leaves the stored lifetime in force.
101
+ return controls.some((v) => {
102
+ const name = v.split("=", 1)[0].trim();
103
+ return name === "no-store" || name === "no-cache" || name === "max-age";
104
+ });
105
+ }
106
+ function expiryFromControls(fs, source, now, controls) {
88
107
  const ages = controls
89
108
  .filter((v) => v.startsWith("max-age="))
90
109
  .map((v) => v.slice(8).replace(/^"+|"+$/g, ""));
@@ -102,8 +121,15 @@ export function expiry(fs, source, now) {
102
121
  }
103
122
  const age = header(fs, "age") ?? "0";
104
123
  ensure(/^[0-9]{1,10}$/.test(age), "invalid_response");
124
+ // RFC 9111 Sections 5.2.2.3 and 5.2.2.4: no-store and no-cache also arrive
125
+ // qualified, as in no-cache="content-type" (and the RFC 7234 quoted form whose
126
+ // member can itself contain commas). Match the directive name before any "=",
127
+ // so every spelling still forces revalidation.
105
128
  return (now +
106
- (controls.some((v) => v === "no-store" || v === "no-cache")
129
+ (controls.some((v) => {
130
+ const name = v.split("=", 1)[0].trim();
131
+ return name === "no-store" || name === "no-cache";
132
+ })
107
133
  ? 0
108
134
  : Math.max(Math.min(ttl, source.maxTtl) - Math.max(Number(age), now - date, 0), 0)));
109
135
  }
@@ -1,17 +1,29 @@
1
1
  import { type Result } from "../result.ts";
2
2
  import { fromSnapshot } from "./snapshot.ts";
3
3
  import { type Resolution } from "./body.ts";
4
+ /** A discovery-backed signature key resolver. @inline */
4
5
  export type KeyResolver = (request: unknown) => Promise<Result<Readonly<{
5
6
  algorithm: Resolution["algorithm"];
6
7
  key: Resolution["key"];
7
8
  }>>>;
9
+ /** Create a resolver over a branded key set or caller-owned discovery cache. */
8
10
  declare function resolver(input: unknown, opts: unknown): Result<KeyResolver>;
11
+ /** Snapshot resolution and explicit discovery operations. */
9
12
  export declare const Discovery: Readonly<{
13
+ /**
14
+ * Create a resolver over a branded key set or caller-owned discovery cache.
15
+ * Options are `algorithms` and an optional `clock` (integer Unix seconds); pass the
16
+ * policy's freshness clock so key freshness and signature freshness agree. Without
17
+ * `clock`, a key set uses system time and a cache uses its own clock. With a cache,
18
+ * `clock` decides only the returned key's freshness; the cache keeps its own clock for
19
+ * fetching and storing.
20
+ */
10
21
  resolver: typeof resolver & {
22
+ /** Parse a caller-supplied discovery snapshot without fetching it. */
11
23
  fromSnapshot: typeof fromSnapshot;
12
24
  };
13
25
  }>;
14
26
  export { DiscoverySource, isDiscoverySource, directoryId } from "./source.ts";
15
- export { isKeySet, isResolution, type KeySet, type Resolution, } from "./body.ts";
27
+ export { isKeySet, isResolution, type KeySet, type NotModified, type Resolution, } from "./body.ts";
16
28
  export { createCache, isDiscoveryCache, type DiscoveryCache, type DiscoveryFetch, } from "./cache.ts";
17
29
  export type { Headers } from "./snapshot.ts";
@@ -2,13 +2,17 @@ import { ok } from "../result.js";
2
2
  import { snapshotOptions } from "../brand.js";
3
3
  import { isKeySet, algorithmList } from "./body.js";
4
4
  import { isDiscoverySource } from "./source.js";
5
- import { isDiscoveryCache } from "./cache.js";
5
+ import { isDiscoveryCache, resolveOnClock } from "./cache.js";
6
6
  import { fromSnapshot } from "./snapshot.js";
7
- import { ensure, unwrap, failure } from "./support.js";
7
+ import { ensure, unwrap, failure, clockValue } from "./support.js";
8
+ /** Create a resolver over a branded key set or caller-owned discovery cache. */
8
9
  function resolver(input, opts) {
9
10
  try {
10
- const a = unwrap(snapshotOptions(opts, ["algorithms"]), "invalid_options");
11
+ const a = unwrap(snapshotOptions(opts, ["algorithms", "clock"]), "invalid_options");
11
12
  const allowed = algorithmList(a.algorithms);
13
+ // An explicit clock, usually the policy's freshness clock; system time otherwise.
14
+ const clock = Object.hasOwn(a, "clock") ? a.clock : undefined;
15
+ ensure(clock === undefined || typeof clock === "function", "invalid_options");
12
16
  let set, cache, source;
13
17
  if (isKeySet(input))
14
18
  set = input;
@@ -24,9 +28,15 @@ function resolver(input, opts) {
24
28
  typeof request === "object" &&
25
29
  Object.hasOwn(request, "keyid"), "unknown_key");
26
30
  const kid = request.keyid;
31
+ // A clock decides key freshness only: a throwing or invalid clock is
32
+ // invalid_options on both paths, and a cache still fetches and stores on its own clock.
27
33
  const result = set
28
- ? await set.lookup(kid, allowed)
29
- : await cache.resolve(source, kid, { algorithms: allowed });
34
+ ? clock === undefined
35
+ ? await set.lookup(kid, allowed)
36
+ : await set.lookup(kid, allowed, clockValue(clock))
37
+ : clock === undefined
38
+ ? await cache.resolve(source, kid, { algorithms: allowed })
39
+ : await resolveOnClock(cache, source, kid, { algorithms: allowed }, clock);
30
40
  return result.ok
31
41
  ? ok(Object.freeze({
32
42
  algorithm: result.value.algorithm,
@@ -44,8 +54,22 @@ function resolver(input, opts) {
44
54
  return failure(e, "invalid_options");
45
55
  }
46
56
  }
47
- const Resolver = Object.freeze(Object.assign(resolver, { fromSnapshot }));
48
- export const Discovery = Object.freeze({ resolver: Resolver });
57
+ const Resolver = Object.freeze(Object.assign(resolver, {
58
+ /** Parse a caller-supplied discovery snapshot without fetching it. */
59
+ fromSnapshot
60
+ }));
61
+ /** Snapshot resolution and explicit discovery operations. */
62
+ export const Discovery = Object.freeze({
63
+ /**
64
+ * Create a resolver over a branded key set or caller-owned discovery cache.
65
+ * Options are `algorithms` and an optional `clock` (integer Unix seconds); pass the
66
+ * policy's freshness clock so key freshness and signature freshness agree. Without
67
+ * `clock`, a key set uses system time and a cache uses its own clock. With a cache,
68
+ * `clock` decides only the returned key's freshness; the cache keeps its own clock for
69
+ * fetching and storing.
70
+ */
71
+ resolver: Resolver
72
+ });
49
73
  export { DiscoverySource, isDiscoverySource, directoryId } from "./source.js";
50
74
  export { isKeySet, isResolution, } from "./body.js";
51
75
  export { createCache, isDiscoveryCache, } from "./cache.js";
@@ -3,5 +3,5 @@ export declare function checkPeer(observed: {
3
3
  address: string | undefined;
4
4
  port: number | undefined;
5
5
  }, address: string, port: number, permittedAddresses: readonly string[]): void;
6
- /** A connected peer denial terminates the address loop. */
6
+ /** Peer and trust-store denials terminate the address loop unchanged. */
7
7
  export declare function allowConnectionRetry(error: unknown): void;
@@ -5,8 +5,8 @@ export function checkPeer(observed, address, port, permittedAddresses) {
5
5
  ensure(sameAddress(observed.address, address) && observed.port === port &&
6
6
  unwrap(addressAllowed(observed.address, permittedAddresses), "address_denied"), "address_denied");
7
7
  }
8
- /** A connected peer denial terminates the address loop. */
8
+ /** Peer and trust-store denials terminate the address loop unchanged. */
9
9
  export function allowConnectionRetry(error) {
10
- if (error && typeof error === "object" && "reason" in error && error.reason === "address_denied")
10
+ if (error && typeof error === "object" && "reason" in error && (error.reason === "address_denied" || error.reason === "transport_unavailable"))
11
11
  throw error;
12
12
  }
@@ -0,0 +1,2 @@
1
+ /** Classify OS trust permission failures without changing other errors. */
2
+ export declare function systemCertificateError(error: unknown): never;
@@ -0,0 +1,9 @@
1
+ import { discoveryError } from '../errors/types.js';
2
+ /** Classify OS trust permission failures without changing other errors. */
3
+ export function systemCertificateError(error) {
4
+ const deno = globalThis.Deno;
5
+ const PermissionError = deno?.errors?.NotCapable;
6
+ if (PermissionError && error instanceof PermissionError)
7
+ throw discoveryError('transport_unavailable');
8
+ throw error;
9
+ }
@@ -1,15 +1,23 @@
1
1
  import { type Result } from "../result.ts";
2
- import { type KeySet } from "./body.ts";
2
+ import { type KeySet, type NotModified } from "./body.ts";
3
3
  export type DiscoverySource = import("./source.ts").DiscoverySource;
4
+ /** Fetch a discovery key set with bounded HTTPS, address, and redirect policies, unconditionally. A `validators` option is accepted and ignored, so this function wires directly as a cache's fetch callback; use `fetchWithValidators` for conditional revalidation. */
4
5
  export declare function fetch(source: unknown, input?: unknown): Promise<Result<KeySet>>;
6
+ /** Conditionally fetch a discovery key set (RFC 9111 Section 4.3.4): send `If-None-Match` with the stored validator, and an unchanged source answers 304 as a `NotModified` outcome instead of a key set. `storedLifetime` seconds persist when the 304 carries no freshness directives of its own. */
7
+ export declare function fetchWithValidators(source: unknown, etag: unknown, storedLifetime: unknown, input?: unknown): Promise<Result<KeySet | NotModified>>;
8
+ /** Discovery resolution with the explicit Node HTTPS transport. */
5
9
  export declare const Discovery: Readonly<{
6
10
  resolver: ((input: unknown, opts: unknown) => Result<import("./index.ts").KeyResolver>) & {
7
11
  fromSnapshot: typeof import("./snapshot.ts").fromSnapshot;
8
12
  };
13
+ /** Fetch a discovery key set through the bounded Node HTTPS transport. */
9
14
  fetch: typeof fetch;
15
+ /** Conditionally fetch a discovery key set (RFC 9111 Section 4.3.4). */
16
+ fetchWithValidators: typeof fetchWithValidators;
10
17
  }>;
11
18
  /** Node source creation fails closed when the OS trust store is unavailable or empty. */
12
19
  export declare const DiscoverySource: Readonly<{
13
20
  directoryId: typeof import("./source.ts").directoryId;
21
+ /** Create a discovery source after validating Node transport options. */
14
22
  create(input: unknown): Result<import("./source.ts").DiscoverySource>;
15
23
  }>;