@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
@@ -3,18 +3,20 @@ import { promises as dns } from "node:dns";
3
3
  import { isIP } from "node:net";
4
4
  import * as tls from "node:tls";
5
5
  import { connect, checkServerIdentity } from "node:tls";
6
- import { err } from "../result.js";
6
+ import { X509Certificate } from "node:crypto";
7
+ import { err, ok } from "../result.js";
7
8
  import { withDeadline, isContext, run, remainingMs, } from "../context.js";
8
- import { discoveryError, custodyError } from "../errors/types.js";
9
+ import { discoveryError } from "../errors/types.js";
9
10
  import { encodeBase64 } from "../bytes.js";
10
11
  import { Discovery as CoreDiscovery } from "./index.js";
11
12
  import { sourceUrl, normalizedHost, sourceAuthority, sourceOrigin, isDiscoverySource, DiscoverySource as CoreSource, } from "./source.js";
12
13
  import { addressAllowed } from "./address.js";
13
14
  import { ensure, unwrap, failure, options, clockValue, systemClock, sslError, list, } from "./support.js";
14
- import { fields, header, expiry, mediaType } from "./headers.js";
15
+ import { fields, header, expiry, hasFreshness, mediaType } from "./headers.js";
15
16
  import { bodyDocument } from "./body.js";
16
17
  import { fromResource } from "./snapshot.js";
17
18
  import { decodeBody } from "./gzip.js";
19
+ import { systemCertificateError } from "./node-trust.js";
18
20
  import { checkPeer, allowConnectionRetry } from "./node-connection.js";
19
21
  const lookup = dns.lookup.bind(dns);
20
22
  function remaining(context) {
@@ -41,6 +43,10 @@ async function resolve(host, context, servers) {
41
43
  ]);
42
44
  if (found.some((r) => r.status === "rejected" && r.reason?.code === "ETIMEOUT"))
43
45
  throw discoveryError("deadline_exceeded");
46
+ // An authoritative NXDOMAIN on every query is a configuration error:
47
+ // the configured domain does not exist, so retrying cannot help.
48
+ if (found.every((r) => r.status === "rejected" && r.reason?.code === "ENOTFOUND"))
49
+ throw discoveryError("domain_not_found");
44
50
  answers = found.flatMap((r, i) => r.status === "fulfilled"
45
51
  ? r.value.map((address) => ({ address, family: i === 0 ? 4 : 6 }))
46
52
  : []);
@@ -53,21 +59,61 @@ async function resolve(host, context, servers) {
53
59
  try {
54
60
  answers = await lookup(host, { all: true });
55
61
  }
56
- catch {
62
+ catch (error) {
63
+ // getaddrinfo's authoritative name-not-found is the same configuration
64
+ // error; any other resolver trouble may succeed on a later retry.
65
+ if (error &&
66
+ typeof error === "object" &&
67
+ error.code === "ENOTFOUND")
68
+ throw discoveryError("domain_not_found");
57
69
  throw discoveryError("resolution_failed");
58
70
  }
59
71
  }
60
72
  remaining(context);
61
- ensure(answers.length >= 1 &&
62
- answers.length <= 64 &&
63
- answers.every((a) => isIP(a.address) === a.family), "resolution_failed");
64
- return [...new Set(answers.map((a) => a.address))];
73
+ ensure(answers.every((a) => isIP(a.address) === a.family), "resolution_failed");
74
+ // Match the reference resolver: IPv6 first, retaining order within each family,
75
+ // then remove duplicates before applying the address bound.
76
+ const addresses = [...new Set(answers.sort((a, b) => b.family - a.family).map((a) => a.address))];
77
+ ensure(addresses.length >= 1 && addresses.length <= 64, "resolution_failed");
78
+ return addresses;
65
79
  }
80
+ /**
81
+ * The OS trust store, or undefined when this runtime provides no usable one: Node before
82
+ * 22.15.0 (and Deno 2.5.6) lack `tls.getCACertificates`, and a host can return an empty
83
+ * store. Bundled roots are never a fallback (ADR 0007); callers get
84
+ * `discovery/transport_unavailable`, the same refusal as a denied store.
85
+ */
66
86
  function systemCertificates() {
67
87
  if (typeof tls.getCACertificates !== "function")
68
88
  return undefined;
69
- const certificates = tls.getCACertificates("system");
70
- return certificates.length > 0 ? certificates : undefined;
89
+ try {
90
+ const certificates = tls.getCACertificates("system");
91
+ return certificates.length > 0 ? certificates : undefined;
92
+ }
93
+ catch (error) {
94
+ systemCertificateError(error);
95
+ }
96
+ }
97
+ /**
98
+ * Check that the peer certificate names the host. An IP literal is matched against the
99
+ * certificate's iPAddress names by OpenSSL (`X509Certificate.checkIP`), never against DNS
100
+ * names or the subject. Node 22.23.1's `tls.checkServerIdentity` IDNA-normalizes the host
101
+ * before testing for an IP, and `domainToASCII("::1")` is "", so every IPv6 literal fails
102
+ * there with ERR_TLS_CERT_ALTNAME_INVALID; DNS hosts keep `tls.checkServerIdentity`.
103
+ */
104
+ function checkHostIdentity(host, cert) {
105
+ if (!isIP(host))
106
+ return checkServerIdentity(host, cert);
107
+ try {
108
+ if (new X509Certificate(cert.raw).checkIP(host) !== undefined)
109
+ return undefined;
110
+ }
111
+ catch {
112
+ // A certificate OpenSSL cannot parse names no address.
113
+ }
114
+ return Object.assign(new Error("certificate does not name the IP address"), {
115
+ code: "ERR_TLS_CERT_ALTNAME_INVALID",
116
+ });
71
117
  }
72
118
  function tlsSocket(ip, url, source, context) {
73
119
  remaining(context);
@@ -78,7 +124,7 @@ function tlsSocket(ip, url, source, context) {
78
124
  encodeBase64(der) +
79
125
  "\n-----END CERTIFICATE-----");
80
126
  if (!ca)
81
- throw custodyError("unsupported_format");
127
+ throw discoveryError("transport_unavailable");
82
128
  return new Promise((done, fail) => {
83
129
  const socket = connect(Object.assign(Object.create(null), {
84
130
  host: ip,
@@ -88,7 +134,7 @@ function tlsSocket(ip, url, source, context) {
88
134
  minVersion: "TLSv1.2",
89
135
  ALPNProtocols: ["http/1.1"],
90
136
  ca,
91
- checkServerIdentity: (_name, cert) => checkServerIdentity(host, cert),
137
+ checkServerIdentity: (_name, cert) => checkHostIdentity(host, cert),
92
138
  lookup: () => {
93
139
  throw discoveryError("address_denied");
94
140
  },
@@ -138,7 +184,7 @@ async function connected(url, source, context, servers) {
138
184
  }
139
185
  throw last;
140
186
  }
141
- async function get(url, source, context, servers) {
187
+ async function get(url, source, context, servers, etag) {
142
188
  const socket = await connected(url, source, context, servers);
143
189
  // HTTPS receives only an already connected, peer-checked TLS socket. No proxy,
144
190
  // pooled connection or late DNS lookup participates in this private agent.
@@ -168,6 +214,7 @@ async function get(url, source, context, servers) {
168
214
  : "application/json, application/jwk-set+json",
169
215
  "Accept-Encoding": "gzip",
170
216
  Connection: "close",
217
+ ...(typeof etag === "string" ? { "If-None-Match": etag } : {}),
171
218
  },
172
219
  lookup: () => {
173
220
  throw discoveryError("address_denied");
@@ -244,7 +291,14 @@ async function get(url, source, context, servers) {
244
291
  }
245
292
  };
246
293
  socket.prependListener("data", onWire);
247
- req.on("information", info => reject(discoveryError("unexpected_status", info.statusCode)));
294
+ // RFC 9110 Section 15.2: interim responses are provisional and precede
295
+ // the final status. Five are skipped, the reference bound; a sixth
296
+ // names a misbehaving publisher and rejects as a malformed response.
297
+ let interims = 0;
298
+ req.on("information", (info) => {
299
+ if (++interims > 5)
300
+ reject(discoveryError("invalid_response"));
301
+ });
248
302
  req.on("upgrade", (res, upgraded) => {
249
303
  reject(discoveryError("unexpected_status", res.statusCode));
250
304
  upgraded.destroy();
@@ -346,8 +400,8 @@ function redirectUrl(current, location) {
346
400
  throw discoveryError("redirect_denied");
347
401
  }
348
402
  }
349
- async function resource(url, source, context, servers, hops = 0) {
350
- const { status, fs, encoded } = await get(url, source, context, servers);
403
+ async function resource(url, source, context, servers, hops = 0, etag) {
404
+ const { status, fs, encoded } = await get(url, source, context, servers, etag);
351
405
  if ([301, 302, 303, 307, 308].includes(status)) {
352
406
  ensure(source.maxRedirects > 0, "redirect_denied");
353
407
  ensure(hops < source.maxRedirects, "redirect_limit");
@@ -356,8 +410,15 @@ async function resource(url, source, context, servers, hops = 0) {
356
410
  const target = redirectUrl(url, location);
357
411
  ensure(source.redirectScope === "any_https" ||
358
412
  sourceOrigin(target) === sourceOrigin(sourceUrl(source.location)), "redirect_denied");
359
- return resource(target, source, context, servers, hops + 1);
413
+ // A validator never crosses a redirect: the target is a different
414
+ // resource representation.
415
+ return resource(target, source, context, servers, hops + 1, undefined);
360
416
  }
417
+ // RFC 9111 Section 15.4.5: 304 is valid only as the answer to the
418
+ // conditional this client sent; an unsolicited 304 falls through to the
419
+ // unexpected-status rejection below.
420
+ if (status === 304 && typeof etag === "string")
421
+ return { notModified: true, fields: fs, location: url.href };
361
422
  if (status !== 200)
362
423
  throw discoveryError("unexpected_status", status);
363
424
  mediaType(fs, source.type);
@@ -367,12 +428,37 @@ async function resource(url, source, context, servers, hops = 0) {
367
428
  remaining(context);
368
429
  return { fields: fs, encoded, decoded, location: url.href };
369
430
  }
431
+ /** 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. */
370
432
  export async function fetch(source, input = {}) {
433
+ const r = await conditional(source, input, undefined);
434
+ // No validator was sent, so a 304 cannot occur; keep the total anyway.
435
+ if (r.ok && "notModified" in r.value && r.value.notModified === true)
436
+ return err(discoveryError("invalid_response"));
437
+ return r;
438
+ }
439
+ /** 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. */
440
+ export async function fetchWithValidators(source, etag, storedLifetime, input = {}) {
441
+ try {
442
+ ensure(typeof etag === "string" &&
443
+ etag.length >= 1 &&
444
+ etag.length <= 512 &&
445
+ Number.isSafeInteger(storedLifetime) &&
446
+ storedLifetime >= 0, "invalid_options");
447
+ }
448
+ catch (e) {
449
+ return failure(e);
450
+ }
451
+ return conditional(source, input, {
452
+ etag: etag,
453
+ lifetime: storedLifetime,
454
+ });
455
+ }
456
+ async function conditional(source, input, validator) {
371
457
  try {
372
458
  ensure(isDiscoverySource(source), "invalid_source");
373
459
  if (source.cacerts === "os" && !systemCertificates())
374
- return err(custodyError("unsupported_format"));
375
- const a = options(input, ["timeoutMs", "clock", "context", "dnsServers"]);
460
+ return err(discoveryError("transport_unavailable"));
461
+ const a = options(input, ["timeoutMs", "clock", "context", "dnsServers", "validators"]);
376
462
  const timeout = Object.hasOwn(a, "timeoutMs")
377
463
  ? a.timeoutMs
378
464
  : source.timeoutMs;
@@ -409,15 +495,38 @@ export async function fetch(source, input = {}) {
409
495
  }), "invalid_options");
410
496
  const result = await run(context, async () => {
411
497
  try {
412
- const outer = await resource(sourceUrl(source.location), source, context, servers);
498
+ const outer = await resource(sourceUrl(source.location), source, context, servers, 0, validator?.etag);
413
499
  const now = clockValue(clock);
500
+ if ("notModified" in outer) {
501
+ // RFC 9111 Sections 3.2 and 4.3.4: a 304's own directives
502
+ // recalculate freshness; without them the stored representation's
503
+ // lifetime persists. A different strong validator identifies a
504
+ // different representation, so it cannot renew this one.
505
+ const updated = header(outer.fields, "etag");
506
+ ensure(updated === null || updated.length <= 512, "invalid_response");
507
+ ensure(updated === null || updated === validator.etag, "invalid_response");
508
+ const fresh = Object.freeze({
509
+ notModified: true,
510
+ etag: validator.etag,
511
+ fetchedAt: now,
512
+ expiresAt: hasFreshness(outer.fields)
513
+ ? expiry(outer.fields, source, now)
514
+ : now + validator.lifetime,
515
+ });
516
+ return ok(fresh);
517
+ }
414
518
  if (source.type === "cimd") {
415
519
  const doc = bodyDocument(outer.decoded, source);
416
520
  ensure(doc.client_id === source.location, "client_id_mismatch");
417
521
  ensure(!(Object.hasOwn(doc, "jwks") && Object.hasOwn(doc, "jwks_uri")), "ambiguous_key_source");
418
522
  if (Object.hasOwn(doc, "jwks_uri")) {
419
523
  const url = sourceUrl(doc.jwks_uri);
420
- ensure(sourceOrigin(url) === sourceOrigin(sourceUrl(source.location)), "redirect_denied");
524
+ ensure(
525
+ // Compare against the origin that served this document, not the
526
+ // configured one: a permissive redirect scope can legitimately
527
+ // move the document to a mirror, and its jwks_uri names that
528
+ // mirror.
529
+ sourceOrigin(url) === sourceOrigin(sourceUrl(outer.location)), "redirect_denied");
421
530
  // The subresource has JWKS media/body ceilings, original CIMD provenance.
422
531
  const nestedSource = unwrap(DiscoverySource.create({
423
532
  ...source,
@@ -425,6 +534,10 @@ export async function fetch(source, input = {}) {
425
534
  location: url.href,
426
535
  }), "invalid_source");
427
536
  const nested = await resource(url, nestedSource, context, servers);
537
+ // The nested subresource is always fetched unconditionally, so a
538
+ // 304 cannot occur here; keep the rejection total anyway.
539
+ if ("notModified" in nested)
540
+ throw discoveryError("unexpected_status");
428
541
  const result = await fromResource(source, nested.decoded, nested.encoded, now, nested.fields, nested.location, expiry(outer.fields, source, now), true, context);
429
542
  remaining(context);
430
543
  return result;
@@ -446,14 +559,25 @@ export async function fetch(source, input = {}) {
446
559
  return failure(error, "invalid_options");
447
560
  }
448
561
  }
449
- export const Discovery = Object.freeze({ ...CoreDiscovery, fetch });
562
+ /** Discovery resolution with the explicit Node HTTPS transport. */
563
+ export const Discovery = Object.freeze({ ...CoreDiscovery,
564
+ /** Fetch a discovery key set through the bounded Node HTTPS transport. */
565
+ fetch,
566
+ /** Conditionally fetch a discovery key set (RFC 9111 Section 4.3.4). */
567
+ fetchWithValidators });
450
568
  /** Node source creation fails closed when the OS trust store is unavailable or empty. */
451
569
  export const DiscoverySource = Object.freeze({
452
570
  ...CoreSource,
571
+ /** Create a discovery source after validating Node transport options. */
453
572
  create(input) {
454
- const result = CoreSource.create(input);
455
- if (result.ok && result.value.cacerts === "os" && !systemCertificates())
456
- return err(custodyError("unsupported_format"));
457
- return result;
573
+ try {
574
+ const result = CoreSource.create(input);
575
+ if (result.ok && result.value.cacerts === "os" && !systemCertificates())
576
+ return err(discoveryError("transport_unavailable"));
577
+ return result;
578
+ }
579
+ catch (error) {
580
+ return failure(error, "invalid_source");
581
+ }
458
582
  },
459
583
  });
@@ -4,7 +4,7 @@ import { Message, Body, TransportFacts, } from "../message/index.js";
4
4
  import { SignatureBase } from "../signature-base/index.js";
5
5
  import { Crypto } from "../crypto/index.js";
6
6
  import { digest } from "../crypto/subtle.js";
7
- import { algorithms } from "../crypto/algorithm.js";
7
+ import { algorithms, joseId } from "../crypto/algorithm.js";
8
8
  import { encodeBase64 } from "../bytes.js";
9
9
  import { sourceAuthority, sourceUrl, thumbprintId } from "./source.js";
10
10
  import { ensure, unwrap } from "./support.js";
@@ -145,7 +145,7 @@ export async function possessionProof(source, keys, location, fs, encoded, now,
145
145
  for (const algorithm of unwrap(algorithms(), "directory_signature_invalid")) {
146
146
  ensure(!context || remainingMs(context) > 0, "deadline_exceeded");
147
147
  if ((entry.assertedAlgorithm === null ||
148
- entry.assertedAlgorithm === algorithm) &&
148
+ entry.assertedAlgorithm === joseId(algorithm)) &&
149
149
  entry.key.bind(algorithm).ok &&
150
150
  (!p.has("alg") || textParam(p, "alg") === algorithm)) {
151
151
  if ((await Crypto.verify(algorithm, entry.key, sig.value.value, base)).ok) {
@@ -57,6 +57,8 @@ export async function fromResource(source, decoded, encoded, now, fs, location,
57
57
  }
58
58
  const bodyRevision = await revision(decoded);
59
59
  ensure(!context || remainingMs(context) > 0, "deadline_exceeded");
60
+ const storedEtag = header(fs, "etag");
61
+ ensure(storedEtag === null || storedEtag.length <= 512, "invalid_response");
60
62
  return ok(keySet({
61
63
  source,
62
64
  origin: sourceOrigin(sourceUrl(location)),
@@ -65,6 +67,7 @@ export async function fromResource(source, decoded, encoded, now, fs, location,
65
67
  expiresAt,
66
68
  revision: bodyRevision,
67
69
  proof: proven.proof,
70
+ etag: storedEtag,
68
71
  }));
69
72
  }
70
73
  catch (e) {
@@ -1,5 +1,7 @@
1
1
  import { type Result } from "../result.ts";
2
+ /** Supported discovery source kinds. @inline */
2
3
  export type SourceType = "directory" | "jwks_uri" | "cimd";
4
+ /** A branded HTTPS discovery source with bounded transport and cache policies. */
3
5
  export type DiscoverySource = Readonly<{
4
6
  type: SourceType;
5
7
  location: string;
@@ -18,9 +20,11 @@ export type DiscoverySource = Readonly<{
18
20
  maxTtl: number;
19
21
  negativeTtl: number;
20
22
  }>;
23
+ /** Check the private brand of a discovery source. */
21
24
  export declare function isDiscoverySource(s: unknown): s is DiscoverySource;
22
25
  export declare function sourceIdentity(s: DiscoverySource): string;
23
26
  export declare function thumbprintId(value: unknown): value is string;
27
+ /** Check whether a string is a valid directory key identifier. */
24
28
  export declare function directoryId(value: unknown): Result<boolean>;
25
29
  export declare function keyIdValid(s: DiscoverySource, v: unknown): v is string;
26
30
  /** Validate raw spelling before WHATWG normalization can erase a dot segment. */
@@ -29,7 +33,10 @@ export declare function normalizedHost(url: URL): string;
29
33
  export declare function sourceOrigin(url: URL): string;
30
34
  export declare function sourceAuthority(url: URL): string;
31
35
  export declare function createSource(input: unknown): Result<DiscoverySource>;
36
+ /** Discovery source construction and directory identifier validation. */
32
37
  export declare const DiscoverySource: Readonly<{
38
+ /** Snapshot, validate, and brand an HTTPS discovery source. */
33
39
  create: typeof createSource;
40
+ /** Check the directory key identifier syntax. */
34
41
  directoryId: typeof directoryId;
35
42
  }>;
@@ -15,6 +15,7 @@ const limits = Object.freeze({
15
15
  maxTtl: 86400,
16
16
  negativeTtl: 30,
17
17
  });
18
+ /** Check the private brand of a discovery source. */
18
19
  export function isDiscoverySource(s) {
19
20
  return !!s && typeof s === "object" && sources.has(s);
20
21
  }
@@ -33,6 +34,7 @@ function validDirectoryId(v) {
33
34
  v.length <= 256 &&
34
35
  /^[\x20-\x7e]+$/.test(v));
35
36
  }
37
+ /** Check whether a string is a valid directory key identifier. */
36
38
  export function directoryId(value) {
37
39
  return ok(validDirectoryId(value));
38
40
  }
@@ -193,7 +195,10 @@ export function createSource(input) {
193
195
  return failure(e, "invalid_source");
194
196
  }
195
197
  }
198
+ /** Discovery source construction and directory identifier validation. */
196
199
  export const DiscoverySource = Object.freeze({
200
+ /** Snapshot, validate, and brand an HTTPS discovery source. */
197
201
  create: createSource,
202
+ /** Check the directory key identifier syntax. */
198
203
  directoryId,
199
204
  });
@@ -1,4 +1,4 @@
1
- import { snapshotOptions, deepFreeze } from "../brand.js";
1
+ import { snapshotOptions, deepFreeze, inspection } from "../brand.js";
2
2
  import { discoveryError, isRequestSealError } from "../errors/types.js";
3
3
  import { err } from "../result.js";
4
4
  export function ensure(value, reason) {
@@ -49,8 +49,7 @@ export function inspectable(value, facts) {
49
49
  const inspect = () => Object.freeze({ ...facts });
50
50
  Object.defineProperties(value, {
51
51
  toJSON: { value: inspect },
52
- [Symbol.for("nodejs.util.inspect.custom")]: { value: inspect },
53
- [Symbol.for("Deno.customInspect")]: { value: inspect },
52
+ ...inspection(inspect),
54
53
  });
55
54
  return Object.freeze(value);
56
55
  }
@@ -1,4 +1,5 @@
1
- export declare const ELIXIR_ERROR_PIN = "b2392871933d99641a7cb2ca2726aefd20081dee";
1
+ /** The immutable reference release used for the error vocabulary. */
2
+ export declare const ELIXIR_ERROR_PIN = "53b6690ccd5036d58b280d08cf0cacc6427262fe";
2
3
  declare const retryableRules: {
3
4
  readonly core: {
4
5
  readonly actor_unbound: {
@@ -37,6 +38,10 @@ declare const retryableRules: {
37
38
  readonly kind: "constant";
38
39
  readonly value: false;
39
40
  };
41
+ readonly deadline: {
42
+ readonly kind: "constant";
43
+ readonly value: true;
44
+ };
40
45
  readonly digest_mismatch: {
41
46
  readonly kind: "constant";
42
47
  readonly value: false;
@@ -65,6 +70,10 @@ declare const retryableRules: {
65
70
  readonly kind: "constant";
66
71
  readonly value: false;
67
72
  };
73
+ readonly internal: {
74
+ readonly kind: "constant";
75
+ readonly value: false;
76
+ };
68
77
  readonly invalid_accept_signature: {
69
78
  readonly kind: "constant";
70
79
  readonly value: false;
@@ -511,6 +520,10 @@ declare const retryableRules: {
511
520
  readonly kind: "constant";
512
521
  readonly value: false;
513
522
  };
523
+ readonly invalid_options: {
524
+ readonly kind: "constant";
525
+ readonly value: false;
526
+ };
514
527
  readonly invalid_signature: {
515
528
  readonly kind: "constant";
516
529
  readonly value: false;
@@ -639,6 +652,10 @@ declare const retryableRules: {
639
652
  readonly kind: "constant";
640
653
  readonly value: false;
641
654
  };
655
+ readonly domain_not_found: {
656
+ readonly kind: "constant";
657
+ readonly value: false;
658
+ };
642
659
  readonly invalid_key_set: {
643
660
  readonly kind: "constant";
644
661
  readonly value: false;
@@ -669,7 +686,7 @@ declare const retryableRules: {
669
686
  };
670
687
  readonly resolution_failed: {
671
688
  readonly kind: "constant";
672
- readonly value: false;
689
+ readonly value: true;
673
690
  };
674
691
  readonly revoked_key: {
675
692
  readonly kind: "constant";
@@ -696,7 +713,7 @@ declare const retryableRules: {
696
713
  readonly minimum: 500;
697
714
  readonly maximum: 599;
698
715
  readonly path: "lib/request_seal/discovery.ex";
699
- readonly line: 301;
716
+ readonly line: 517;
700
717
  };
701
718
  readonly unknown_key: {
702
719
  readonly kind: "constant";
@@ -903,6 +920,7 @@ declare const retryableRules: {
903
920
  };
904
921
  };
905
922
  export declare const ERROR_RETRYABLE: typeof retryableRules;
923
+ /** The pinned reason, layer, and retryability table. */
906
924
  export declare const ERROR_PARITY: readonly Readonly<{
907
925
  readonly kind: "core";
908
926
  readonly layer: "binding";
@@ -1113,6 +1131,11 @@ export declare const ERROR_PARITY: readonly Readonly<{
1113
1131
  readonly layer: "input";
1114
1132
  readonly reason: "unsupported_component";
1115
1133
  readonly rule_id: "core/input.unsupported_component";
1134
+ } | {
1135
+ readonly kind: "core";
1136
+ readonly layer: "internal";
1137
+ readonly reason: "internal";
1138
+ readonly rule_id: "core/internal.internal";
1116
1139
  } | {
1117
1140
  readonly kind: "core";
1118
1141
  readonly layer: "key";
@@ -1198,6 +1221,11 @@ export declare const ERROR_PARITY: readonly Readonly<{
1198
1221
  readonly layer: "quorum";
1199
1222
  readonly reason: "binding_unsatisfied";
1200
1223
  readonly rule_id: "core/quorum.binding_unsatisfied";
1224
+ } | {
1225
+ readonly kind: "core";
1226
+ readonly layer: "quorum";
1227
+ readonly reason: "deadline";
1228
+ readonly rule_id: "core/quorum.deadline";
1201
1229
  } | {
1202
1230
  readonly kind: "core";
1203
1231
  readonly layer: "quorum";
@@ -1480,6 +1508,10 @@ export declare const ERROR_PARITY: readonly Readonly<{
1480
1508
  readonly kind: "crypto";
1481
1509
  readonly reason: "invalid_key";
1482
1510
  readonly rule_id: "crypto/invalid_key";
1511
+ } | {
1512
+ readonly kind: "crypto";
1513
+ readonly reason: "invalid_options";
1514
+ readonly rule_id: "crypto/invalid_options";
1483
1515
  } | {
1484
1516
  readonly kind: "crypto";
1485
1517
  readonly reason: "invalid_signature";
@@ -1604,6 +1636,10 @@ export declare const ERROR_PARITY: readonly Readonly<{
1604
1636
  readonly kind: "discovery";
1605
1637
  readonly reason: "directory_unsigned";
1606
1638
  readonly rule_id: "discovery/directory_unsigned";
1639
+ } | {
1640
+ readonly kind: "discovery";
1641
+ readonly reason: "domain_not_found";
1642
+ readonly rule_id: "discovery/domain_not_found";
1607
1643
  } | {
1608
1644
  readonly kind: "discovery";
1609
1645
  readonly reason: "invalid_key_set";
@@ -1882,13 +1918,18 @@ export declare const ERROR_PARITY: readonly Readonly<{
1882
1918
  readonly reason: "untrusted_proxy";
1883
1919
  readonly rule_id: "adapter/untrusted_proxy";
1884
1920
  }>[];
1921
+ /** A pinned error reason and its layers and retryability metadata. */
1885
1922
  export type ParityEntry = typeof ERROR_PARITY[number];
1923
+ /** An error family present in the pinned parity table. */
1886
1924
  export type ErrorKind = ParityEntry["kind"];
1925
+ /** The allowed reason vocabulary for an error family. */
1887
1926
  export type Reason<K extends ErrorKind> = Extract<ParityEntry, {
1888
1927
  kind: K;
1889
1928
  }>["reason"];
1929
+ /** The allowed layer vocabulary for an error family. */
1890
1930
  export type Layer<K extends "core" | "jose"> = Extract<ParityEntry, {
1891
1931
  kind: K;
1892
1932
  }>["layer"];
1933
+ /** The complete set of rule IDs derived from the parity table. */
1893
1934
  export declare const RULE_IDS: ReadonlySet<string>;
1894
1935
  export {};