@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.
- package/CHANGELOG.md +49 -1
- package/README.md +342 -114
- package/dist/accept-signature.d.ts +11 -0
- package/dist/accept-signature.js +7 -0
- package/dist/adapters/fetch/index.d.ts +10 -0
- package/dist/adapters/fetch/index.js +21 -4
- package/dist/adapters/fetch/runtime.d.ts +1 -0
- package/dist/adapters/fetch/runtime.js +1 -0
- package/dist/adapters/node-http/index.d.ts +23 -0
- package/dist/adapters/node-http/index.js +28 -8
- package/dist/brand.d.ts +5 -0
- package/dist/brand.js +12 -0
- package/dist/bytes.d.ts +4 -0
- package/dist/bytes.js +5 -0
- package/dist/constants.d.ts +64 -4
- package/dist/constants.js +62 -2
- package/dist/context.d.ts +4 -0
- package/dist/context.js +3 -0
- package/dist/crypto/algorithm.d.ts +9 -0
- package/dist/crypto/algorithm.js +12 -0
- package/dist/crypto/index.d.ts +12 -2
- package/dist/crypto/index.js +34 -11
- package/dist/crypto/options.d.ts +3 -0
- package/dist/crypto/options.js +15 -0
- package/dist/crypto/primitive.d.ts +7 -3
- package/dist/crypto/primitive.js +36 -10
- package/dist/custody/content_key.d.ts +4 -0
- package/dist/custody/content_key.js +4 -2
- package/dist/custody/index.d.ts +2 -0
- package/dist/custody/index.js +5 -1
- package/dist/custody/local.d.ts +17 -1
- package/dist/custody/local.js +10 -5
- package/dist/custody/ssh-agent/index.d.ts +2 -0
- package/dist/custody/ssh-agent/index.js +11 -181
- package/dist/custody/ssh-agent/port.d.ts +19 -0
- package/dist/custody/ssh-agent/port.js +183 -0
- package/dist/digest.d.ts +22 -0
- package/dist/digest.js +15 -0
- package/dist/discovery/body.d.ts +19 -2
- package/dist/discovery/body.js +36 -11
- package/dist/discovery/cache.d.ts +21 -2
- package/dist/discovery/cache.js +105 -39
- package/dist/discovery/headers.d.ts +3 -0
- package/dist/discovery/headers.js +27 -1
- package/dist/discovery/index.d.ts +13 -1
- package/dist/discovery/index.js +31 -7
- package/dist/discovery/node-connection.d.ts +1 -1
- package/dist/discovery/node-connection.js +2 -2
- package/dist/discovery/node-trust.d.ts +2 -0
- package/dist/discovery/node-trust.js +9 -0
- package/dist/discovery/node.d.ts +9 -1
- package/dist/discovery/node.js +150 -26
- package/dist/discovery/proof.js +2 -2
- package/dist/discovery/snapshot.js +3 -0
- package/dist/discovery/source.d.ts +7 -0
- package/dist/discovery/source.js +5 -0
- package/dist/discovery/support.js +2 -3
- package/dist/errors/parity.d.ts +44 -3
- package/dist/errors/parity.js +45 -4
- package/dist/errors/types.d.ts +17 -3
- package/dist/errors/types.js +16 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/jose/header.d.ts +1 -0
- package/dist/jose/jwe.d.ts +9 -0
- package/dist/jose/jwe.js +17 -3
- package/dist/jose/jws.d.ts +11 -0
- package/dist/jose/jws.js +14 -2
- package/dist/jose/key-management.d.ts +11 -0
- package/dist/jose/key-management.js +13 -2
- package/dist/jose/nested.d.ts +6 -0
- package/dist/jose/nested.js +32 -2
- package/dist/json.d.ts +7 -0
- package/dist/key_handle.d.ts +12 -3
- package/dist/key_handle.js +32 -25
- package/dist/key_identity.d.ts +7 -0
- package/dist/key_identity.js +10 -2
- package/dist/message/index.d.ts +24 -0
- package/dist/message/index.js +22 -1
- package/dist/numbers.d.ts +6 -0
- package/dist/numbers.js +5 -0
- package/dist/policy.d.ts +27 -0
- package/dist/policy.js +42 -21
- package/dist/profile.d.ts +36 -0
- package/dist/profile.js +215 -0
- package/dist/public_key.d.ts +11 -2
- package/dist/public_key.js +31 -8
- package/dist/quorum/evaluation.d.ts +1 -1
- package/dist/quorum/evaluation.js +18 -4
- package/dist/quorum/verification.d.ts +2 -0
- package/dist/quorum/verification.js +1 -0
- package/dist/quorum.d.ts +9 -0
- package/dist/quorum.js +7 -1
- package/dist/replay/claim.d.ts +5 -0
- package/dist/replay/claim.js +9 -1
- package/dist/replay/index.d.ts +6 -0
- package/dist/replay/index.js +13 -1
- package/dist/replay/postgres.d.ts +3 -0
- package/dist/replay/postgres.js +7 -0
- package/dist/replay/receipt.d.ts +1 -0
- package/dist/replay/receipt.js +1 -0
- package/dist/replay/store.d.ts +1 -0
- package/dist/replay/store.js +1 -0
- package/dist/replay-port.d.ts +6 -1
- package/dist/result.d.ts +3 -0
- package/dist/result.js +2 -0
- package/dist/sign.d.ts +26 -0
- package/dist/sign.js +25 -3
- package/dist/signature-base/index.d.ts +2 -0
- package/dist/signature-base/index.js +5 -1
- package/dist/signature-fields.d.ts +5 -1
- package/dist/signature-fields.js +9 -1
- package/dist/structured-fields/index.d.ts +21 -0
- package/dist/structured-fields/index.js +14 -1
- package/dist/verification.d.ts +5 -1
- package/dist/verification.js +9 -6
- package/dist/verify-quorum.d.ts +1 -0
- package/dist/verify-quorum.js +30 -8
- package/dist/verify.d.ts +23 -0
- package/dist/verify.js +46 -14
- package/dist/web-bot-auth/index.d.ts +3 -0
- package/dist/web-bot-auth/index.js +3 -0
- package/dist/web-bot-auth/policy.d.ts +8 -0
- package/dist/web-bot-auth/policy.js +8 -4
- package/dist/web-bot-auth/verification.d.ts +8 -0
- package/dist/web-bot-auth/verification.js +4 -2
- package/dist/web-bot-auth/verify.js +2 -2
- package/package.json +29 -6
package/dist/discovery/node.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
});
|
package/dist/discovery/proof.js
CHANGED
|
@@ -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
|
}>;
|
package/dist/discovery/source.js
CHANGED
|
@@ -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
|
-
|
|
53
|
-
[Symbol.for("Deno.customInspect")]: { value: inspect },
|
|
52
|
+
...inspection(inspect),
|
|
54
53
|
});
|
|
55
54
|
return Object.freeze(value);
|
|
56
55
|
}
|
package/dist/errors/parity.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
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:
|
|
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:
|
|
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 {};
|