@adcp/sdk 16.1.0 → 16.2.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/dist/lib/governance/authorization.d.mts +66 -0
- package/dist/lib/governance/authorization.d.ts +66 -0
- package/dist/lib/governance/authorization.js +181 -37
- package/dist/lib/governance/authorization.mjs +183 -36
- package/dist/lib/governance/index.d.mts +2 -2
- package/dist/lib/governance/index.d.ts +2 -2
- package/dist/lib/governance/index.js +4 -2
- package/dist/lib/governance/index.mjs +3 -1
- package/dist/lib/identity-3.3/index.d.mts +1 -0
- package/dist/lib/identity-3.3/index.d.ts +1 -0
- package/dist/lib/identity-3.3/index.js +3 -1
- package/dist/lib/identity-3.3/index.mjs +1 -0
- package/dist/lib/identity-3.3/jws-artifacts.d.mts +147 -0
- package/dist/lib/identity-3.3/jws-artifacts.d.ts +147 -0
- package/dist/lib/identity-3.3/jws-artifacts.js +258 -0
- package/dist/lib/identity-3.3/jws-artifacts.mjs +234 -0
- package/dist/lib/identity-3.3/revocation.d.mts +7 -0
- package/dist/lib/identity-3.3/revocation.d.ts +7 -0
- package/dist/lib/identity-3.3/revocation.js +13 -0
- package/dist/lib/identity-3.3/revocation.mjs +13 -0
- package/dist/lib/index.d.mts +2 -2
- package/dist/lib/index.d.ts +2 -2
- package/dist/lib/index.js +2 -0
- package/dist/lib/index.mjs +2 -0
- package/dist/lib/registry/types.generated.d.mts +24 -1
- package/dist/lib/registry/types.generated.d.ts +24 -1
- package/dist/lib/server/index.d.mts +2 -2
- package/dist/lib/server/index.d.ts +2 -2
- package/dist/lib/server/index.js +2 -0
- package/dist/lib/server/index.mjs +2 -0
- package/dist/lib/signing/web-bot-auth.js +5 -0
- package/dist/lib/signing/web-bot-auth.mjs +4 -0
- package/dist/lib/testing/storyboard/validations.d.mts +1 -1
- package/dist/lib/testing/storyboard/validations.d.ts +1 -1
- package/dist/lib/version.d.mts +3 -3
- package/dist/lib/version.d.ts +3 -3
- package/dist/lib/version.js +3 -3
- package/dist/lib/version.mjs +3 -3
- package/docs/TYPE-SUMMARY.md +2 -2
- package/docs/llms.txt +3 -3
- package/docs/migration-14.x-rc-worksheet.md +4 -4
- package/docs/migration-agent-identity-3.3.md +51 -6
- package/package.json +6 -4
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { type SsrfFetchOptions } from '../net';
|
|
2
|
+
import type { AdcpJsonWebKey } from '../signing/types';
|
|
3
|
+
import { type SignatureDirectoryResolver } from '../signing/web-bot-auth';
|
|
4
|
+
import type { OriginRevocationResolver, OriginRevocationStatus } from './revocation';
|
|
5
|
+
import type { TrustEvaluator } from './trust';
|
|
6
|
+
import type { RevocationDetail } from './types';
|
|
7
|
+
/** Which profile verifies a JWS artifact from origin `O`. */
|
|
8
|
+
export type JwsArtifactProfile = {
|
|
9
|
+
profile: 'web-bot-auth';
|
|
10
|
+
/** `O`, the canonical origin of the signing agent's URL. */
|
|
11
|
+
origin: string;
|
|
12
|
+
/** The registrable domain whose trust record lists `O`. */
|
|
13
|
+
operator: string;
|
|
14
|
+
} | {
|
|
15
|
+
profile: '3.2';
|
|
16
|
+
origin: string;
|
|
17
|
+
/**
|
|
18
|
+
* `no_trust_record`: `O` has no registrable domain (IP literal, public
|
|
19
|
+
* suffix, no PSL match), or its operator has not published a trust
|
|
20
|
+
* record (404/410 with no remembered 200). `unlisted`: the record does
|
|
21
|
+
* not list `O`.
|
|
22
|
+
*/
|
|
23
|
+
reason: 'no_trust_record' | 'unlisted';
|
|
24
|
+
};
|
|
25
|
+
export type JwsArtifactErrorCode =
|
|
26
|
+
/** A trust record, key directory or archive could not be read. Retryable. */
|
|
27
|
+
'jws_key_unavailable'
|
|
28
|
+
/** No key at `O` (directory, or archive for history) carries the `kid`. */
|
|
29
|
+
| 'jws_key_unknown'
|
|
30
|
+
/** Wrong `typ`, `alg` not `EdDSA`, `kid` not the key's thumbprint, or a signature that does not verify. */
|
|
31
|
+
| 'jws_invalid'
|
|
32
|
+
/** `O`'s revocation list revokes the `kid`. */
|
|
33
|
+
| 'jws_key_revoked'
|
|
34
|
+
/** `O`'s revocation status is unknown, or (by default) `O` serves no list. Retryable. */
|
|
35
|
+
| 'jws_revocation_unavailable';
|
|
36
|
+
export declare class JwsArtifactError extends Error {
|
|
37
|
+
readonly code: JwsArtifactErrorCode;
|
|
38
|
+
constructor(code: JwsArtifactErrorCode, message: string, options?: {
|
|
39
|
+
cause?: unknown;
|
|
40
|
+
});
|
|
41
|
+
/** True for outages: retry later, never treat as absence or as a verdict on the artifact. */
|
|
42
|
+
get transient(): boolean;
|
|
43
|
+
}
|
|
44
|
+
/** Components the WBA path reuses. */
|
|
45
|
+
export interface JwsArtifactWebBotAuthOptions {
|
|
46
|
+
/**
|
|
47
|
+
* Derives the trust record from `O` (R3a step 1) to select the profile.
|
|
48
|
+
* Configure durable `authorityHistory` on it for public, multi-replica or
|
|
49
|
+
* serverless verifiers: without it, the rule that a 404/410 after a 200 is an
|
|
50
|
+
* outage (never a return to 3.2) holds only within one process.
|
|
51
|
+
*/
|
|
52
|
+
trust: TrustEvaluator;
|
|
53
|
+
/** `O`'s Web Bot Auth key directory. */
|
|
54
|
+
directory: SignatureDirectoryResolver;
|
|
55
|
+
/** `O`'s revocation list (R3). Required: the WBA path never skips revocation. */
|
|
56
|
+
revocation: OriginRevocationResolver;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Select the profile for a JWS artifact whose signing agent URL is
|
|
60
|
+
* `signingAgentUrl` (R4). Throws {@link JwsArtifactError}
|
|
61
|
+
* `jws_key_unavailable` when the trust record cannot be read: a fetch failure
|
|
62
|
+
* is never absence and never selects the 3.2 profile. An unparseable
|
|
63
|
+
* `signingAgentUrl` is a caller error (`TypeError` from canonicalization).
|
|
64
|
+
*/
|
|
65
|
+
export declare function selectJwsArtifactProfile(signingAgentUrl: string, trust: TrustEvaluator): Promise<JwsArtifactProfile>;
|
|
66
|
+
export interface VerifyWebBotAuthJwsArtifactOptions {
|
|
67
|
+
/** The protected-header `typ` this surface accepts, e.g. `adcp-gov+jws`. Any other is refused. */
|
|
68
|
+
typ: string;
|
|
69
|
+
directory: SignatureDirectoryResolver;
|
|
70
|
+
/**
|
|
71
|
+
* `O`'s revocation list (R3). Pass `false` only when the caller checks the
|
|
72
|
+
* returned `kid` against `O`'s list itself.
|
|
73
|
+
*/
|
|
74
|
+
revocation: OriginRevocationResolver | false;
|
|
75
|
+
/**
|
|
76
|
+
* `O`'s revocation status `absent` (the origin serves no list). `reject`
|
|
77
|
+
* (default) fails closed, as a WBA identity without a list cannot be bound
|
|
78
|
+
* for spend. `accept` treats it as nothing revoked.
|
|
79
|
+
*/
|
|
80
|
+
revocationAbsent?: 'reject' | 'accept';
|
|
81
|
+
/**
|
|
82
|
+
* A key `O`'s list revokes. `reject` (default) refuses the artifact, as
|
|
83
|
+
* online verification must (R9: a revoked key is rejected whatever its
|
|
84
|
+
* detail). `report` verifies it and returns the revocation and its detail,
|
|
85
|
+
* for audit of retained artifacts.
|
|
86
|
+
*/
|
|
87
|
+
revokedKey?: 'reject' | 'report';
|
|
88
|
+
/**
|
|
89
|
+
* Historical verification: also look the `kid` up in the governance key
|
|
90
|
+
* archive at `O` when the directory does not carry it or cannot be read.
|
|
91
|
+
* Governance contexts only; only governance agents publish an archive.
|
|
92
|
+
*/
|
|
93
|
+
archive?: GovernanceKeyArchiveResolver;
|
|
94
|
+
/** Critical header parameters the surface understands (jose `crit`). */
|
|
95
|
+
crit?: Record<string, boolean>;
|
|
96
|
+
}
|
|
97
|
+
export interface VerifiedWebBotAuthJwsArtifact {
|
|
98
|
+
origin: string;
|
|
99
|
+
kid: string;
|
|
100
|
+
key: AdcpJsonWebKey;
|
|
101
|
+
/** Where the key came from: `O`'s key directory, or (history) its archive. */
|
|
102
|
+
keySource: 'directory' | 'archive';
|
|
103
|
+
protectedHeader: Record<string, unknown>;
|
|
104
|
+
payload: Uint8Array;
|
|
105
|
+
/** `O`'s revocation status for `kid`, when a resolver was supplied. Never `unknown`. */
|
|
106
|
+
revocation?: Exclude<OriginRevocationStatus, 'unknown'>;
|
|
107
|
+
/** The most severe revocation detail recorded for `kid` (R9), when it is revoked. */
|
|
108
|
+
revocationDetail?: RevocationDetail;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Verify a compact JWS with a key from `origin`'s WBA key directory (R4):
|
|
112
|
+
* `typ` as the surface requires, `alg` `EdDSA`, `kid` the key's JWK
|
|
113
|
+
* thumbprint, no `use`/`key_ops`/`adcp_use` checks, and revocation from
|
|
114
|
+
* `origin`'s list. Signature and key only: every other check of the surface
|
|
115
|
+
* (claims, audience, freshness, replay) stays with the surface. Throws
|
|
116
|
+
* {@link JwsArtifactError}.
|
|
117
|
+
*/
|
|
118
|
+
export declare function verifyWebBotAuthJwsArtifact(token: string, origin: string, options: VerifyWebBotAuthJwsArtifactOptions): Promise<VerifiedWebBotAuthJwsArtifact>;
|
|
119
|
+
export declare const GOVERNANCE_KEY_ARCHIVE_PATH = "/.well-known/jwks-archive.json";
|
|
120
|
+
export interface GovernanceKeyArchiveResolverOptions {
|
|
121
|
+
now?: () => number;
|
|
122
|
+
fetchOptions?: Pick<SsrfFetchOptions, 'lookup' | 'trustedFetchFn'>;
|
|
123
|
+
/** Seconds an archive read stays cached. Default 300. */
|
|
124
|
+
maxAgeSeconds?: number;
|
|
125
|
+
/** Origins cached at once. Default 256. */
|
|
126
|
+
maxOrigins?: number;
|
|
127
|
+
/** Published fixture keys are read only outside production, as for the key directory. */
|
|
128
|
+
allowTestKeys?: boolean;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The governance key archive at an origin (`security.mdx` §Revocation):
|
|
132
|
+
* `{origin}/.well-known/jwks-archive.json`, a JWKS of retired governance keys
|
|
133
|
+
* kept discoverable for audit. On the WBA path only Ed25519 keys whose `kid`
|
|
134
|
+
* (when present) is their JWK thumbprint are read; other keys are ignored. A
|
|
135
|
+
* 404/410 is an origin with no archive. Any other failure is
|
|
136
|
+
* `jws_key_unavailable`, never absence, and is retried after a cooldown.
|
|
137
|
+
*/
|
|
138
|
+
export declare class GovernanceKeyArchiveResolver {
|
|
139
|
+
private readonly options;
|
|
140
|
+
private readonly cache;
|
|
141
|
+
private readonly failures;
|
|
142
|
+
private readonly pending;
|
|
143
|
+
private readonly maxOrigins;
|
|
144
|
+
constructor(options?: GovernanceKeyArchiveResolverOptions);
|
|
145
|
+
resolve(origin: string, kid: string): Promise<AdcpJsonWebKey | null>;
|
|
146
|
+
private fetch;
|
|
147
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var jws_artifacts_exports = {};
|
|
20
|
+
__export(jws_artifacts_exports, {
|
|
21
|
+
GOVERNANCE_KEY_ARCHIVE_PATH: () => GOVERNANCE_KEY_ARCHIVE_PATH,
|
|
22
|
+
GovernanceKeyArchiveResolver: () => GovernanceKeyArchiveResolver,
|
|
23
|
+
JwsArtifactError: () => JwsArtifactError,
|
|
24
|
+
selectJwsArtifactProfile: () => selectJwsArtifactProfile,
|
|
25
|
+
verifyWebBotAuthJwsArtifact: () => verifyWebBotAuthJwsArtifact
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(jws_artifacts_exports);
|
|
28
|
+
var import_jose = require("jose");
|
|
29
|
+
var import_net = require('../net/index.js');
|
|
30
|
+
var import_canonicalize = require('../signing/agent-resolver/canonicalize.js');
|
|
31
|
+
var import_etld = require('../signing/agent-resolver/etld.js');
|
|
32
|
+
var import_strict_json = require('../signing/agent-resolver/strict-json.js');
|
|
33
|
+
var import_web_bot_auth = require('../signing/web-bot-auth.js');
|
|
34
|
+
class JwsArtifactError extends Error {
|
|
35
|
+
constructor(code, message, options) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.code = code;
|
|
38
|
+
this.name = "JwsArtifactError";
|
|
39
|
+
if (options?.cause !== void 0) Object.defineProperty(this, "cause", { value: options.cause });
|
|
40
|
+
}
|
|
41
|
+
code;
|
|
42
|
+
/** True for outages: retry later, never treat as absence or as a verdict on the artifact. */
|
|
43
|
+
get transient() {
|
|
44
|
+
return this.code === "jws_key_unavailable" || this.code === "jws_revocation_unavailable";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function selectJwsArtifactProfile(signingAgentUrl, trust) {
|
|
48
|
+
const origin = (0, import_canonicalize.canonicalAgentOrigin)(signingAgentUrl);
|
|
49
|
+
try {
|
|
50
|
+
(0, import_etld.eTldPlusOne)(origin);
|
|
51
|
+
} catch {
|
|
52
|
+
return { profile: "3.2", origin, reason: "no_trust_record" };
|
|
53
|
+
}
|
|
54
|
+
let record;
|
|
55
|
+
try {
|
|
56
|
+
record = await trust.get(origin);
|
|
57
|
+
} catch (cause) {
|
|
58
|
+
throw new JwsArtifactError("jws_key_unavailable", "Issuer trust record is unavailable", { cause });
|
|
59
|
+
}
|
|
60
|
+
if (record.status !== "present") return { profile: "3.2", origin, reason: "no_trust_record" };
|
|
61
|
+
const listed = (record.record.agents ?? []).some((agent) => {
|
|
62
|
+
try {
|
|
63
|
+
return (0, import_canonicalize.canonicalAgentOrigin)(agent.url) === origin;
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
return listed ? { profile: "web-bot-auth", origin, operator: record.operator } : { profile: "3.2", origin, reason: "unlisted" };
|
|
69
|
+
}
|
|
70
|
+
async function verifyWebBotAuthJwsArtifact(token, origin, options) {
|
|
71
|
+
if (typeof options.typ !== "string" || !options.typ)
|
|
72
|
+
throw new TypeError("verifyWebBotAuthJwsArtifact requires the typ the surface accepts");
|
|
73
|
+
if (options.revocation === void 0)
|
|
74
|
+
throw new TypeError("verifyWebBotAuthJwsArtifact requires a revocation resolver, or revocation: false");
|
|
75
|
+
let header;
|
|
76
|
+
try {
|
|
77
|
+
header = (0, import_jose.decodeProtectedHeader)(token);
|
|
78
|
+
} catch {
|
|
79
|
+
throw new JwsArtifactError("jws_invalid", "Malformed JWS protected header");
|
|
80
|
+
}
|
|
81
|
+
if (header.typ !== options.typ)
|
|
82
|
+
throw new JwsArtifactError("jws_invalid", "JWS typ is not the one this surface accepts");
|
|
83
|
+
if (header.alg !== "EdDSA") throw new JwsArtifactError("jws_invalid", "A Web Bot Auth JWS artifact uses alg EdDSA");
|
|
84
|
+
const kid = header.kid;
|
|
85
|
+
if (typeof kid !== "string" || kid.length === 0 || kid.length > 256)
|
|
86
|
+
throw new JwsArtifactError("jws_key_unknown", "JWS kid is missing");
|
|
87
|
+
try {
|
|
88
|
+
origin = (0, import_web_bot_auth.receivedAgentOrigin)((0, import_canonicalize.canonicalAgentOrigin)(origin));
|
|
89
|
+
} catch {
|
|
90
|
+
throw new JwsArtifactError("jws_key_unknown", "Signing origin is not an https origin");
|
|
91
|
+
}
|
|
92
|
+
let key = null;
|
|
93
|
+
let keySource = "directory";
|
|
94
|
+
let directoryFailure;
|
|
95
|
+
try {
|
|
96
|
+
key = await options.directory.resolve(kid, origin);
|
|
97
|
+
} catch (cause) {
|
|
98
|
+
directoryFailure = cause;
|
|
99
|
+
}
|
|
100
|
+
if (!key && options.archive) {
|
|
101
|
+
const archived = await options.archive.resolve(origin, kid);
|
|
102
|
+
if (archived) {
|
|
103
|
+
key = archived;
|
|
104
|
+
keySource = "archive";
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (!key) {
|
|
108
|
+
if (directoryFailure !== void 0)
|
|
109
|
+
throw new JwsArtifactError("jws_key_unavailable", "Issuer key directory is unavailable", {
|
|
110
|
+
cause: directoryFailure
|
|
111
|
+
});
|
|
112
|
+
throw new JwsArtifactError("jws_key_unknown", "No key at the issuer origin carries this kid");
|
|
113
|
+
}
|
|
114
|
+
if (key.kty !== "OKP" || key.crv !== "Ed25519" || typeof key.x !== "string")
|
|
115
|
+
throw new JwsArtifactError("jws_invalid", "A Web Bot Auth JWS artifact is signed with an Ed25519 key");
|
|
116
|
+
let thumbprint;
|
|
117
|
+
try {
|
|
118
|
+
thumbprint = (0, import_web_bot_auth.jwkThumbprint)(key);
|
|
119
|
+
} catch {
|
|
120
|
+
throw new JwsArtifactError("jws_invalid", "Key thumbprint cannot be computed");
|
|
121
|
+
}
|
|
122
|
+
if (kid !== thumbprint) throw new JwsArtifactError("jws_invalid", "JWS kid is not the key JWK thumbprint");
|
|
123
|
+
let verified;
|
|
124
|
+
try {
|
|
125
|
+
const publicKey = { kty: "OKP", crv: "Ed25519", x: key.x };
|
|
126
|
+
verified = await (0, import_jose.compactVerify)(token, await (0, import_jose.importJWK)(publicKey, "EdDSA"), {
|
|
127
|
+
algorithms: ["EdDSA"],
|
|
128
|
+
...options.crit ? { crit: options.crit } : {}
|
|
129
|
+
});
|
|
130
|
+
} catch {
|
|
131
|
+
throw new JwsArtifactError("jws_invalid", "JWS signature verification failed");
|
|
132
|
+
}
|
|
133
|
+
const result = {
|
|
134
|
+
origin,
|
|
135
|
+
kid,
|
|
136
|
+
key,
|
|
137
|
+
keySource,
|
|
138
|
+
protectedHeader: verified.protectedHeader,
|
|
139
|
+
payload: verified.payload
|
|
140
|
+
};
|
|
141
|
+
if (options.revocation) {
|
|
142
|
+
let status;
|
|
143
|
+
try {
|
|
144
|
+
status = await options.revocation.status(origin, kid);
|
|
145
|
+
} catch (cause) {
|
|
146
|
+
throw new JwsArtifactError("jws_revocation_unavailable", "Issuer revocation status could not be established", {
|
|
147
|
+
cause
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
if (status === "unknown" || status === "absent" && (options.revocationAbsent ?? "reject") === "reject")
|
|
151
|
+
throw new JwsArtifactError("jws_revocation_unavailable", "Issuer revocation status could not be established");
|
|
152
|
+
if (status === "revoked") {
|
|
153
|
+
if ((options.revokedKey ?? "reject") === "reject")
|
|
154
|
+
throw new JwsArtifactError("jws_key_revoked", "Signing key is revoked by its origin");
|
|
155
|
+
try {
|
|
156
|
+
const detail = await options.revocation.detail(origin, kid);
|
|
157
|
+
if (detail) result.revocationDetail = detail;
|
|
158
|
+
} catch (cause) {
|
|
159
|
+
throw new JwsArtifactError("jws_revocation_unavailable", "Issuer revocation detail could not be read", {
|
|
160
|
+
cause
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
result.revocation = status;
|
|
165
|
+
}
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
const GOVERNANCE_KEY_ARCHIVE_PATH = "/.well-known/jwks-archive.json";
|
|
169
|
+
const ARCHIVE_FAILURE_COOLDOWN_SECONDS = 60;
|
|
170
|
+
class GovernanceKeyArchiveResolver {
|
|
171
|
+
constructor(options = {}) {
|
|
172
|
+
this.options = options;
|
|
173
|
+
const maxAge = options.maxAgeSeconds ?? 300;
|
|
174
|
+
if (!Number.isFinite(maxAge) || maxAge < 0)
|
|
175
|
+
throw new TypeError("maxAgeSeconds must be a finite, non-negative number");
|
|
176
|
+
this.maxOrigins = options.maxOrigins ?? 256;
|
|
177
|
+
if (!Number.isSafeInteger(this.maxOrigins) || this.maxOrigins < 1)
|
|
178
|
+
throw new TypeError("maxOrigins must be a positive safe integer");
|
|
179
|
+
}
|
|
180
|
+
options;
|
|
181
|
+
cache = /* @__PURE__ */ new Map();
|
|
182
|
+
failures = /* @__PURE__ */ new Map();
|
|
183
|
+
pending = /* @__PURE__ */ new Map();
|
|
184
|
+
maxOrigins;
|
|
185
|
+
async resolve(origin, kid) {
|
|
186
|
+
origin = (0, import_web_bot_auth.receivedAgentOrigin)((0, import_canonicalize.canonicalAgentOrigin)(origin));
|
|
187
|
+
const now = (this.options.now ?? (() => Date.now() / 1e3))();
|
|
188
|
+
const cached = this.cache.get(origin);
|
|
189
|
+
let keys;
|
|
190
|
+
if (cached && cached.freshUntil > now) keys = cached.keys;
|
|
191
|
+
else {
|
|
192
|
+
const failure = this.failures.get(origin);
|
|
193
|
+
if (failure && failure.until > now)
|
|
194
|
+
throw new JwsArtifactError("jws_key_unavailable", "Governance key archive is unavailable", {
|
|
195
|
+
cause: failure.cause
|
|
196
|
+
});
|
|
197
|
+
let pending = this.pending.get(origin);
|
|
198
|
+
if (!pending) {
|
|
199
|
+
pending = this.fetch(origin, now);
|
|
200
|
+
this.pending.set(origin, pending);
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
keys = await pending;
|
|
204
|
+
} finally {
|
|
205
|
+
if (this.pending.get(origin) === pending) this.pending.delete(origin);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return keys.find((key) => key.kid === kid) ?? null;
|
|
209
|
+
}
|
|
210
|
+
async fetch(origin, now) {
|
|
211
|
+
let keys;
|
|
212
|
+
try {
|
|
213
|
+
const response = await (0, import_net.ssrfSafeFetch)(origin + GOVERNANCE_KEY_ARCHIVE_PATH, {
|
|
214
|
+
...this.options.fetchOptions,
|
|
215
|
+
headers: { accept: "application/json" },
|
|
216
|
+
timeoutMs: 1e4,
|
|
217
|
+
maxBodyBytes: 1024 * 1024
|
|
218
|
+
});
|
|
219
|
+
if (response.status === 404 || response.status === 410) keys = [];
|
|
220
|
+
else {
|
|
221
|
+
if (response.status !== 200) throw new Error(`Archive answered ${response.status}`);
|
|
222
|
+
const document = (0, import_strict_json.parseStrictJson)(new TextDecoder("utf-8", { fatal: true }).decode(response.body), 32);
|
|
223
|
+
if (!Array.isArray(document.keys) || document.keys.length > 4096) throw new Error("Invalid key archive");
|
|
224
|
+
keys = [];
|
|
225
|
+
for (const raw of document.keys) {
|
|
226
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
|
|
227
|
+
const jwk = raw;
|
|
228
|
+
if (Object.keys(jwk).some((name) => PRIVATE_JWK_MEMBERS.has(name.toLowerCase()) || name.startsWith("_private")))
|
|
229
|
+
throw new Error("Key archive publishes private key material");
|
|
230
|
+
if (jwk.kty !== "OKP" || jwk.crv !== "Ed25519" || typeof jwk.x !== "string") continue;
|
|
231
|
+
const thumbprint = (0, import_web_bot_auth.jwkThumbprint)(jwk);
|
|
232
|
+
if (jwk.kid !== void 0 && jwk.kid !== thumbprint) continue;
|
|
233
|
+
if ((0, import_web_bot_auth.isRefusedWebBotAuthTestKey)(thumbprint, this.options.allowTestKeys)) continue;
|
|
234
|
+
keys.push({ kty: "OKP", crv: "Ed25519", x: jwk.x, kid: thumbprint });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
} catch (cause) {
|
|
238
|
+
if (this.failures.size >= this.maxOrigins && !this.failures.has(origin))
|
|
239
|
+
this.failures.delete(this.failures.keys().next().value);
|
|
240
|
+
this.failures.set(origin, { until: now + ARCHIVE_FAILURE_COOLDOWN_SECONDS, cause });
|
|
241
|
+
throw new JwsArtifactError("jws_key_unavailable", "Governance key archive is unavailable", { cause });
|
|
242
|
+
}
|
|
243
|
+
this.failures.delete(origin);
|
|
244
|
+
if (this.cache.size >= this.maxOrigins && !this.cache.has(origin))
|
|
245
|
+
this.cache.delete(this.cache.keys().next().value);
|
|
246
|
+
this.cache.set(origin, { keys, freshUntil: now + (this.options.maxAgeSeconds ?? 300) });
|
|
247
|
+
return keys;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const PRIVATE_JWK_MEMBERS = /* @__PURE__ */ new Set(["d", "p", "q", "dp", "dq", "qi", "oth", "k"]);
|
|
251
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
252
|
+
0 && (module.exports = {
|
|
253
|
+
GOVERNANCE_KEY_ARCHIVE_PATH,
|
|
254
|
+
GovernanceKeyArchiveResolver,
|
|
255
|
+
JwsArtifactError,
|
|
256
|
+
selectJwsArtifactProfile,
|
|
257
|
+
verifyWebBotAuthJwsArtifact
|
|
258
|
+
});
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { compactVerify, decodeProtectedHeader, importJWK } from "jose";
|
|
2
|
+
import { ssrfSafeFetch } from "../net/index.mjs";
|
|
3
|
+
import { canonicalAgentOrigin } from "../signing/agent-resolver/canonicalize.mjs";
|
|
4
|
+
import { eTldPlusOne } from "../signing/agent-resolver/etld.mjs";
|
|
5
|
+
import { parseStrictJson } from "../signing/agent-resolver/strict-json.mjs";
|
|
6
|
+
import {
|
|
7
|
+
isRefusedWebBotAuthTestKey,
|
|
8
|
+
jwkThumbprint,
|
|
9
|
+
receivedAgentOrigin
|
|
10
|
+
} from "../signing/web-bot-auth.mjs";
|
|
11
|
+
class JwsArtifactError extends Error {
|
|
12
|
+
constructor(code, message, options) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.name = "JwsArtifactError";
|
|
16
|
+
if (options?.cause !== void 0) Object.defineProperty(this, "cause", { value: options.cause });
|
|
17
|
+
}
|
|
18
|
+
code;
|
|
19
|
+
/** True for outages: retry later, never treat as absence or as a verdict on the artifact. */
|
|
20
|
+
get transient() {
|
|
21
|
+
return this.code === "jws_key_unavailable" || this.code === "jws_revocation_unavailable";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function selectJwsArtifactProfile(signingAgentUrl, trust) {
|
|
25
|
+
const origin = canonicalAgentOrigin(signingAgentUrl);
|
|
26
|
+
try {
|
|
27
|
+
eTldPlusOne(origin);
|
|
28
|
+
} catch {
|
|
29
|
+
return { profile: "3.2", origin, reason: "no_trust_record" };
|
|
30
|
+
}
|
|
31
|
+
let record;
|
|
32
|
+
try {
|
|
33
|
+
record = await trust.get(origin);
|
|
34
|
+
} catch (cause) {
|
|
35
|
+
throw new JwsArtifactError("jws_key_unavailable", "Issuer trust record is unavailable", { cause });
|
|
36
|
+
}
|
|
37
|
+
if (record.status !== "present") return { profile: "3.2", origin, reason: "no_trust_record" };
|
|
38
|
+
const listed = (record.record.agents ?? []).some((agent) => {
|
|
39
|
+
try {
|
|
40
|
+
return canonicalAgentOrigin(agent.url) === origin;
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
return listed ? { profile: "web-bot-auth", origin, operator: record.operator } : { profile: "3.2", origin, reason: "unlisted" };
|
|
46
|
+
}
|
|
47
|
+
async function verifyWebBotAuthJwsArtifact(token, origin, options) {
|
|
48
|
+
if (typeof options.typ !== "string" || !options.typ)
|
|
49
|
+
throw new TypeError("verifyWebBotAuthJwsArtifact requires the typ the surface accepts");
|
|
50
|
+
if (options.revocation === void 0)
|
|
51
|
+
throw new TypeError("verifyWebBotAuthJwsArtifact requires a revocation resolver, or revocation: false");
|
|
52
|
+
let header;
|
|
53
|
+
try {
|
|
54
|
+
header = decodeProtectedHeader(token);
|
|
55
|
+
} catch {
|
|
56
|
+
throw new JwsArtifactError("jws_invalid", "Malformed JWS protected header");
|
|
57
|
+
}
|
|
58
|
+
if (header.typ !== options.typ)
|
|
59
|
+
throw new JwsArtifactError("jws_invalid", "JWS typ is not the one this surface accepts");
|
|
60
|
+
if (header.alg !== "EdDSA") throw new JwsArtifactError("jws_invalid", "A Web Bot Auth JWS artifact uses alg EdDSA");
|
|
61
|
+
const kid = header.kid;
|
|
62
|
+
if (typeof kid !== "string" || kid.length === 0 || kid.length > 256)
|
|
63
|
+
throw new JwsArtifactError("jws_key_unknown", "JWS kid is missing");
|
|
64
|
+
try {
|
|
65
|
+
origin = receivedAgentOrigin(canonicalAgentOrigin(origin));
|
|
66
|
+
} catch {
|
|
67
|
+
throw new JwsArtifactError("jws_key_unknown", "Signing origin is not an https origin");
|
|
68
|
+
}
|
|
69
|
+
let key = null;
|
|
70
|
+
let keySource = "directory";
|
|
71
|
+
let directoryFailure;
|
|
72
|
+
try {
|
|
73
|
+
key = await options.directory.resolve(kid, origin);
|
|
74
|
+
} catch (cause) {
|
|
75
|
+
directoryFailure = cause;
|
|
76
|
+
}
|
|
77
|
+
if (!key && options.archive) {
|
|
78
|
+
const archived = await options.archive.resolve(origin, kid);
|
|
79
|
+
if (archived) {
|
|
80
|
+
key = archived;
|
|
81
|
+
keySource = "archive";
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (!key) {
|
|
85
|
+
if (directoryFailure !== void 0)
|
|
86
|
+
throw new JwsArtifactError("jws_key_unavailable", "Issuer key directory is unavailable", {
|
|
87
|
+
cause: directoryFailure
|
|
88
|
+
});
|
|
89
|
+
throw new JwsArtifactError("jws_key_unknown", "No key at the issuer origin carries this kid");
|
|
90
|
+
}
|
|
91
|
+
if (key.kty !== "OKP" || key.crv !== "Ed25519" || typeof key.x !== "string")
|
|
92
|
+
throw new JwsArtifactError("jws_invalid", "A Web Bot Auth JWS artifact is signed with an Ed25519 key");
|
|
93
|
+
let thumbprint;
|
|
94
|
+
try {
|
|
95
|
+
thumbprint = jwkThumbprint(key);
|
|
96
|
+
} catch {
|
|
97
|
+
throw new JwsArtifactError("jws_invalid", "Key thumbprint cannot be computed");
|
|
98
|
+
}
|
|
99
|
+
if (kid !== thumbprint) throw new JwsArtifactError("jws_invalid", "JWS kid is not the key JWK thumbprint");
|
|
100
|
+
let verified;
|
|
101
|
+
try {
|
|
102
|
+
const publicKey = { kty: "OKP", crv: "Ed25519", x: key.x };
|
|
103
|
+
verified = await compactVerify(token, await importJWK(publicKey, "EdDSA"), {
|
|
104
|
+
algorithms: ["EdDSA"],
|
|
105
|
+
...options.crit ? { crit: options.crit } : {}
|
|
106
|
+
});
|
|
107
|
+
} catch {
|
|
108
|
+
throw new JwsArtifactError("jws_invalid", "JWS signature verification failed");
|
|
109
|
+
}
|
|
110
|
+
const result = {
|
|
111
|
+
origin,
|
|
112
|
+
kid,
|
|
113
|
+
key,
|
|
114
|
+
keySource,
|
|
115
|
+
protectedHeader: verified.protectedHeader,
|
|
116
|
+
payload: verified.payload
|
|
117
|
+
};
|
|
118
|
+
if (options.revocation) {
|
|
119
|
+
let status;
|
|
120
|
+
try {
|
|
121
|
+
status = await options.revocation.status(origin, kid);
|
|
122
|
+
} catch (cause) {
|
|
123
|
+
throw new JwsArtifactError("jws_revocation_unavailable", "Issuer revocation status could not be established", {
|
|
124
|
+
cause
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (status === "unknown" || status === "absent" && (options.revocationAbsent ?? "reject") === "reject")
|
|
128
|
+
throw new JwsArtifactError("jws_revocation_unavailable", "Issuer revocation status could not be established");
|
|
129
|
+
if (status === "revoked") {
|
|
130
|
+
if ((options.revokedKey ?? "reject") === "reject")
|
|
131
|
+
throw new JwsArtifactError("jws_key_revoked", "Signing key is revoked by its origin");
|
|
132
|
+
try {
|
|
133
|
+
const detail = await options.revocation.detail(origin, kid);
|
|
134
|
+
if (detail) result.revocationDetail = detail;
|
|
135
|
+
} catch (cause) {
|
|
136
|
+
throw new JwsArtifactError("jws_revocation_unavailable", "Issuer revocation detail could not be read", {
|
|
137
|
+
cause
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
result.revocation = status;
|
|
142
|
+
}
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
const GOVERNANCE_KEY_ARCHIVE_PATH = "/.well-known/jwks-archive.json";
|
|
146
|
+
const ARCHIVE_FAILURE_COOLDOWN_SECONDS = 60;
|
|
147
|
+
class GovernanceKeyArchiveResolver {
|
|
148
|
+
constructor(options = {}) {
|
|
149
|
+
this.options = options;
|
|
150
|
+
const maxAge = options.maxAgeSeconds ?? 300;
|
|
151
|
+
if (!Number.isFinite(maxAge) || maxAge < 0)
|
|
152
|
+
throw new TypeError("maxAgeSeconds must be a finite, non-negative number");
|
|
153
|
+
this.maxOrigins = options.maxOrigins ?? 256;
|
|
154
|
+
if (!Number.isSafeInteger(this.maxOrigins) || this.maxOrigins < 1)
|
|
155
|
+
throw new TypeError("maxOrigins must be a positive safe integer");
|
|
156
|
+
}
|
|
157
|
+
options;
|
|
158
|
+
cache = /* @__PURE__ */ new Map();
|
|
159
|
+
failures = /* @__PURE__ */ new Map();
|
|
160
|
+
pending = /* @__PURE__ */ new Map();
|
|
161
|
+
maxOrigins;
|
|
162
|
+
async resolve(origin, kid) {
|
|
163
|
+
origin = receivedAgentOrigin(canonicalAgentOrigin(origin));
|
|
164
|
+
const now = (this.options.now ?? (() => Date.now() / 1e3))();
|
|
165
|
+
const cached = this.cache.get(origin);
|
|
166
|
+
let keys;
|
|
167
|
+
if (cached && cached.freshUntil > now) keys = cached.keys;
|
|
168
|
+
else {
|
|
169
|
+
const failure = this.failures.get(origin);
|
|
170
|
+
if (failure && failure.until > now)
|
|
171
|
+
throw new JwsArtifactError("jws_key_unavailable", "Governance key archive is unavailable", {
|
|
172
|
+
cause: failure.cause
|
|
173
|
+
});
|
|
174
|
+
let pending = this.pending.get(origin);
|
|
175
|
+
if (!pending) {
|
|
176
|
+
pending = this.fetch(origin, now);
|
|
177
|
+
this.pending.set(origin, pending);
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
keys = await pending;
|
|
181
|
+
} finally {
|
|
182
|
+
if (this.pending.get(origin) === pending) this.pending.delete(origin);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return keys.find((key) => key.kid === kid) ?? null;
|
|
186
|
+
}
|
|
187
|
+
async fetch(origin, now) {
|
|
188
|
+
let keys;
|
|
189
|
+
try {
|
|
190
|
+
const response = await ssrfSafeFetch(origin + GOVERNANCE_KEY_ARCHIVE_PATH, {
|
|
191
|
+
...this.options.fetchOptions,
|
|
192
|
+
headers: { accept: "application/json" },
|
|
193
|
+
timeoutMs: 1e4,
|
|
194
|
+
maxBodyBytes: 1024 * 1024
|
|
195
|
+
});
|
|
196
|
+
if (response.status === 404 || response.status === 410) keys = [];
|
|
197
|
+
else {
|
|
198
|
+
if (response.status !== 200) throw new Error(`Archive answered ${response.status}`);
|
|
199
|
+
const document = parseStrictJson(new TextDecoder("utf-8", { fatal: true }).decode(response.body), 32);
|
|
200
|
+
if (!Array.isArray(document.keys) || document.keys.length > 4096) throw new Error("Invalid key archive");
|
|
201
|
+
keys = [];
|
|
202
|
+
for (const raw of document.keys) {
|
|
203
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
|
|
204
|
+
const jwk = raw;
|
|
205
|
+
if (Object.keys(jwk).some((name) => PRIVATE_JWK_MEMBERS.has(name.toLowerCase()) || name.startsWith("_private")))
|
|
206
|
+
throw new Error("Key archive publishes private key material");
|
|
207
|
+
if (jwk.kty !== "OKP" || jwk.crv !== "Ed25519" || typeof jwk.x !== "string") continue;
|
|
208
|
+
const thumbprint = jwkThumbprint(jwk);
|
|
209
|
+
if (jwk.kid !== void 0 && jwk.kid !== thumbprint) continue;
|
|
210
|
+
if (isRefusedWebBotAuthTestKey(thumbprint, this.options.allowTestKeys)) continue;
|
|
211
|
+
keys.push({ kty: "OKP", crv: "Ed25519", x: jwk.x, kid: thumbprint });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
} catch (cause) {
|
|
215
|
+
if (this.failures.size >= this.maxOrigins && !this.failures.has(origin))
|
|
216
|
+
this.failures.delete(this.failures.keys().next().value);
|
|
217
|
+
this.failures.set(origin, { until: now + ARCHIVE_FAILURE_COOLDOWN_SECONDS, cause });
|
|
218
|
+
throw new JwsArtifactError("jws_key_unavailable", "Governance key archive is unavailable", { cause });
|
|
219
|
+
}
|
|
220
|
+
this.failures.delete(origin);
|
|
221
|
+
if (this.cache.size >= this.maxOrigins && !this.cache.has(origin))
|
|
222
|
+
this.cache.delete(this.cache.keys().next().value);
|
|
223
|
+
this.cache.set(origin, { keys, freshUntil: now + (this.options.maxAgeSeconds ?? 300) });
|
|
224
|
+
return keys;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const PRIVATE_JWK_MEMBERS = /* @__PURE__ */ new Set(["d", "p", "q", "dp", "dq", "qi", "oth", "k"]);
|
|
228
|
+
export {
|
|
229
|
+
GOVERNANCE_KEY_ARCHIVE_PATH,
|
|
230
|
+
GovernanceKeyArchiveResolver,
|
|
231
|
+
JwsArtifactError,
|
|
232
|
+
selectJwsArtifactProfile,
|
|
233
|
+
verifyWebBotAuthJwsArtifact
|
|
234
|
+
};
|
|
@@ -78,6 +78,13 @@ export declare class OriginRevocationResolver {
|
|
|
78
78
|
*/
|
|
79
79
|
status(originUrl: string, keyid: string): Promise<OriginRevocationStatus>;
|
|
80
80
|
private currentStatus;
|
|
81
|
+
/**
|
|
82
|
+
* The most severe detail stored for a revoked key (R9: `keyCompromise` and
|
|
83
|
+
* the earliest `invalidity_date` win), or undefined when the key is not
|
|
84
|
+
* revoked. Reads the history store only; call {@link status} first to refresh.
|
|
85
|
+
* Throws {@link WebBotAuthUnavailableError} when the store fails.
|
|
86
|
+
*/
|
|
87
|
+
detail(originUrl: string, keyid: string): Promise<RevocationDetail | undefined>;
|
|
81
88
|
private readStored;
|
|
82
89
|
private refresh;
|
|
83
90
|
private now;
|
|
@@ -78,6 +78,13 @@ export declare class OriginRevocationResolver {
|
|
|
78
78
|
*/
|
|
79
79
|
status(originUrl: string, keyid: string): Promise<OriginRevocationStatus>;
|
|
80
80
|
private currentStatus;
|
|
81
|
+
/**
|
|
82
|
+
* The most severe detail stored for a revoked key (R9: `keyCompromise` and
|
|
83
|
+
* the earliest `invalidity_date` win), or undefined when the key is not
|
|
84
|
+
* revoked. Reads the history store only; call {@link status} first to refresh.
|
|
85
|
+
* Throws {@link WebBotAuthUnavailableError} when the store fails.
|
|
86
|
+
*/
|
|
87
|
+
detail(originUrl: string, keyid: string): Promise<RevocationDetail | undefined>;
|
|
81
88
|
private readStored;
|
|
82
89
|
private refresh;
|
|
83
90
|
private now;
|