@fora-protocol/sdk 1.0.3
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/LICENSE +201 -0
- package/README.md +37 -0
- package/gen/ts/vocab/functiontokens.js +78 -0
- package/gen/ts/vocab/functiontokens.ts +84 -0
- package/gen/ts/vocab/geographytokens.js +30 -0
- package/gen/ts/vocab/geographytokens.ts +37 -0
- package/gen/ts/vocab/pricingunits.js +56 -0
- package/gen/ts/vocab/pricingunits.ts +63 -0
- package/gen/ts/vocab/quotametrics.js +40 -0
- package/gen/ts/vocab/quotametrics.ts +47 -0
- package/gen/ts/vocab/usertypes.js +40 -0
- package/gen/ts/vocab/usertypes.ts +46 -0
- package/gen/ts/wire/base.js +245 -0
- package/gen/ts/wire/base.ts +255 -0
- package/gen/ts/wire/names.js +41 -0
- package/gen/ts/wire/names.ts +42 -0
- package/gen/ts/wire/schemas.js +111 -0
- package/gen/ts/wire/schemas.ts +216 -0
- package/package.json +164 -0
- package/sdk/ts/client/content.d.ts +74 -0
- package/sdk/ts/client/content.js +466 -0
- package/sdk/ts/client/errors.d.ts +79 -0
- package/sdk/ts/client/errors.js +148 -0
- package/sdk/ts/client/index.d.ts +241 -0
- package/sdk/ts/client/index.js +740 -0
- package/sdk/ts/client/route.d.ts +41 -0
- package/sdk/ts/client/route.js +107 -0
- package/sdk/ts/client/send.d.ts +64 -0
- package/sdk/ts/client/send.js +142 -0
- package/sdk/ts/client/transport.d.ts +187 -0
- package/sdk/ts/client/transport.js +381 -0
- package/sdk/ts/core/multisig-parse.d.ts +47 -0
- package/sdk/ts/core/multisig-parse.js +234 -0
- package/sdk/ts/core/sign-request.d.ts +83 -0
- package/sdk/ts/core/sign-request.js +146 -0
- package/sdk/ts/core/sign.d.ts +31 -0
- package/sdk/ts/core/sign.js +82 -0
- package/sdk/ts/core/signing-transport.d.ts +75 -0
- package/sdk/ts/core/signing-transport.js +172 -0
- package/sdk/ts/core/verifier.d.ts +177 -0
- package/sdk/ts/core/verifier.js +269 -0
- package/sdk/ts/core/verify-multisig-request.d.ts +72 -0
- package/sdk/ts/core/verify-multisig-request.js +112 -0
- package/sdk/ts/core/verify-request.d.ts +181 -0
- package/sdk/ts/core/verify-request.js +284 -0
- package/sdk/ts/core/window.d.ts +22 -0
- package/sdk/ts/core/window.js +43 -0
- package/sdk/ts/core/wire-canon.d.ts +20 -0
- package/sdk/ts/core/wire-canon.js +174 -0
- package/sdk/ts/hono/middleware.d.ts +38 -0
- package/sdk/ts/hono/middleware.js +64 -0
- package/sdk/ts/resolvers/errors.d.ts +130 -0
- package/sdk/ts/resolvers/errors.js +150 -0
- package/sdk/ts/resolvers/http.d.ts +98 -0
- package/sdk/ts/resolvers/http.js +274 -0
- package/sdk/ts/resolvers/index.d.ts +9 -0
- package/sdk/ts/resolvers/index.js +15 -0
- package/sdk/ts/resolvers/jwks.d.ts +2 -0
- package/sdk/ts/resolvers/jwks.js +43 -0
- package/sdk/ts/resolvers/offer-key-cache.d.ts +68 -0
- package/sdk/ts/resolvers/offer-key-cache.js +152 -0
- package/sdk/ts/resolvers/registration-requirements.d.ts +47 -0
- package/sdk/ts/resolvers/registration-requirements.js +220 -0
- package/sdk/ts/resolvers/ssrf.d.ts +28 -0
- package/sdk/ts/resolvers/ssrf.js +235 -0
- package/sdk/ts/resolvers/static.d.ts +7 -0
- package/sdk/ts/resolvers/static.js +16 -0
- package/sdk/ts/resolvers/wba.d.ts +134 -0
- package/sdk/ts/resolvers/wba.js +506 -0
- package/sdk/ts/resolvers/wellknown.d.ts +28 -0
- package/sdk/ts/resolvers/wellknown.js +169 -0
- package/sdk/ts/src/acceptance.d.ts +46 -0
- package/sdk/ts/src/acceptance.js +162 -0
- package/sdk/ts/src/base64url.d.ts +19 -0
- package/sdk/ts/src/base64url.js +55 -0
- package/sdk/ts/src/crossfield.d.ts +28 -0
- package/sdk/ts/src/crossfield.js +215 -0
- package/sdk/ts/src/endpoint-rule.d.ts +30 -0
- package/sdk/ts/src/endpoint-rule.js +63 -0
- package/sdk/ts/src/errordetail.d.ts +115 -0
- package/sdk/ts/src/errordetail.js +256 -0
- package/sdk/ts/src/hashurl.d.ts +5 -0
- package/sdk/ts/src/hashurl.js +15 -0
- package/sdk/ts/src/host-ref.d.ts +70 -0
- package/sdk/ts/src/host-ref.js +320 -0
- package/sdk/ts/src/hosts.d.ts +138 -0
- package/sdk/ts/src/hosts.js +214 -0
- package/sdk/ts/src/idempotency.d.ts +11 -0
- package/sdk/ts/src/idempotency.js +27 -0
- package/sdk/ts/src/jsondepth.d.ts +27 -0
- package/sdk/ts/src/jsondepth.js +77 -0
- package/sdk/ts/src/licenseterm.d.ts +113 -0
- package/sdk/ts/src/licenseterm.js +433 -0
- package/sdk/ts/src/money.d.ts +21 -0
- package/sdk/ts/src/money.js +60 -0
- package/sdk/ts/src/offer-sign.d.ts +11 -0
- package/sdk/ts/src/offer-sign.js +31 -0
- package/sdk/ts/src/opaque-url.d.ts +3 -0
- package/sdk/ts/src/opaque-url.js +20 -0
- package/sdk/ts/src/pop.d.ts +51 -0
- package/sdk/ts/src/pop.js +166 -0
- package/sdk/ts/src/regschema.d.ts +229 -0
- package/sdk/ts/src/regschema.js +1315 -0
- package/sdk/ts/src/scopes.d.ts +20 -0
- package/sdk/ts/src/scopes.js +45 -0
- package/sdk/ts/src/signurl.d.ts +23 -0
- package/sdk/ts/src/signurl.js +107 -0
- package/sdk/ts/src/thumbprint.d.ts +14 -0
- package/sdk/ts/src/thumbprint.js +24 -0
- package/sdk/ts/src/verify.d.ts +49 -0
- package/sdk/ts/src/verify.js +109 -0
- package/sdk/ts/src/wire-names.d.ts +1 -0
- package/sdk/ts/src/wire-names.js +16 -0
- package/sdk/ts/src/wire.d.ts +68 -0
- package/sdk/ts/src/wire.js +114 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// Reading what one Exchange asks of a registration — the terms revision that
|
|
2
|
+
// submitting one accepts, and the schema its registration_data must match — out of
|
|
3
|
+
// that Exchange's own /.well-known/fora.json. TS port of the Go oracle
|
|
4
|
+
// (resolvers/registrationrequirements.go).
|
|
5
|
+
//
|
|
6
|
+
// EVERY READ IS A FRESH FETCH, and that is the whole design. The protocol requires
|
|
7
|
+
// it in as many words: a registering client MUST read the terms digest from a
|
|
8
|
+
// freshly fetched manifest rather than a cached copy. A cached ENDPOINT is fine — a
|
|
9
|
+
// wrong one fails loudly — but a cached DIGEST is not, because a client cannot
|
|
10
|
+
// detect staleness locally, so a warm cache would make it echo a value the Exchange
|
|
11
|
+
// has already stopped accepting and retry the same refusal until the cache expired.
|
|
12
|
+
//
|
|
13
|
+
// That is why this is a SEPARATE face rather than a third method on the endpoint
|
|
14
|
+
// resolver. That resolver is built out of exactly the mechanism this value may not
|
|
15
|
+
// touch — a per-host TTL cache with single-flight coalescing on top — and it exposes
|
|
16
|
+
// no bypass. A face that holds no document cache leaves no cache slot to reuse,
|
|
17
|
+
// which is what makes the rule structural rather than a convention callers remember.
|
|
18
|
+
//
|
|
19
|
+
// Not even the compiled validator is held. Memoising it is a cache, and the useful
|
|
20
|
+
// key for one is a property of a deployment's threat model rather than of the
|
|
21
|
+
// protocol; an application that wants it wraps this face.
|
|
22
|
+
import { isBareDomain } from "../src/hosts.js";
|
|
23
|
+
import { invalidHost } from "../src/host-ref.js";
|
|
24
|
+
import { compileRegistrationSchema, } from "../src/regschema.js";
|
|
25
|
+
import { manifestVersionRefusal, WellKnownPath } from "../src/wire.js";
|
|
26
|
+
import { DirectoryUnavailable, ExchangeNotPermitted, ManifestNotExchange, ManifestUnusable, } from "./errors.js";
|
|
27
|
+
import { fetchStrict, guardedFetchFromEnv } from "./http.js";
|
|
28
|
+
/** Read registration requirements from an Exchange's own well-known manifest. */
|
|
29
|
+
export function createWellKnownRequirementsReader(opts = {}) {
|
|
30
|
+
// Built ONCE per reader, never per read: guardedFetchFromEnv constructs a
|
|
31
|
+
// dispatcher, so a per-read default would trade an unguarded dial for a socket
|
|
32
|
+
// leak.
|
|
33
|
+
const fetchFn = opts.fetch ?? guardedFetchFromEnv();
|
|
34
|
+
const scheme = opts.scheme && opts.scheme !== "" ? opts.scheme : "https";
|
|
35
|
+
const allow = opts.allow;
|
|
36
|
+
return {
|
|
37
|
+
async resolveRegistrationRequirements(exchange) {
|
|
38
|
+
// The SHAPE predicate is the contract's isBareDomain, not the routing tier's
|
|
39
|
+
// isBareHost. The two are kept apart deliberately and this leg is where the
|
|
40
|
+
// difference bites: nothing upstream has run the contract's rule, and the URL
|
|
41
|
+
// below is built by concatenation, so a value carrying a path or userinfo
|
|
42
|
+
// would choose WHAT is fetched rather than merely where from.
|
|
43
|
+
//
|
|
44
|
+
// The document's own version is read BEFORE any other member below, which is the
|
|
45
|
+
// rule the contract states for every consumer of this document and not only for
|
|
46
|
+
// the face that reads an endpoint out of it. A layout this reader cannot classify
|
|
47
|
+
// does not get to supply a terms digest that a request signature then covers.
|
|
48
|
+
if (!isBareDomain(exchange)) {
|
|
49
|
+
throw invalidHost(exchange, "not a bare domain");
|
|
50
|
+
}
|
|
51
|
+
if (allow && !allow(exchange)) {
|
|
52
|
+
throw new ExchangeNotPermitted(`exchange ${exchange} not permitted by policy`);
|
|
53
|
+
}
|
|
54
|
+
const url = `${scheme}://${exchange}${WellKnownPath}`;
|
|
55
|
+
const body = await fetchStrict(fetchFn, url);
|
|
56
|
+
let doc;
|
|
57
|
+
try {
|
|
58
|
+
doc = JSON.parse(body);
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
throw new DirectoryUnavailable(`manifest decode ${url}`, { cause: err });
|
|
62
|
+
}
|
|
63
|
+
// The document version gate, before any other member is read — the contract's own
|
|
64
|
+
// ordering, and the same call the sibling endpoint face makes. A document that is
|
|
65
|
+
// not an object carries no `ver`, so it is refused here as an absent one rather
|
|
66
|
+
// than through a guard of its own: a body of bare `null` is a body with no
|
|
67
|
+
// version, whatever else it is, and giving it a separate answer is what made the
|
|
68
|
+
// three languages disagree about it.
|
|
69
|
+
//
|
|
70
|
+
// The refusal wears THIS seam's verdict, never the endpoint seam's
|
|
71
|
+
// ManifestVersionRefused. The two vocabularies are disjoint on purpose: one
|
|
72
|
+
// answers whether an endpoint may be dialled, this one whether a document can be
|
|
73
|
+
// read for what a registration owes.
|
|
74
|
+
const versionRefusal = manifestVersionRefusal(doc?.ver);
|
|
75
|
+
if (versionRefusal !== undefined) {
|
|
76
|
+
throw new ManifestUnusable(versionRefusal);
|
|
77
|
+
}
|
|
78
|
+
if (!describesExchange(doc)) {
|
|
79
|
+
throw new ManifestNotExchange(`host=${exchange}`);
|
|
80
|
+
}
|
|
81
|
+
const digest = doc.terms_digest;
|
|
82
|
+
const out = {
|
|
83
|
+
termsDigest: typeof digest === "string" ? digest : undefined,
|
|
84
|
+
schema: null,
|
|
85
|
+
verdict: "not_published",
|
|
86
|
+
};
|
|
87
|
+
// Compiled from the bytes AS SERVED. Every cap the registration-schema rules
|
|
88
|
+
// state is defined over those bytes, so the member is sliced out of the body
|
|
89
|
+
// rather than re-serialised from the parsed value: JSON.parse discards
|
|
90
|
+
// whitespace and reformats numbers, and a document that minifies under the
|
|
91
|
+
// cap while exceeding it on the wire would be refused by an Exchange and
|
|
92
|
+
// accepted here — the two-privately-chosen-limits failure the contract's
|
|
93
|
+
// numbers exist to prevent.
|
|
94
|
+
const raw = rawMember(body, "account_registration", "data_schema");
|
|
95
|
+
if (raw !== undefined) {
|
|
96
|
+
const compiled = compileRegistrationSchema(raw);
|
|
97
|
+
out.schema = compiled.schema;
|
|
98
|
+
out.verdict = compiled.verdict;
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/** proto-JSON renders an enum as its name or its number, so both are accepted. An
|
|
105
|
+
* absent or unrecognised role is not an Exchange. */
|
|
106
|
+
function describesExchange(doc) {
|
|
107
|
+
const role = doc.role;
|
|
108
|
+
return role === "ROLE_EXCHANGE" || role === 2;
|
|
109
|
+
}
|
|
110
|
+
/** rawMember returns the EXACT served text of a nested object member, or
|
|
111
|
+
* `undefined` when any step of the path is absent.
|
|
112
|
+
*
|
|
113
|
+
* It exists because JavaScript has no equivalent of a raw-message slice: the only
|
|
114
|
+
* way to hand a nested member to a rule defined over the bytes AS SERVED is to
|
|
115
|
+
* find its extent in the body. `body` has already parsed as JSON when this runs,
|
|
116
|
+
* so the scan below is over known-well-formed input and needs no error path — it
|
|
117
|
+
* returns `undefined` for anything it cannot walk, and the caller reads that as
|
|
118
|
+
* "no schema published", which is the safe direction.
|
|
119
|
+
*/
|
|
120
|
+
function rawMember(body, ...path) {
|
|
121
|
+
let start = 0;
|
|
122
|
+
for (let step = 0; step < path.length; step++) {
|
|
123
|
+
const found = memberExtent(body, start, path[step]);
|
|
124
|
+
if (found === undefined)
|
|
125
|
+
return undefined;
|
|
126
|
+
if (step === path.length - 1)
|
|
127
|
+
return body.slice(found[0], found[1]);
|
|
128
|
+
start = found[0];
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
/** memberExtent finds `key` in the object beginning at `from` and returns the
|
|
133
|
+
* [start, end) of its VALUE. */
|
|
134
|
+
function memberExtent(body, from, key) {
|
|
135
|
+
let i = skipWhitespace(body, from);
|
|
136
|
+
if (body[i] !== "{")
|
|
137
|
+
return undefined;
|
|
138
|
+
i = skipWhitespace(body, i + 1);
|
|
139
|
+
if (body[i] === "}")
|
|
140
|
+
return undefined;
|
|
141
|
+
for (;;) {
|
|
142
|
+
if (body[i] !== '"')
|
|
143
|
+
return undefined;
|
|
144
|
+
const keyEnd = skipString(body, i);
|
|
145
|
+
const name = JSON.parse(body.slice(i, keyEnd));
|
|
146
|
+
i = skipWhitespace(body, keyEnd);
|
|
147
|
+
if (body[i] !== ":")
|
|
148
|
+
return undefined;
|
|
149
|
+
const valueStart = skipWhitespace(body, i + 1);
|
|
150
|
+
const valueEnd = skipValue(body, valueStart);
|
|
151
|
+
if (valueEnd === undefined)
|
|
152
|
+
return undefined;
|
|
153
|
+
if (name === key)
|
|
154
|
+
return [valueStart, valueEnd];
|
|
155
|
+
i = skipWhitespace(body, valueEnd);
|
|
156
|
+
if (body[i] === ",") {
|
|
157
|
+
i = skipWhitespace(body, i + 1);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** RFC 8259 whitespace, and nothing wider — the same four bytes the blank-schema
|
|
164
|
+
* rule counts. */
|
|
165
|
+
function skipWhitespace(body, i) {
|
|
166
|
+
while (i < body.length) {
|
|
167
|
+
const c = body[i];
|
|
168
|
+
if (c !== " " && c !== "\t" && c !== "\r" && c !== "\n")
|
|
169
|
+
return i;
|
|
170
|
+
i++;
|
|
171
|
+
}
|
|
172
|
+
return i;
|
|
173
|
+
}
|
|
174
|
+
/** skipString returns the index just past a JSON string starting at `i`. */
|
|
175
|
+
function skipString(body, i) {
|
|
176
|
+
i++; // the opening quote
|
|
177
|
+
while (i < body.length) {
|
|
178
|
+
const c = body[i];
|
|
179
|
+
if (c === "\\") {
|
|
180
|
+
i += 2;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (c === '"')
|
|
184
|
+
return i + 1;
|
|
185
|
+
i++;
|
|
186
|
+
}
|
|
187
|
+
return i;
|
|
188
|
+
}
|
|
189
|
+
/** skipValue returns the index just past the JSON value starting at `i`, or
|
|
190
|
+
* `undefined` when the value does not terminate. */
|
|
191
|
+
function skipValue(body, i) {
|
|
192
|
+
const c = body[i];
|
|
193
|
+
if (c === '"')
|
|
194
|
+
return skipString(body, i);
|
|
195
|
+
if (c === "{" || c === "[") {
|
|
196
|
+
const close = c === "{" ? "}" : "]";
|
|
197
|
+
let depth = 0;
|
|
198
|
+
while (i < body.length) {
|
|
199
|
+
const ch = body[i];
|
|
200
|
+
if (ch === '"') {
|
|
201
|
+
i = skipString(body, i);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (ch === "{" || ch === "[")
|
|
205
|
+
depth++;
|
|
206
|
+
else if (ch === "}" || ch === "]") {
|
|
207
|
+
depth--;
|
|
208
|
+
if (depth === 0) {
|
|
209
|
+
return body[i] === close ? i + 1 : undefined;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
i++;
|
|
213
|
+
}
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
// A number, or one of the three literals: everything up to the next structural
|
|
217
|
+
// character. The body has already parsed, so whatever lies here is well formed.
|
|
218
|
+
const end = /[\s,}\]]/.exec(body.slice(i));
|
|
219
|
+
return end === null ? body.length : i + end.index;
|
|
220
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** The scheme allowlist the SSRF guard enforces: deny-by-default, only http and
|
|
2
|
+
* https may be dialed (mirrors the Go oracle's allowedScheme). Blocks
|
|
3
|
+
* file/data/gopher/dict/ftp/ldap and every other exotic scheme that would let a
|
|
4
|
+
* caller-supplied URL reach the local filesystem or unintended protocols. The
|
|
5
|
+
* comparison is case-insensitive (HTTPS ≡ https). */
|
|
6
|
+
export declare function allowedScheme(scheme: string): boolean;
|
|
7
|
+
/** The shared redirect-depth cap. Deny-by-default and IDENTICAL across the three
|
|
8
|
+
* SDKs (Go maxWBARedirects / httpx max_redirects): the guarded transport follows at
|
|
9
|
+
* most this many redirect hops and refuses the next, so no SDK inherits its HTTP
|
|
10
|
+
* library's looser default (undici/fetch follow ~20). The real transport enforces
|
|
11
|
+
* it via undici's `interceptors.redirect({ maxRedirections })`; the corpus pins the
|
|
12
|
+
* predicate below. */
|
|
13
|
+
export declare const MAX_REDIRECTS = 5;
|
|
14
|
+
/** Report whether a chain of `hops` redirects must be refused — the shared
|
|
15
|
+
* redirect-depth policy (refused iff it EXCEEDS MAX_REDIRECTS). Corpus-tested
|
|
16
|
+
* against the Go oracle's redirectChainRefused so the three SDKs agree on the
|
|
17
|
+
* boundary. */
|
|
18
|
+
export declare function redirectChainRefused(hops: number): boolean;
|
|
19
|
+
/** Reports whether `ip` (a numeric IPv4 or IPv6 literal — never a hostname) is
|
|
20
|
+
* a reserved / non-public address the SSRF guard must refuse to dial. It first
|
|
21
|
+
* unwraps v4-mapped and NAT64 forms and re-checks the embedded v4, so an IPv6
|
|
22
|
+
* literal embedding a private v4 cannot slip past a v6-only test. An
|
|
23
|
+
* unparseable input is treated as blocked (fail-closed: never dial what we
|
|
24
|
+
* cannot classify). */
|
|
25
|
+
export declare function blockedAddress(ip: string): boolean;
|
|
26
|
+
/** Parse an IPv4 or IPv6 literal into its network-order bytes (4 or 16), or
|
|
27
|
+
* `undefined` when it is not a bare numeric address. */
|
|
28
|
+
export declare function parseAddr(ip: string): Uint8Array | undefined;
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// The SSRF address guard for the fetching resolvers' DEFAULT transport. Ports
|
|
2
|
+
// sdk/go/helpers/wbakeyresolver.go (ssrfBlockedPrefixes / ssrfBlocked) 1:1.
|
|
3
|
+
//
|
|
4
|
+
// A WBA/well-known resolver fetches a directory from a caller-supplied
|
|
5
|
+
// Signature-Agent host BEFORE the ed25519 signature check, so an unguarded
|
|
6
|
+
// default is a pre-auth SSRF lever against internal networks. blockedAddress is
|
|
7
|
+
// the reserved / non-public address predicate the guarded default transport
|
|
8
|
+
// applies to EVERY resolved address before connecting. It replaces the
|
|
9
|
+
// IsPrivate/IsLinkLocal heuristic, which misses CGNAT (100.64.0.0/10),
|
|
10
|
+
// 0.0.0.0/8, the TEST-NETs, benchmarking, protocol-assignment, and other
|
|
11
|
+
// reserved ranges. IPv4-mapped (::ffff:a.b.c.d) and NAT64 (64:ff9b::a.b.c.d)
|
|
12
|
+
// forms are unwrapped and their embedded v4 re-checked, so an IPv6 literal
|
|
13
|
+
// cannot smuggle a private v4 past a v6-form-only test.
|
|
14
|
+
/** The scheme allowlist the SSRF guard enforces: deny-by-default, only http and
|
|
15
|
+
* https may be dialed (mirrors the Go oracle's allowedScheme). Blocks
|
|
16
|
+
* file/data/gopher/dict/ftp/ldap and every other exotic scheme that would let a
|
|
17
|
+
* caller-supplied URL reach the local filesystem or unintended protocols. The
|
|
18
|
+
* comparison is case-insensitive (HTTPS ≡ https). */
|
|
19
|
+
export function allowedScheme(scheme) {
|
|
20
|
+
const s = scheme.toLowerCase();
|
|
21
|
+
return s === "http" || s === "https";
|
|
22
|
+
}
|
|
23
|
+
/** The shared redirect-depth cap. Deny-by-default and IDENTICAL across the three
|
|
24
|
+
* SDKs (Go maxWBARedirects / httpx max_redirects): the guarded transport follows at
|
|
25
|
+
* most this many redirect hops and refuses the next, so no SDK inherits its HTTP
|
|
26
|
+
* library's looser default (undici/fetch follow ~20). The real transport enforces
|
|
27
|
+
* it via undici's `interceptors.redirect({ maxRedirections })`; the corpus pins the
|
|
28
|
+
* predicate below. */
|
|
29
|
+
export const MAX_REDIRECTS = 5;
|
|
30
|
+
/** Report whether a chain of `hops` redirects must be refused — the shared
|
|
31
|
+
* redirect-depth policy (refused iff it EXCEEDS MAX_REDIRECTS). Corpus-tested
|
|
32
|
+
* against the Go oracle's redirectChainRefused so the three SDKs agree on the
|
|
33
|
+
* boundary. */
|
|
34
|
+
export function redirectChainRefused(hops) {
|
|
35
|
+
return hops > MAX_REDIRECTS;
|
|
36
|
+
}
|
|
37
|
+
// The reserved / non-public prefix set the guard rejects — the EXACT set the Go
|
|
38
|
+
// oracle rejects (ssrfBlockedPrefixes), split v4 / v6.
|
|
39
|
+
const BLOCKED_CIDRS = [
|
|
40
|
+
// IPv4
|
|
41
|
+
"0.0.0.0/8",
|
|
42
|
+
"10.0.0.0/8",
|
|
43
|
+
"100.64.0.0/10",
|
|
44
|
+
"127.0.0.0/8",
|
|
45
|
+
"169.254.0.0/16",
|
|
46
|
+
"172.16.0.0/12",
|
|
47
|
+
"192.0.0.0/24",
|
|
48
|
+
"192.0.2.0/24",
|
|
49
|
+
"192.88.99.0/24",
|
|
50
|
+
"192.168.0.0/16",
|
|
51
|
+
"198.18.0.0/15",
|
|
52
|
+
"198.51.100.0/24",
|
|
53
|
+
"203.0.113.0/24",
|
|
54
|
+
"224.0.0.0/4",
|
|
55
|
+
"240.0.0.0/4",
|
|
56
|
+
"255.255.255.255/32",
|
|
57
|
+
// IPv6
|
|
58
|
+
"::/128",
|
|
59
|
+
"::1/128",
|
|
60
|
+
// IPv4-compatible IPv6 (::a.b.c.d) — deprecated; block the whole ::/96 so an
|
|
61
|
+
// embedded v4 cannot smuggle a reserved address through a v6 literal.
|
|
62
|
+
"::/96",
|
|
63
|
+
"100::/64",
|
|
64
|
+
"2001:db8::/32",
|
|
65
|
+
"2001::/23",
|
|
66
|
+
// 6to4 (RFC 3056) — block wholesale; a 2002:V4ADDR:: literal embeds an
|
|
67
|
+
// arbitrary v4 in bits 16..48 and must never be dialed.
|
|
68
|
+
"2002::/16",
|
|
69
|
+
"fc00::/7",
|
|
70
|
+
"fe80::/10",
|
|
71
|
+
"ff00::/8",
|
|
72
|
+
// NAT64 (the /96 well-known prefix's embedded v4 is also re-checked below)
|
|
73
|
+
"64:ff9b::/96",
|
|
74
|
+
"64:ff9b:1::/48",
|
|
75
|
+
];
|
|
76
|
+
// The well-known NAT64 prefix (RFC 6052). An address inside it embeds an IPv4
|
|
77
|
+
// in its trailing 32 bits, which must be re-checked for reserved-ness.
|
|
78
|
+
const NAT64_WELL_KNOWN = parseCidr("64:ff9b::/96");
|
|
79
|
+
const BLOCKED_PARSED = BLOCKED_CIDRS.map(parseCidr);
|
|
80
|
+
/** Reports whether `ip` (a numeric IPv4 or IPv6 literal — never a hostname) is
|
|
81
|
+
* a reserved / non-public address the SSRF guard must refuse to dial. It first
|
|
82
|
+
* unwraps v4-mapped and NAT64 forms and re-checks the embedded v4, so an IPv6
|
|
83
|
+
* literal embedding a private v4 cannot slip past a v6-only test. An
|
|
84
|
+
* unparseable input is treated as blocked (fail-closed: never dial what we
|
|
85
|
+
* cannot classify). */
|
|
86
|
+
export function blockedAddress(ip) {
|
|
87
|
+
const addr = parseAddr(ip);
|
|
88
|
+
if (!addr)
|
|
89
|
+
return true;
|
|
90
|
+
return blockedBytes(addr);
|
|
91
|
+
}
|
|
92
|
+
function blockedBytes(addr) {
|
|
93
|
+
// Unwrap an IPv4-mapped IPv6 (::ffff:a.b.c.d) to its 4-byte form so the v4
|
|
94
|
+
// prefix table applies. Then, for any remaining 16-byte address, re-check a
|
|
95
|
+
// NAT64-embedded v4 before the prefix scan.
|
|
96
|
+
const unmapped = unmapV4(addr);
|
|
97
|
+
if (unmapped.length === 16) {
|
|
98
|
+
const embedded = nat64EmbeddedV4(unmapped);
|
|
99
|
+
if (embedded && blockedBytes(embedded))
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
for (const cidr of BLOCKED_PARSED) {
|
|
103
|
+
if (cidr.bytes.length === unmapped.length && contains(cidr, unmapped))
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
/** Extract the trailing 32 bits of a 64:ff9b::/96 address as a 4-byte v4, or
|
|
109
|
+
* `undefined` when `addr` is not in the well-known NAT64 prefix. */
|
|
110
|
+
function nat64EmbeddedV4(addr) {
|
|
111
|
+
if (!contains(NAT64_WELL_KNOWN, addr))
|
|
112
|
+
return undefined;
|
|
113
|
+
return addr.slice(12, 16);
|
|
114
|
+
}
|
|
115
|
+
/** Unwrap an IPv4-mapped IPv6 address (::ffff:a.b.c.d) to its 4-byte v4 form;
|
|
116
|
+
* pass any other address through unchanged. */
|
|
117
|
+
function unmapV4(addr) {
|
|
118
|
+
if (addr.length !== 16)
|
|
119
|
+
return addr;
|
|
120
|
+
for (let i = 0; i < 10; i++)
|
|
121
|
+
if (addr[i] !== 0)
|
|
122
|
+
return addr;
|
|
123
|
+
if (addr[10] !== 0xff || addr[11] !== 0xff)
|
|
124
|
+
return addr;
|
|
125
|
+
return addr.slice(12, 16);
|
|
126
|
+
}
|
|
127
|
+
/** Whether `addr` (same byte-length as the CIDR) falls inside `cidr` — a masked
|
|
128
|
+
* byte comparison over the prefix length. */
|
|
129
|
+
function contains(cidr, addr) {
|
|
130
|
+
if (addr.length !== cidr.bytes.length)
|
|
131
|
+
return false;
|
|
132
|
+
let bits = cidr.bits;
|
|
133
|
+
for (let i = 0; i < cidr.bytes.length; i++) {
|
|
134
|
+
if (bits <= 0)
|
|
135
|
+
break;
|
|
136
|
+
const take = bits >= 8 ? 8 : bits;
|
|
137
|
+
const mask = take === 8 ? 0xff : (0xff << (8 - take)) & 0xff;
|
|
138
|
+
if ((addr[i] & mask) !== (cidr.bytes[i] & mask))
|
|
139
|
+
return false;
|
|
140
|
+
bits -= 8;
|
|
141
|
+
}
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
function parseCidr(cidr) {
|
|
145
|
+
const slash = cidr.lastIndexOf("/");
|
|
146
|
+
const addrPart = cidr.slice(0, slash);
|
|
147
|
+
const bits = Number(cidr.slice(slash + 1));
|
|
148
|
+
const bytes = parseAddr(addrPart);
|
|
149
|
+
if (!bytes)
|
|
150
|
+
throw new Error(`ssrf: unparseable CIDR ${cidr}`);
|
|
151
|
+
return { bytes, bits };
|
|
152
|
+
}
|
|
153
|
+
/** Parse an IPv4 or IPv6 literal into its network-order bytes (4 or 16), or
|
|
154
|
+
* `undefined` when it is not a bare numeric address. */
|
|
155
|
+
export function parseAddr(ip) {
|
|
156
|
+
if (ip.includes(":"))
|
|
157
|
+
return parseV6(ip);
|
|
158
|
+
return parseV4(ip);
|
|
159
|
+
}
|
|
160
|
+
function parseV4(ip) {
|
|
161
|
+
const parts = ip.split(".");
|
|
162
|
+
if (parts.length !== 4)
|
|
163
|
+
return undefined;
|
|
164
|
+
const out = new Uint8Array(4);
|
|
165
|
+
for (let i = 0; i < 4; i++) {
|
|
166
|
+
const p = parts[i];
|
|
167
|
+
if (!/^\d{1,3}$/.test(p))
|
|
168
|
+
return undefined;
|
|
169
|
+
const n = Number(p);
|
|
170
|
+
if (n > 255)
|
|
171
|
+
return undefined;
|
|
172
|
+
out[i] = n;
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
function parseV6(ip) {
|
|
177
|
+
// Strip a zone id (fe80::1%eth0) — irrelevant to the prefix check.
|
|
178
|
+
const zone = ip.indexOf("%");
|
|
179
|
+
if (zone >= 0)
|
|
180
|
+
ip = ip.slice(0, zone);
|
|
181
|
+
const halves = ip.split("::");
|
|
182
|
+
if (halves.length > 2)
|
|
183
|
+
return undefined;
|
|
184
|
+
const head = halves[0] === "" ? [] : halves[0].split(":");
|
|
185
|
+
const tailStr = halves.length === 2 ? halves[1] : undefined;
|
|
186
|
+
const tail = tailStr === undefined || tailStr === "" ? [] : tailStr.split(":");
|
|
187
|
+
const out = new Uint8Array(16);
|
|
188
|
+
let idx = 0;
|
|
189
|
+
const writeGroups = (groups, start) => {
|
|
190
|
+
let pos = start;
|
|
191
|
+
for (let g = 0; g < groups.length; g++) {
|
|
192
|
+
const grp = groups[g];
|
|
193
|
+
// A trailing IPv4 in the last group (e.g. ::ffff:1.2.3.4 or 64:ff9b::a.b.c.d).
|
|
194
|
+
if (grp.includes(".")) {
|
|
195
|
+
if (g !== groups.length - 1)
|
|
196
|
+
return false;
|
|
197
|
+
const v4 = parseV4(grp);
|
|
198
|
+
if (!v4)
|
|
199
|
+
return false;
|
|
200
|
+
if (pos + 4 > 16)
|
|
201
|
+
return false;
|
|
202
|
+
out.set(v4, pos);
|
|
203
|
+
pos += 4;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (!/^[0-9a-fA-F]{1,4}$/.test(grp))
|
|
207
|
+
return false;
|
|
208
|
+
if (pos + 2 > 16)
|
|
209
|
+
return false;
|
|
210
|
+
const val = parseInt(grp, 16);
|
|
211
|
+
out[pos] = (val >> 8) & 0xff;
|
|
212
|
+
out[pos + 1] = val & 0xff;
|
|
213
|
+
pos += 2;
|
|
214
|
+
}
|
|
215
|
+
return true;
|
|
216
|
+
};
|
|
217
|
+
// Count the bytes each side occupies (an embedded v4 counts as 4, not 2).
|
|
218
|
+
const byteLen = (groups) => groups.reduce((n, g) => n + (g.includes(".") ? 4 : 2), 0);
|
|
219
|
+
if (halves.length === 1) {
|
|
220
|
+
// No "::" — must be a full address.
|
|
221
|
+
if (byteLen(head) !== 16)
|
|
222
|
+
return undefined;
|
|
223
|
+
return writeGroups(head, 0) ? out : undefined;
|
|
224
|
+
}
|
|
225
|
+
const headBytes = byteLen(head);
|
|
226
|
+
const tailBytes = byteLen(tail);
|
|
227
|
+
if (headBytes + tailBytes > 16)
|
|
228
|
+
return undefined;
|
|
229
|
+
if (!writeGroups(head, 0))
|
|
230
|
+
return undefined;
|
|
231
|
+
idx = 16 - tailBytes;
|
|
232
|
+
if (!writeGroups(tail, idx))
|
|
233
|
+
return undefined;
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** The static key face. */
|
|
2
|
+
export interface StaticKeyResolver {
|
|
3
|
+
resolve(keyid: string): Promise<Uint8Array | undefined>;
|
|
4
|
+
put(keyid: string, pub: Uint8Array): void;
|
|
5
|
+
}
|
|
6
|
+
/** Return a resolver seeded with `keys` (copied). */
|
|
7
|
+
export declare function createStaticKeyResolver(keys?: Record<string, Uint8Array>): StaticKeyResolver;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// The static named key resolver face — a thin in-memory map satisfying the
|
|
2
|
+
// RequestKeyResolver-shaped `resolve(keyid) -> Promise<Uint8Array | undefined>` plus a
|
|
3
|
+
// `put` for dynamic/test seeding. Mirrors the Go NewStaticKeyResolver: a plain
|
|
4
|
+
// unknown key is `undefined` (the fall-through miss), never a thrown error.
|
|
5
|
+
/** Return a resolver seeded with `keys` (copied). */
|
|
6
|
+
export function createStaticKeyResolver(keys = {}) {
|
|
7
|
+
const map = new Map(Object.entries(keys));
|
|
8
|
+
return {
|
|
9
|
+
resolve(keyid) {
|
|
10
|
+
return Promise.resolve(map.get(keyid));
|
|
11
|
+
},
|
|
12
|
+
put(keyid, pub) {
|
|
13
|
+
map.set(keyid, pub);
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { WBAFileSchema } from "../../../gen/ts/wire/schemas.ts";
|
|
2
|
+
import { type FetchLike } from "./http.ts";
|
|
3
|
+
/** The single public well-known path a WBA identity directory is served at (Web
|
|
4
|
+
* Bot Auth; the identity half of the identity/commercial split — the commercial
|
|
5
|
+
* overlay stays in /.well-known/fora.json). The one shared copy across the whole SDK. */
|
|
6
|
+
export declare const WBA_DIRECTORY_PATH = "/.well-known/http-message-signatures-directory";
|
|
7
|
+
/** Build the full WBA identity-directory URL from a scheme and an already-joined
|
|
8
|
+
* host: `${scheme}://${host}` + {@link WBA_DIRECTORY_PATH}. An empty scheme
|
|
9
|
+
* defaults to https. A PURE string function — the host arrives ALREADY-JOINED (any
|
|
10
|
+
* port-join / IPv6 bracketing is the caller's concern), there is NO env read and NO
|
|
11
|
+
* scheme-in-host detection (those stay consumer glue). It mirrors the sdk/go
|
|
12
|
+
* WBADirectoryURL oracle byte-for-byte, locked by the tri-replayed
|
|
13
|
+
* wba-url-vectors.json corpus. */
|
|
14
|
+
export declare function wbaDirectoryURL(scheme: string, host: string): string;
|
|
15
|
+
type WBAFile = ReturnType<typeof WBAFileSchema.parse>;
|
|
16
|
+
/** Options for the WBA resolver. Zero values are safe defaults; tests inject the
|
|
17
|
+
* clock (`now`), the poll timer (`after`), and the armed/cycle seams. */
|
|
18
|
+
export interface WBAKeyResolverOptions {
|
|
19
|
+
scheme?: string;
|
|
20
|
+
ttlMs?: number;
|
|
21
|
+
pollIntervalMs?: number;
|
|
22
|
+
/** Throttle for the unknown-thumbprint force-refresh, per directory host (≤0 →
|
|
23
|
+
* 5000). The resolver runs BEFORE the ed25519 check and the host is the
|
|
24
|
+
* caller-supplied Signature-Agent, so this caps the outbound directory GETs an
|
|
25
|
+
* unauthenticated caller can drive by presenting unknown thumbprints. The
|
|
26
|
+
* TTL-cache refresh path is NOT gated by it. */
|
|
27
|
+
syncDebounceMs?: number;
|
|
28
|
+
/** When set, `resolve` fails closed with RevocationUnevaluated for a key whose
|
|
29
|
+
* directory declares a revocation_url but no snapshot has been fetched
|
|
30
|
+
* (unreachable or not host-anchored) — i.e. revocation could not be evaluated.
|
|
31
|
+
* Default false keeps the best-effort behavior (a declared-but-unreachable
|
|
32
|
+
* revocation channel does not block resolution). Set it where a revoked key
|
|
33
|
+
* must never resolve even if the revocation channel is down. */
|
|
34
|
+
requireRevocation?: boolean;
|
|
35
|
+
now?: () => number;
|
|
36
|
+
after?: (ms: number) => Promise<void>;
|
|
37
|
+
onPollArmed?: () => void;
|
|
38
|
+
onPollCycle?: () => void;
|
|
39
|
+
fetch?: FetchLike;
|
|
40
|
+
}
|
|
41
|
+
/** The WBA key face. `resolve` returns the raw Ed25519 public key, `undefined`
|
|
42
|
+
* for an unknown/absent thumbprint (fall-through), and throws KeyRevoked /
|
|
43
|
+
* KeyExpired / DirectoryUnavailable for the distinct fail-closed verdicts. */
|
|
44
|
+
export interface WBAKeyResolver {
|
|
45
|
+
resolve(thumbprint: string, directory: string): Promise<Uint8Array | undefined>;
|
|
46
|
+
run(signal: AbortSignal): Promise<void>;
|
|
47
|
+
/** Whether `keyId` (a thumbprint) is in ANY host's fetched revocation snapshot,
|
|
48
|
+
* INDEPENDENT of WBA directory membership. `resolve` gates a key only when the
|
|
49
|
+
* directory lists it (removal is not revocation), so a key resolved from another
|
|
50
|
+
* source — e.g. a static bootstrap file — is invisible to that path; `revoked`
|
|
51
|
+
* is the fail-closed hook a composite consults to reject a broker-revoked,
|
|
52
|
+
* directory-absent thumbprint. False when no snapshot has been fetched. */
|
|
53
|
+
revoked(keyId: string): boolean;
|
|
54
|
+
}
|
|
55
|
+
/** Construct a WBA resolver with defaults applied. */
|
|
56
|
+
export declare function createWBAKeyResolver(opts?: WBAKeyResolverOptions): WBAKeyResolver;
|
|
57
|
+
/** First window-active, well-formed Ed25519 key (raw 32 bytes) from `directory`,
|
|
58
|
+
* or `null`. Selects an identity's signing key BY DOCUMENT ORDER when its thumbprint
|
|
59
|
+
* is not known ahead of time — complementing WBAKeyResolver, which matches a KNOWN
|
|
60
|
+
* thumbprint. Iterates the directory's keys in document order and returns the FIRST
|
|
61
|
+
* key that passes ALL of: window-active ([not_before, not_after) half-open covers
|
|
62
|
+
* `now` (epoch-ms), both bounds RFC 3339-parseable — a missing/unparseable bound
|
|
63
|
+
* makes the key inactive); `kty === "OKP"` and `crv === "Ed25519"` matched
|
|
64
|
+
* CASE-INSENSITIVELY (a deliberate lenient SDK convention: RFC 7517/8037 specify the
|
|
65
|
+
* exact-case "OKP" / "Ed25519", but all three SDKs accept any case IDENTICALLY so a
|
|
66
|
+
* case-varying directory resolves the SAME key everywhere); and a present `x` that
|
|
67
|
+
* base64url-decodes to exactly 32 bytes. Any key failing any check is skipped and
|
|
68
|
+
* iteration continues.
|
|
69
|
+
*
|
|
70
|
+
* The result is the first window-active key in document order — this SDK's
|
|
71
|
+
* deterministic tie-break, NOT a normative "current" key: the protocol permits
|
|
72
|
+
* several simultaneously-active keys during overlap rotation and defines no "first".
|
|
73
|
+
*
|
|
74
|
+
* `maxScan` is an OPTIONAL document-order bound. `undefined` (the default) scans the
|
|
75
|
+
* WHOLE directory — unbounded, so a valid key at any position is reachable; a silent
|
|
76
|
+
* cap would make a high-position key indistinguishable from "no active key" (a
|
|
77
|
+
* DoS-by-directory-padding footgun). A defined bound caps the scan at
|
|
78
|
+
* `Math.max(0, maxScan)` keys (0 or negative scans none); when a positive bound is
|
|
79
|
+
* exhausted while more keys remain, the exhaustion is logged. Returns `null` when no
|
|
80
|
+
* examined key qualifies (or `directory` is null/undefined). Byte-parity with the
|
|
81
|
+
* Go `ActiveEd25519Key` / Python `active_ed25519_key` oracles.
|
|
82
|
+
*
|
|
83
|
+
* REVOCATION: this selector screens ONLY validity windows and key well-formedness —
|
|
84
|
+
* it does NOT consult any revocation channel. A key that was emergency-revoked but
|
|
85
|
+
* is still window-active in a (possibly CDN-cached) directory WILL be selected. A
|
|
86
|
+
* caller on a VERIFICATION path MUST NOT trust the result until it has screened the
|
|
87
|
+
* selected key's RFC 7638 thumbprint against the resolver's revoked-thumbprint set
|
|
88
|
+
* (`WBAKeyResolver.revoked` / a revocation snapshot); otherwise adopting this
|
|
89
|
+
* selector defeats emergency revocation. Prefer {@link activeEd25519KeyScreened},
|
|
90
|
+
* which folds that screen into selection. This bare form is for non-verification
|
|
91
|
+
* callers only. */
|
|
92
|
+
export declare function activeEd25519Key(directory: WBAFile, now: number, maxScan?: number): Uint8Array | null;
|
|
93
|
+
/** Like {@link activeEd25519Key}, but ALSO returns the selected key's expiry.
|
|
94
|
+
* Runs the IDENTICAL document-order selection and returns `{ key, notAfter }` for
|
|
95
|
+
* the FIRST qualifying key — the raw 32 bytes plus the SAME `not_after` the window
|
|
96
|
+
* check parsed, as epoch-ms (the module's time convention). A downstream caller
|
|
97
|
+
* (e.g. an offer-key cache) clamps its cache TTL to `min(now + ttl, notAfter)` so
|
|
98
|
+
* a cached key never outlives its validity window. `notAfter` is guaranteed
|
|
99
|
+
* finite — selection required it (a key with a missing/unparseable bound is
|
|
100
|
+
* inactive and skipped). Returns `null` when no examined key qualifies.
|
|
101
|
+
* Byte-parity with the Go `ActiveEd25519KeyWithExpiry` / Python
|
|
102
|
+
* `active_ed25519_key_with_expiry` oracles.
|
|
103
|
+
*
|
|
104
|
+
* REVOCATION: like {@link activeEd25519Key}, this bare form does NOT consult
|
|
105
|
+
* revocation — it can return a window-active-but-revoked key. A VERIFICATION path
|
|
106
|
+
* MUST screen the result, or use the revocation-aware
|
|
107
|
+
* {@link activeEd25519KeyWithExpiryScreened} instead. */
|
|
108
|
+
export declare function activeEd25519KeyWithExpiry(directory: WBAFile, now: number, maxScan?: number): {
|
|
109
|
+
key: Uint8Array;
|
|
110
|
+
notAfter: number;
|
|
111
|
+
} | null;
|
|
112
|
+
/** {@link activeEd25519Key} made REVOCATION-AWARE. Runs the same document-order
|
|
113
|
+
* window + well-formedness selection but ALSO skips any key whose RFC 7638
|
|
114
|
+
* thumbprint `revoked` reports true, so a window-active-but-revoked key is never
|
|
115
|
+
* returned. It is the selector a VERIFICATION path adopts — folding the revoked-set
|
|
116
|
+
* screen the bare {@link activeEd25519Key} leaves to the caller into selection
|
|
117
|
+
* itself, so an emergency-revoked key still listed in a CDN-cached directory is
|
|
118
|
+
* passed over for the next active, non-revoked key. `revoked` is REQUIRED: pass a
|
|
119
|
+
* predicate over the resolver's revoked-thumbprint set (e.g. `WBAKeyResolver.revoked`)
|
|
120
|
+
* or, for a caller with no revocation channel, an explicit `() => false` to make the
|
|
121
|
+
* waiver visible. It is ASYNC because screening computes each candidate's RFC 7638
|
|
122
|
+
* thumbprint (the SAME `crypto.subtle` primitive `WBAKeyResolver.resolve` keys on).
|
|
123
|
+
* Returns `null` when no examined, non-revoked key qualifies. */
|
|
124
|
+
export declare function activeEd25519KeyScreened(directory: WBAFile, now: number, revoked: (thumbprint: string) => boolean, maxScan?: number): Promise<Uint8Array | null>;
|
|
125
|
+
/** {@link activeEd25519KeyWithExpiry} made REVOCATION-AWARE (see
|
|
126
|
+
* {@link activeEd25519KeyScreened}): the same selection, plus a skip of any key whose
|
|
127
|
+
* RFC 7638 thumbprint `revoked` reports true, returned with the selected key's
|
|
128
|
+
* `notAfter` for cache-TTL clamping. `revoked` is REQUIRED; ASYNC for the same
|
|
129
|
+
* thumbprint reason. Returns `null` when no examined, non-revoked key qualifies. */
|
|
130
|
+
export declare function activeEd25519KeyWithExpiryScreened(directory: WBAFile, now: number, revoked: (thumbprint: string) => boolean, maxScan?: number): Promise<{
|
|
131
|
+
key: Uint8Array;
|
|
132
|
+
notAfter: number;
|
|
133
|
+
} | null>;
|
|
134
|
+
export {};
|