@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/body.js
CHANGED
|
@@ -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,
|
|
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 ||
|
|
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
|
-
|
|
128
|
-
|
|
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 (
|
|
131
|
-
imported.alg =
|
|
132
|
-
const
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
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>;
|
package/dist/discovery/cache.js
CHANGED
|
@@ -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, {
|
|
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
|
-
|
|
153
|
-
|
|
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) =>
|
|
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";
|
package/dist/discovery/index.js
CHANGED
|
@@ -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
|
-
?
|
|
29
|
-
|
|
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, {
|
|
48
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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,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
|
+
}
|
package/dist/discovery/node.d.ts
CHANGED
|
@@ -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
|
}>;
|