wb-auth 0.2.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/dist/index.cjs +81 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +81 -33
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.cjs
CHANGED
|
@@ -33,10 +33,10 @@ module.exports = __toCommonJS(index_exports);
|
|
|
33
33
|
var import_web_bot_auth = require("web-bot-auth");
|
|
34
34
|
var DEFAULT_LABEL = "sig1";
|
|
35
35
|
var DEFAULT_EXPIRES_SECONDS = 60;
|
|
36
|
-
var
|
|
37
|
-
function componentsFor(
|
|
36
|
+
var MAX_EXPIRES_SECONDS = 3600;
|
|
37
|
+
function componentsFor(_method, hasBody) {
|
|
38
38
|
const base = ["@authority", "signature-agent"];
|
|
39
|
-
if (hasBody
|
|
39
|
+
if (hasBody) {
|
|
40
40
|
base.push("content-digest");
|
|
41
41
|
}
|
|
42
42
|
return base;
|
|
@@ -58,9 +58,9 @@ async function sign(request, identity, opts = {}) {
|
|
|
58
58
|
);
|
|
59
59
|
}
|
|
60
60
|
const expiresAfterSeconds = opts.expiresAfterSeconds ?? DEFAULT_EXPIRES_SECONDS;
|
|
61
|
-
if (!Number.isFinite(expiresAfterSeconds) || expiresAfterSeconds <= 0) {
|
|
61
|
+
if (typeof expiresAfterSeconds !== "number" || !Number.isFinite(expiresAfterSeconds) || expiresAfterSeconds <= 0 || expiresAfterSeconds > MAX_EXPIRES_SECONDS) {
|
|
62
62
|
throw new RangeError(
|
|
63
|
-
`expiresAfterSeconds must be a
|
|
63
|
+
`expiresAfterSeconds must be a number in (0, ${MAX_EXPIRES_SECONDS}], got: ${expiresAfterSeconds}`
|
|
64
64
|
);
|
|
65
65
|
}
|
|
66
66
|
removeHeaders(request.headers, RESIGN_HEADERS);
|
|
@@ -79,7 +79,10 @@ async function sign(request, identity, opts = {}) {
|
|
|
79
79
|
const headers = new Headers();
|
|
80
80
|
for (const [k, v] of Object.entries(request.headers)) headers.set(k, v);
|
|
81
81
|
const init = { method: request.method, headers };
|
|
82
|
-
|
|
82
|
+
const m = request.method.toUpperCase();
|
|
83
|
+
if (hasBody && m !== "GET" && m !== "HEAD") {
|
|
84
|
+
init.body = request.body;
|
|
85
|
+
}
|
|
83
86
|
const req = new Request(request.url, init);
|
|
84
87
|
const params = {
|
|
85
88
|
created,
|
|
@@ -100,7 +103,6 @@ async function sign(request, identity, opts = {}) {
|
|
|
100
103
|
|
|
101
104
|
// src/adapters/_utils.ts
|
|
102
105
|
var import_node_crypto = require("crypto");
|
|
103
|
-
var DIGEST_METHODS2 = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
|
|
104
106
|
var LIBRARY_DEFAULT_UA = /^(node|undici|node-fetch)(\/|$)/i;
|
|
105
107
|
function findHeader(headers, name) {
|
|
106
108
|
const n = name.toLowerCase();
|
|
@@ -108,7 +110,7 @@ function findHeader(headers, name) {
|
|
|
108
110
|
}
|
|
109
111
|
function ensureContentDigest(method, headers, body) {
|
|
110
112
|
if (!body || body.byteLength === 0) return;
|
|
111
|
-
|
|
113
|
+
void method;
|
|
112
114
|
if (findHeader(headers, "content-digest")) return;
|
|
113
115
|
const b64 = (0, import_node_crypto.createHash)("sha256").update(body).digest("base64");
|
|
114
116
|
headers["Content-Digest"] = `sha-256=:${b64}:`;
|
|
@@ -138,41 +140,87 @@ function hostMatches(hostname, allow) {
|
|
|
138
140
|
}
|
|
139
141
|
|
|
140
142
|
// src/adapters/fetch.ts
|
|
143
|
+
var REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
144
|
+
var CROSS_ORIGIN_STRIP = /* @__PURE__ */ new Set([
|
|
145
|
+
"authorization",
|
|
146
|
+
"cookie",
|
|
147
|
+
"proxy-authorization"
|
|
148
|
+
]);
|
|
149
|
+
var BODY_HEADERS = /* @__PURE__ */ new Set([
|
|
150
|
+
"content-type",
|
|
151
|
+
"content-length",
|
|
152
|
+
"content-encoding",
|
|
153
|
+
"content-language",
|
|
154
|
+
"content-location",
|
|
155
|
+
"content-digest"
|
|
156
|
+
]);
|
|
157
|
+
function dropHeaders(headers, names) {
|
|
158
|
+
for (const k of Object.keys(headers)) {
|
|
159
|
+
if (names.has(k.toLowerCase())) delete headers[k];
|
|
160
|
+
}
|
|
161
|
+
}
|
|
141
162
|
function createSignedFetch(identity, options = {}) {
|
|
142
163
|
return async function signedFetch(input, init = {}) {
|
|
143
164
|
const req = new Request(input, init);
|
|
144
|
-
|
|
145
|
-
|
|
165
|
+
let method = req.method;
|
|
166
|
+
let url = req.url;
|
|
146
167
|
let body = null;
|
|
147
168
|
if (req.body) {
|
|
148
169
|
const buf = await req.clone().arrayBuffer();
|
|
149
170
|
body = buf.byteLength > 0 ? new Uint8Array(buf) : null;
|
|
150
171
|
}
|
|
151
|
-
const
|
|
172
|
+
const baseHeaders = {};
|
|
152
173
|
req.headers.forEach((value, key) => {
|
|
153
|
-
|
|
174
|
+
baseHeaders[key] = value;
|
|
154
175
|
});
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const outInit = {
|
|
159
|
-
...init,
|
|
160
|
-
method,
|
|
161
|
-
headers,
|
|
162
|
-
// Uint8Array is a valid BodyInit at runtime; DOM typings disagree.
|
|
163
|
-
body: body ?? void 0,
|
|
164
|
-
signal: init.signal ?? req.signal,
|
|
165
|
-
redirect: init.redirect ?? req.redirect,
|
|
166
|
-
credentials: init.credentials ?? req.credentials,
|
|
167
|
-
cache: init.cache ?? req.cache,
|
|
168
|
-
keepalive: init.keepalive ?? req.keepalive
|
|
169
|
-
};
|
|
170
|
-
const integrity = init.integrity ?? req.integrity;
|
|
171
|
-
if (integrity) outInit.integrity = integrity;
|
|
172
|
-
const referrerPolicy = init.referrerPolicy ?? req.referrerPolicy;
|
|
173
|
-
if (referrerPolicy) outInit.referrerPolicy = referrerPolicy;
|
|
176
|
+
const redirectMode = init.redirect ?? req.redirect;
|
|
177
|
+
const followHere = redirectMode === "follow";
|
|
178
|
+
const maxRedirects = options.maxRedirects ?? 20;
|
|
174
179
|
const doFetch = options.fetch ?? globalThis.fetch;
|
|
175
|
-
|
|
180
|
+
for (let hop = 0; ; hop++) {
|
|
181
|
+
const headers = { ...baseHeaders };
|
|
182
|
+
ensureContentDigest(method, headers, body);
|
|
183
|
+
await sign({ method, url, headers, body }, identity);
|
|
184
|
+
applyUserAgent(headers, identity.userAgent);
|
|
185
|
+
const outInit = {
|
|
186
|
+
...init,
|
|
187
|
+
method,
|
|
188
|
+
headers,
|
|
189
|
+
body: body ?? void 0,
|
|
190
|
+
signal: init.signal ?? req.signal,
|
|
191
|
+
redirect: followHere ? "manual" : redirectMode,
|
|
192
|
+
credentials: init.credentials ?? req.credentials,
|
|
193
|
+
cache: init.cache ?? req.cache,
|
|
194
|
+
keepalive: init.keepalive ?? req.keepalive
|
|
195
|
+
};
|
|
196
|
+
const integrity = init.integrity ?? req.integrity;
|
|
197
|
+
if (integrity) outInit.integrity = integrity;
|
|
198
|
+
const referrerPolicy = init.referrerPolicy ?? req.referrerPolicy;
|
|
199
|
+
if (referrerPolicy) outInit.referrerPolicy = referrerPolicy;
|
|
200
|
+
const res = await doFetch(url, outInit);
|
|
201
|
+
if (!followHere || !REDIRECT_STATUSES.has(res.status)) return res;
|
|
202
|
+
const location = res.headers.get("location");
|
|
203
|
+
if (location === null) return res;
|
|
204
|
+
if (hop >= maxRedirects) {
|
|
205
|
+
throw new TypeError(`redirect count exceeded (${maxRedirects})`);
|
|
206
|
+
}
|
|
207
|
+
const next = new URL(location, url);
|
|
208
|
+
if (next.protocol !== "http:" && next.protocol !== "https:") {
|
|
209
|
+
throw new TypeError(`unsupported redirect scheme: ${next.protocol}`);
|
|
210
|
+
}
|
|
211
|
+
const upper = method.toUpperCase();
|
|
212
|
+
if (res.status === 303 && upper !== "GET" && upper !== "HEAD" || (res.status === 301 || res.status === 302) && upper === "POST") {
|
|
213
|
+
method = "GET";
|
|
214
|
+
body = null;
|
|
215
|
+
dropHeaders(baseHeaders, BODY_HEADERS);
|
|
216
|
+
}
|
|
217
|
+
if (new URL(url).origin !== next.origin) {
|
|
218
|
+
dropHeaders(baseHeaders, CROSS_ORIGIN_STRIP);
|
|
219
|
+
}
|
|
220
|
+
await res.body?.cancel().catch(() => {
|
|
221
|
+
});
|
|
222
|
+
url = next.href;
|
|
223
|
+
}
|
|
176
224
|
};
|
|
177
225
|
}
|
|
178
226
|
|
|
@@ -359,7 +407,7 @@ async function generateKeypairTo(path) {
|
|
|
359
407
|
}
|
|
360
408
|
|
|
361
409
|
// src/index.ts
|
|
362
|
-
var VERSION = "0.
|
|
410
|
+
var VERSION = "0.3.0";
|
|
363
411
|
// Annotate the CommonJS export names for ESM import in node:
|
|
364
412
|
0 && (module.exports = {
|
|
365
413
|
DEFAULT_KEY_PATH,
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/signer.ts","../src/adapters/_utils.ts","../src/adapters/fetch.ts","../src/adapters/playwright.ts","../src/identity.ts"],"sourcesContent":["/**\n * wbauth: Web Bot Auth (RFC 9421) TypeScript SDK — Phase 4 public surface.\n *\n * Per CONTEXT D-74: flat root exports. Subpath exports (`wb-auth/adapters`)\n * are a v1.x optimization, not required for Phase 4.\n *\n * Quickstart:\n * import { Identity, createSignedFetch } from \"wb-auth\";\n * const id = await Identity.loadOrGenerate(\"./key.pem\", {\n * signatureAgentUrl: \"https://example.com/.well-known/.../<kid>\",\n * });\n * const sf = createSignedFetch(id);\n * const res = await sf(\"https://api.example.com/data\");\n */\n\nexport type { SignedFetchOptions } from \"./adapters/fetch.js\";\nexport { createSignedFetch } from \"./adapters/fetch.js\";\nexport type { ApplyToOptions } from \"./adapters/playwright.js\";\nexport { applyTo } from \"./adapters/playwright.js\";\nexport type { IdentityOptions, KeyPair } from \"./identity.js\";\nexport { DEFAULT_KEY_PATH, Identity } from \"./identity.js\";\nexport type { NormalizedRequest } from \"./normalized-request.js\";\nexport type { SignatureHeaders, SignOptions } from \"./signer.js\";\nexport { sign } from \"./signer.js\";\n\nexport const VERSION = \"0.2.0\";\n","/**\n * Pure-function signer for Web Bot Auth (RFC 9421).\n *\n * Mirrors python/src/wbauth/signer.py — same defaults (Ed25519, tag=\n * \"web-bot-auth\", expires=created+60s, covered components [\"@authority\",\n * \"signature-agent\"] + \"content-digest\" for bodies).\n *\n * The function delegates all RFC 9421 mechanics (signing-base canonicalization,\n * structured-field encoding, Ed25519 signing) to `web-bot-auth` 0.1.3's\n * `signatureHeaders`. We only own the Web Bot Auth profile glue: pre-setting\n * Signature-Agent (Pitfall 6), picking covered components, defaulting expires.\n */\nimport { type SignatureParams, signatureHeaders } from \"web-bot-auth\";\nimport type { Identity } from \"./identity.js\";\nimport type { NormalizedRequest } from \"./normalized-request.js\";\n\n// ---------- Module constants (HARD-CODED — typos = silent verifier reject) ----------\n\n/** IETF Web Bot Auth profile mandate. NEVER change. Pitfall 6. */\nconst WEB_BOT_AUTH_TAG = \"web-bot-auth\";\n/** Canonical Signature label. Most verifier examples assume \"sig1\". */\nconst DEFAULT_LABEL = \"sig1\";\n/** Default expires window. 60s leaves NTP-skew + network-latency headroom. */\nconst DEFAULT_EXPIRES_SECONDS = 60;\n/** Methods that may carry a body and therefore want content-digest covered. */\nconst DIGEST_METHODS = new Set([\"POST\", \"PUT\", \"PATCH\"]);\n// WEB_BOT_AUTH_TAG is enforced internally by web-bot-auth's signatureHeaders;\n// we keep the constant for greppability of the profile invariant.\nvoid WEB_BOT_AUTH_TAG;\n\n// ---------- Public types ----------\n\nexport interface SignatureHeaders {\n\tsignature: string;\n\tsignatureInput: string;\n\tsignatureAgent: string;\n}\n\nexport interface SignOptions {\n\t/** Signing timestamp. Defaults to `new Date()`. Override for vector reproducibility. */\n\tcreated?: Date;\n\t/** Validity window in seconds. Defaults to 60. */\n\texpiresAfterSeconds?: number;\n\t/** Anti-replay nonce. Defaults to web-bot-auth's `generateNonce()` (64-byte b64). */\n\tnonce?: string;\n\t/** Signature label (the `sig1=` prefix). Defaults to \"sig1\". */\n\tlabel?: string;\n}\n\n// ---------- Components selector (mirror Python `_components_for`) ----------\n\nfunction componentsFor(method: string, hasBody: boolean): string[] {\n\tconst base = [\"@authority\", \"signature-agent\"];\n\tif (hasBody && DIGEST_METHODS.has(method.toUpperCase())) {\n\t\tbase.push(\"content-digest\");\n\t}\n\treturn base;\n}\n\n/** Headers regenerated on every sign() call. */\nconst RESIGN_HEADERS = [\"signature\", \"signature-input\", \"signature-agent\"];\n\nfunction hasHeader(headers: Record<string, string>, name: string): boolean {\n\tconst n = name.toLowerCase();\n\treturn Object.keys(headers).some((k) => k.toLowerCase() === n);\n}\n\nfunction removeHeaders(headers: Record<string, string>, names: string[]): void {\n\tfor (const k of Object.keys(headers)) {\n\t\tif (names.includes(k.toLowerCase())) delete headers[k];\n\t}\n}\n\n// ---------- Public API ----------\n\n/**\n * Sign a NormalizedRequest with the Web Bot Auth profile.\n *\n * Mutates `request.headers` in place: writes Signature-Agent BEFORE building\n * the signing base (Pitfall 6 — without pre-set, web-bot-auth defaults to\n * `[\"@authority\"]` only and silently breaks byte-equality with the Python\n * signer). After signatureHeaders returns, also writes Signature and\n * Signature-Input back into `request.headers`.\n *\n * @returns the same three values as a typed dataclass for callers that prefer\n * a return value over header inspection.\n */\nexport async function sign(\n\trequest: NormalizedRequest,\n\tidentity: Identity,\n\topts: SignOptions = {},\n): Promise<SignatureHeaders> {\n\t// 1. Defensive https:// check — Identity ctor enforces it too, but a future\n\t// bypassed ctor would otherwise leak http://. Pitfall 1.\n\tif (!identity.signatureAgentUrl.startsWith(\"https://\")) {\n\t\tthrow new Error(\n\t\t\t`signatureAgentUrl must be https://, got: ${identity.signatureAgentUrl}`,\n\t\t);\n\t}\n\n\t// 2. Validate options up-front (parity with Python signer).\n\tconst expiresAfterSeconds =\n\t\topts.expiresAfterSeconds ?? DEFAULT_EXPIRES_SECONDS;\n\tif (!Number.isFinite(expiresAfterSeconds) || expiresAfterSeconds <= 0) {\n\t\tthrow new RangeError(\n\t\t\t`expiresAfterSeconds must be a positive number, got: ${expiresAfterSeconds}`,\n\t\t);\n\t}\n\n\t// 3. Drop stale signature headers in ANY casing. A retried / redirected\n\t// request still carries the previous hop's values; leaving them in place\n\t// produces merged `sig1=..., sig1=...` headers that every verifier rejects.\n\tremoveHeaders(request.headers, RESIGN_HEADERS);\n\n\t// 4. Set Signature-Agent header (RFC 8941 string in double quotes).\n\t// CRITICAL: must be set BEFORE signatureHeaders inspects the message,\n\t// otherwise web-bot-auth picks REQUEST_COMPONENTS_WITHOUT_SIGNATURE_AGENT.\n\tconst signatureAgentHeader = `\"${identity.signatureAgentUrl}\"`;\n\trequest.headers[\"Signature-Agent\"] = signatureAgentHeader;\n\n\t// 5. Defaults.\n\tconst created = opts.created ?? new Date();\n\tconst expires = new Date(created.getTime() + expiresAfterSeconds * 1000);\n\tconst label = opts.label ?? DEFAULT_LABEL;\n\n\t// 6. Components based on body presence.\n\tconst hasBody =\n\t\trequest.body !== null &&\n\t\trequest.body !== undefined &&\n\t\trequest.body.byteLength > 0;\n\tconst components = componentsFor(request.method, hasBody);\n\tif (\n\t\tcomponents.includes(\"content-digest\") &&\n\t\t!hasHeader(request.headers, \"content-digest\")\n\t) {\n\t\t// Python raises here too. Signing a missing header would yield a\n\t\t// signature that no verifier can ever validate.\n\t\tthrow new Error(\n\t\t\t'Covered header field \"content-digest\" not found in the message. ' +\n\t\t\t\t\"Call ensureContentDigest() or use createSignedFetch()/applyTo(), which add it automatically.\",\n\t\t);\n\t}\n\n\t// 7. Build a Web `Request` for web-bot-auth's `signatureHeaders`.\n\tconst headers = new Headers();\n\tfor (const [k, v] of Object.entries(request.headers)) headers.set(k, v);\n\tconst init: RequestInit = { method: request.method, headers };\n\t// Cast: Uint8Array is a valid BodyInit at runtime (web fetch spec) but DOM\n\t// typing wants ArrayBufferView/string/Blob etc. The cast is safe.\n\tif (hasBody) init.body = request.body as unknown as BodyInit;\n\tconst req = new Request(request.url, init);\n\n\t// 8. Delegate to web-bot-auth. The signer is the active key's pre-resolved\n\t// Signer (cached in Identity at construction time).\n\tconst params: SignatureParams = {\n\t\tcreated,\n\t\texpires,\n\t\tkey: label,\n\t\tcomponents,\n\t\t...(opts.nonce !== undefined ? { nonce: opts.nonce } : {}),\n\t};\n\tconst result = await signatureHeaders(req, identity._signer(), params);\n\n\t// 9. Mutate request.headers (mirror Python signer behavior).\n\trequest.headers.Signature = result.Signature;\n\trequest.headers[\"Signature-Input\"] = result[\"Signature-Input\"];\n\n\treturn {\n\t\tsignature: result.Signature,\n\t\tsignatureInput: result[\"Signature-Input\"],\n\t\tsignatureAgent: signatureAgentHeader,\n\t};\n}\n","/**\n * Shared adapter helpers (internal — not part of the public surface).\n *\n * Mirror python/src/wbauth/adapters/_utils.py. The signer auto-includes\n * `content-digest` in covered components for POST/PUT/PATCH with a body;\n * adapters call ensureContentDigest() before sign() so that precondition\n * is met. RFC 9530 sha-256 in structured-fields form (`sha-256=:<b64>:`).\n */\nimport { createHash } from \"node:crypto\";\n\nconst DIGEST_METHODS = new Set([\"POST\", \"PUT\", \"PATCH\"]);\n\n/** Default UAs injected by HTTP stacks; an Identity UA should replace them. */\nconst LIBRARY_DEFAULT_UA = /^(node|undici|node-fetch)(\\/|$)/i;\n\nfunction findHeader(\n\theaders: Record<string, string>,\n\tname: string,\n): string | undefined {\n\tconst n = name.toLowerCase();\n\treturn Object.keys(headers).find((k) => k.toLowerCase() === n);\n}\n\n/**\n * Mutate `headers` in place to add Content-Digest when warranted. No-op if:\n * - body is null/empty,\n * - method is not POST/PUT/PATCH,\n * - the request already carries a Content-Digest header (case-insensitive).\n */\nexport function ensureContentDigest(\n\tmethod: string,\n\theaders: Record<string, string>,\n\tbody: Uint8Array | null,\n): void {\n\tif (!body || body.byteLength === 0) return;\n\tif (!DIGEST_METHODS.has(method.toUpperCase())) return;\n\tif (findHeader(headers, \"content-digest\")) return;\n\tconst b64 = createHash(\"sha256\").update(body).digest(\"base64\");\n\theaders[\"Content-Digest\"] = `sha-256=:${b64}:`;\n}\n\n/**\n * Apply the Identity's User-Agent unless the caller explicitly set a\n * non-library UA. Library defaults (node, undici) are replaced.\n */\nexport function applyUserAgent(\n\theaders: Record<string, string>,\n\tuserAgent: string | undefined,\n): void {\n\tif (!userAgent) return;\n\tconst key = findHeader(headers, \"user-agent\");\n\tif (key === undefined) {\n\t\theaders[\"User-Agent\"] = userAgent;\n\t\treturn;\n\t}\n\tif (LIBRARY_DEFAULT_UA.test(headers[key] ?? \"\")) {\n\t\tdelete headers[key];\n\t\theaders[\"User-Agent\"] = userAgent;\n\t}\n}\n\n/**\n * Host allow-list matcher. Entries are exact hostnames (`api.example.com`)\n * or wildcard subdomains (`*.example.com`, which also matches `example.com`).\n */\nexport function hostMatches(\n\thostname: string,\n\tallow: readonly string[],\n): boolean {\n\tconst h = hostname.toLowerCase().replace(/\\.$/, \"\");\n\treturn allow.some((raw) => {\n\t\tconst a = raw.toLowerCase().replace(/\\.$/, \"\");\n\t\tif (a.startsWith(\"*.\")) {\n\t\t\tconst base = a.slice(2);\n\t\t\treturn h === base || h.endsWith(`.${base}`);\n\t\t}\n\t\treturn h === a;\n\t});\n}\n","/**\n * createSignedFetch — drop-in `fetch` wrapper that signs every outgoing request.\n *\n * Mirrors python/src/wbauth/adapters/httpx_auth.py. Stateless: closure over\n * `identity` only. Returns a function whose signature matches `typeof fetch`,\n * so user code can swap `fetch` for `signedFetch` without other changes.\n *\n * - Request-object inputs keep their `signal`, `redirect`, `credentials`,\n * `cache`, `keepalive`, `integrity` and `referrerPolicy`.\n * - Re-sending an already signed request (retry wrappers) produces exactly one\n * fresh signature — stale Signature* headers are replaced, never merged.\n *\n * v1 limitation: streaming bodies are buffered (`req.clone().arrayBuffer()`)\n * so Content-Digest can be computed.\n */\n\nimport type { Identity } from \"../identity.js\";\nimport { sign } from \"../signer.js\";\nimport { applyUserAgent, ensureContentDigest } from \"./_utils.js\";\n\nexport interface SignedFetchOptions {\n\t/** Underlying fetch implementation. Defaults to `globalThis.fetch` at call time. */\n\tfetch?: typeof fetch;\n}\n\nexport function createSignedFetch(\n\tidentity: Identity,\n\toptions: SignedFetchOptions = {},\n): typeof fetch {\n\treturn async function signedFetch(\n\t\tinput: RequestInfo | URL,\n\t\tinit: RequestInit = {},\n\t): Promise<Response> {\n\t\tconst req = new Request(input, init);\n\t\tconst method = req.method;\n\t\tconst url = req.url;\n\n\t\t// Read body once — Request consumes its body stream; clone first.\n\t\tlet body: Uint8Array | null = null;\n\t\tif (req.body) {\n\t\t\tconst buf = await req.clone().arrayBuffer();\n\t\t\tbody = buf.byteLength > 0 ? new Uint8Array(buf) : null;\n\t\t}\n\n\t\t// Mutable header dict (Headers already lower-cases + de-duplicates).\n\t\tconst headers: Record<string, string> = {};\n\t\treq.headers.forEach((value, key) => {\n\t\t\theaders[key] = value;\n\t\t});\n\n\t\t// Auto-Content-Digest BEFORE sign() (signer precondition for POST/PUT/PATCH + body).\n\t\tensureContentDigest(method, headers, body);\n\n\t\t// Sign — replaces stale Signature* headers, then writes fresh ones.\n\t\tawait sign({ method, url, headers, body }, identity);\n\t\tapplyUserAgent(headers, identity.userAgent);\n\n\t\tconst outInit: RequestInit = {\n\t\t\t...init,\n\t\t\tmethod,\n\t\t\theaders,\n\t\t\t// Uint8Array is a valid BodyInit at runtime; DOM typings disagree.\n\t\t\tbody: (body ?? undefined) as BodyInit | undefined,\n\t\t\tsignal: init.signal ?? req.signal,\n\t\t\tredirect: init.redirect ?? req.redirect,\n\t\t\tcredentials: init.credentials ?? req.credentials,\n\t\t\tcache: init.cache ?? req.cache,\n\t\t\tkeepalive: init.keepalive ?? req.keepalive,\n\t\t};\n\t\tconst integrity = init.integrity ?? req.integrity;\n\t\tif (integrity) outInit.integrity = integrity;\n\t\tconst referrerPolicy = init.referrerPolicy ?? req.referrerPolicy;\n\t\tif (referrerPolicy) outInit.referrerPolicy = referrerPolicy;\n\n\t\tconst doFetch = options.fetch ?? globalThis.fetch;\n\t\treturn doFetch(url, outInit);\n\t};\n}\n","/**\n * applyTo — sign every matching request a Playwright page sends.\n *\n * Mirrors python/src/wbauth/adapters/playwright.py.\n *\n * Privacy: by default every http(s) request is signed, including third-party\n * analytics/CDN hosts, which reveals your agent identity to them. Pass\n * `hosts` to restrict signing to the sites you actually target:\n *\n * await applyTo(page, identity, { hosts: [\"example.com\", \"*.example.org\"] });\n *\n * Non-http(s) requests (data:, blob:, chrome-extension:) are never signed.\n * Note: Playwright disables the HTTP cache while a route is active.\n */\nimport type { Page, Request as PWRequest, Route } from \"playwright\";\nimport type { Identity } from \"../identity.js\";\nimport { sign } from \"../signer.js\";\nimport { applyUserAgent, ensureContentDigest, hostMatches } from \"./_utils.js\";\n\nexport interface ApplyToOptions {\n\t/** Only sign requests to these hosts (`example.com`, `*.example.com`). */\n\thosts?: readonly string[];\n\t/** Custom predicate; takes precedence over `hosts`. */\n\tshouldSign?: (url: URL) => boolean;\n\t/** Route pattern passed to `page.route`. Default `\"**\\/*\"`. */\n\tpattern?: string;\n}\n\nexport async function applyTo(\n\tpage: Page,\n\tidentity: Identity,\n\toptions: ApplyToOptions = {},\n): Promise<void> {\n\tconst pattern = options.pattern ?? \"**/*\";\n\tconst decide = (raw: string): boolean => {\n\t\tlet u: URL;\n\t\ttry {\n\t\t\tu = new URL(raw);\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t\tif (u.protocol !== \"https:\" && u.protocol !== \"http:\") return false;\n\t\tif (options.shouldSign) return options.shouldSign(u);\n\t\tif (options.hosts) return hostMatches(u.hostname, options.hosts);\n\t\treturn true;\n\t};\n\n\tawait page.route(pattern, async (route: Route, request: PWRequest) => {\n\t\tconst url = request.url();\n\t\tif (!decide(url)) {\n\t\t\tconst r = route as Route & { fallback?: () => Promise<void> };\n\t\t\tawait (r.fallback ? r.fallback() : route.continue());\n\t\t\treturn;\n\t\t}\n\t\tconst headers = await request.allHeaders();\n\t\tconst postData = request.postDataBuffer();\n\t\tconst body = postData ? new Uint8Array(postData) : null;\n\t\tensureContentDigest(request.method(), headers, body);\n\t\tawait sign({ method: request.method(), url, headers, body }, identity);\n\t\tapplyUserAgent(headers, identity.userAgent);\n\t\tawait route.continue({ headers });\n\t});\n}\n","/**\n * Identity, KeyPair, and key-file helpers for the wbauth TypeScript SDK.\n *\n * Mirrors python/src/wbauth/identity.py — same on-disk format (PKCS8\n * NoEncryption PEM at 0o600), same kid (RFC 7638 JWK thumbprint), same\n * multi-key rotation semantics (active + retiring), same REDACTED repr\n * (IDENT-08).\n *\n * Cross-language guarantee (D-60): the PEM file Python writes via\n * `Ed25519PrivateKey.private_bytes(PEM, PKCS8, NoEncryption)` is byte-equal\n * to what Node's `crypto.generateKeyPairSync('ed25519').privateKey.export({\n * format: 'pem', type: 'pkcs8' })` writes. Both runtimes can load each\n * other's keyfile and produce the same kid + signatures.\n *\n * Key implementation choice (Pitfall 1): PEM→JWK is done via Node 20+\n * stdlib `node:crypto.createPrivateKey(pem).export({format:'jwk'})`. We do\n * NOT use `jose` or any third-party JOSE library — the stdlib output is\n * verified byte-identical to what Python's cryptography library produces\n * for the same PKCS8 PEM (RESEARCH §2.2 live verification 2026-05-10).\n */\n\nimport { createPrivateKey, generateKeyPairSync } from \"node:crypto\";\nimport {\n\tcloseSync,\n\texistsSync,\n\tmkdirSync,\n\topenSync,\n\treadFileSync,\n\tstatSync,\n\twriteFileSync,\n} from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, resolve } from \"node:path\";\nimport type { Signer } from \"http-message-sig\";\nimport { signerFromJWK } from \"web-bot-auth/crypto\";\n\n// ---------- Module constants ----------\n\n/** Default path for the active signing key. Matches Python DEFAULT_KEY_PATH. */\nexport const DEFAULT_KEY_PATH = resolve(\n\thomedir(),\n\t\".config\",\n\t\"wbauth\",\n\t\"key.pem\",\n);\n\n/**\n * RFC 9421 Appendix B.1.4 publicly-known test key — NEVER use in production.\n * The private \"d\" component is published in the RFC and known to everyone.\n * `Identity.fromTestKey()` returns an Identity using these bytes.\n */\nconst TEST_KEY_JWK: JsonWebKey = {\n\tkty: \"OKP\",\n\tcrv: \"Ed25519\",\n\td: \"n4Ni-HpISpVObnQMW0wOhCKROaIKqKtW_2ZYb2p9KcU\",\n\tx: \"JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs\",\n};\n\n// ---------- Public types ----------\n\nexport interface IdentityOptions {\n\t/** Stable HTTPS URL where this agent's JWKS is published. Must start with `https://`. */\n\tsignatureAgentUrl: string;\n\t/** Optional User-Agent string adapters inject when caller did not supply one. */\n\tuserAgent?: string;\n}\n\nexport interface KeyPair {\n\t/** Private JWK (includes \"d\") — used to mint the signer. Never logged. */\n\tprivateJwk: JsonWebKey;\n\t/** Public JWK ({kty, crv, x, kid}) — what exportJwks returns per key. */\n\tpublicJwk: JsonWebKey & { kid: string };\n\t/** RFC 7638 base64url-no-pad thumbprint of the public key. */\n\tkid: string;\n\t/** Pre-resolved web-bot-auth Signer (caches the imported CryptoKey). */\n\tsigner: Signer;\n}\n\n// ---------- Identity class ----------\n\nexport class Identity {\n\tprivate _active: KeyPair;\n\tprivate _retiring: KeyPair | null;\n\treadonly signatureAgentUrl: string;\n\treadonly userAgent: string | undefined;\n\n\tprivate constructor(\n\t\tactive: KeyPair,\n\t\topts: IdentityOptions,\n\t\tretiring: KeyPair | null = null,\n\t) {\n\t\tif (!opts.signatureAgentUrl.startsWith(\"https://\")) {\n\t\t\tthrow new Error(\n\t\t\t\t`signatureAgentUrl must be https://, got: ${opts.signatureAgentUrl}`,\n\t\t\t);\n\t\t}\n\t\tthis._active = active;\n\t\tthis._retiring = retiring;\n\t\tthis.signatureAgentUrl = opts.signatureAgentUrl;\n\t\tthis.userAgent = opts.userAgent;\n\t}\n\n\t// ---------- Public read-only API ----------\n\n\t/** The active key's kid (used as `keyid=` param in Signature-Input). */\n\tget kid(): string {\n\t\treturn this._active.kid;\n\t}\n\n\t/** Module-internal: returns the resolved Signer for sign() to use. */\n\t_signer(): Signer {\n\t\treturn this._active.signer;\n\t}\n\n\t/**\n\t * Export the public JWKS document.\n\t *\n\t * Returns `{keys: [active_jwk]}` if no retiring key is held, or\n\t * `{keys: [active_jwk, retiring_jwk]}` after a rotation. Active is always\n\t * first. Verifiers select by `kid`. Mirror of Python `export_jwks`.\n\t */\n\texportJwks(): { keys: Array<JsonWebKey & { kid: string }> } {\n\t\tconst keys: Array<JsonWebKey & { kid: string }> = [this._active.publicJwk];\n\t\tif (this._retiring) keys.push(this._retiring.publicJwk);\n\t\treturn { keys };\n\t}\n\n\t/**\n\t * IDENT-07: generate a new active key, demote current to retiring.\n\t *\n\t * Returns a NEW Identity (immutable update — the old Identity is unchanged).\n\t * Any previously-retiring key is dropped (only one overlap window).\n\t */\n\tasync rotate(newPath: string = DEFAULT_KEY_PATH): Promise<Identity> {\n\t\tconst newPair = await generateKeypairTo(expandUser(newPath));\n\t\treturn new Identity(\n\t\t\tnewPair,\n\t\t\t{ signatureAgentUrl: this.signatureAgentUrl, userAgent: this.userAgent },\n\t\t\tthis._active,\n\t\t);\n\t}\n\n\t// ---------- Constructors ----------\n\n\t/**\n\t * Primary entry point. Loads `path` if it exists, generates a fresh key\n\t * there otherwise. The on-disk file is PKCS8 NoEncryption PEM at 0o600,\n\t * byte-equal to what Python's `Identity.load_or_generate` writes (D-60).\n\t */\n\tstatic async loadOrGenerate(\n\t\tpath: string = DEFAULT_KEY_PATH,\n\t\topts: IdentityOptions,\n\t): Promise<Identity> {\n\t\tconst resolved = expandUser(path);\n\t\tconst pair = existsSync(resolved)\n\t\t\t? await loadKeypair(resolved)\n\t\t\t: await generateKeypairTo(resolved);\n\t\treturn new Identity(pair, opts);\n\t}\n\n\t/**\n\t * Construct from the RFC 9421 Appendix B.1.4 publicly-known test key.\n\t * For tests + Cloudflare debug smoke checks. NEVER use in production.\n\t * Does NOT write any file to disk.\n\t */\n\tstatic async fromTestKey(\n\t\tsignatureAgentUrl: string,\n\t\topts: Omit<IdentityOptions, \"signatureAgentUrl\"> = {},\n\t): Promise<Identity> {\n\t\tconst pair = await keyPairFromJwk(TEST_KEY_JWK);\n\t\treturn new Identity(pair, { ...opts, signatureAgentUrl });\n\t}\n\n\t// ---------- Redaction guarantees (IDENT-08) ----------\n\n\ttoString(): string {\n\t\treturn `<Identity REDACTED kid='${this.kid}' sig_agent='${this.signatureAgentUrl}'>`;\n\t}\n\n\t/** util.inspect / console.log render the same REDACTED form (IDENT-08). */\n\t[Symbol.for(\"nodejs.util.inspect.custom\")](): string {\n\t\treturn this.toString();\n\t}\n}\n\n// ---------- Helpers (module-private) ----------\n\nfunction expandUser(path: string): string {\n\treturn resolve(path.startsWith(\"~\") ? path.replace(/^~/, homedir()) : path);\n}\n\nasync function keyPairFromJwk(privateJwk: JsonWebKey): Promise<KeyPair> {\n\tconst signer = await signerFromJWK(privateJwk);\n\t// signerFromJWK pre-computes the kid (RFC 7638 thumbprint) into signer.keyid.\n\t// Reuse it instead of calling jwkToKeyID separately (avoids the 3-arg helper).\n\tconst kid = signer.keyid;\n\tconst publicJwk: JsonWebKey & { kid: string } = {\n\t\tkty: privateJwk.kty,\n\t\tcrv: privateJwk.crv,\n\t\tx: privateJwk.x,\n\t\tkid,\n\t};\n\treturn { privateJwk, publicJwk, kid, signer };\n}\n\nasync function loadKeypair(path: string): Promise<KeyPair> {\n\t// POSIX permission check (mirror Python — refuse wider-than-0o600).\n\t// Pitfall 8: Windows lacks POSIX mode bits → emit warning, do not enforce.\n\tif (process.platform !== \"win32\") {\n\t\tconst mode = statSync(path).mode & 0o777;\n\t\tif ((mode & 0o077) !== 0) {\n\t\t\tthrow new Error(\n\t\t\t\t`Key file ${path} has mode 0o${mode.toString(8)}; expected 0o600. Fix: chmod 600 ${path}`,\n\t\t\t);\n\t\t}\n\t} else {\n\t\tprocess.emitWarning(\n\t\t\t`Windows detected — file permissions on ${path} cannot be enforced via POSIX mode. Use NTFS ACLs or store the key in a per-user secrets vault.`,\n\t\t);\n\t}\n\tconst pem = readFileSync(path, \"utf8\");\n\tconst jwk = createPrivateKey(pem).export({ format: \"jwk\" }) as JsonWebKey;\n\tif (jwk.kty !== \"OKP\" || jwk.crv !== \"Ed25519\") {\n\t\tthrow new TypeError(`Expected Ed25519, got kty=${jwk.kty}, crv=${jwk.crv}`);\n\t}\n\treturn keyPairFromJwk(jwk);\n}\n\nasync function generateKeypairTo(path: string): Promise<KeyPair> {\n\tconst dir = dirname(path);\n\tmkdirSync(dir, { recursive: true });\n\tif (existsSync(path)) {\n\t\t// Belt + suspenders: O_EXCL would catch this too, but raising here gives\n\t\t// a cleaner error than the low-level EEXIST.\n\t\tthrow new Error(`Key already exists at ${path}; refuse overwrite`);\n\t}\n\tconst { privateKey } = generateKeyPairSync(\"ed25519\");\n\tconst pem = privateKey.export({ format: \"pem\", type: \"pkcs8\" }) as string;\n\t// Race-free 0o600 creation: openSync with \"wx\" = O_WRONLY|O_CREAT|O_EXCL.\n\t// Mirrors Python `os.open(path, O_WRONLY|O_CREAT|O_EXCL, 0o600)`.\n\tconst fd = openSync(path, \"wx\", 0o600);\n\ttry {\n\t\twriteFileSync(fd, pem);\n\t} finally {\n\t\tcloseSync(fd);\n\t}\n\tconst jwk = privateKey.export({ format: \"jwk\" }) as JsonWebKey;\n\treturn keyPairFromJwk(jwk);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACYA,0BAAuD;AASvD,IAAM,gBAAgB;AAEtB,IAAM,0BAA0B;AAEhC,IAAM,iBAAiB,oBAAI,IAAI,CAAC,QAAQ,OAAO,OAAO,CAAC;AA0BvD,SAAS,cAAc,QAAgB,SAA4B;AAClE,QAAM,OAAO,CAAC,cAAc,iBAAiB;AAC7C,MAAI,WAAW,eAAe,IAAI,OAAO,YAAY,CAAC,GAAG;AACxD,SAAK,KAAK,gBAAgB;AAAA,EAC3B;AACA,SAAO;AACR;AAGA,IAAM,iBAAiB,CAAC,aAAa,mBAAmB,iBAAiB;AAEzE,SAAS,UAAU,SAAiC,MAAuB;AAC1E,QAAM,IAAI,KAAK,YAAY;AAC3B,SAAO,OAAO,KAAK,OAAO,EAAE,KAAK,CAAC,MAAM,EAAE,YAAY,MAAM,CAAC;AAC9D;AAEA,SAAS,cAAc,SAAiC,OAAuB;AAC9E,aAAW,KAAK,OAAO,KAAK,OAAO,GAAG;AACrC,QAAI,MAAM,SAAS,EAAE,YAAY,CAAC,EAAG,QAAO,QAAQ,CAAC;AAAA,EACtD;AACD;AAgBA,eAAsB,KACrB,SACA,UACA,OAAoB,CAAC,GACO;AAG5B,MAAI,CAAC,SAAS,kBAAkB,WAAW,UAAU,GAAG;AACvD,UAAM,IAAI;AAAA,MACT,4CAA4C,SAAS,iBAAiB;AAAA,IACvE;AAAA,EACD;AAGA,QAAM,sBACL,KAAK,uBAAuB;AAC7B,MAAI,CAAC,OAAO,SAAS,mBAAmB,KAAK,uBAAuB,GAAG;AACtE,UAAM,IAAI;AAAA,MACT,uDAAuD,mBAAmB;AAAA,IAC3E;AAAA,EACD;AAKA,gBAAc,QAAQ,SAAS,cAAc;AAK7C,QAAM,uBAAuB,IAAI,SAAS,iBAAiB;AAC3D,UAAQ,QAAQ,iBAAiB,IAAI;AAGrC,QAAM,UAAU,KAAK,WAAW,oBAAI,KAAK;AACzC,QAAM,UAAU,IAAI,KAAK,QAAQ,QAAQ,IAAI,sBAAsB,GAAI;AACvE,QAAM,QAAQ,KAAK,SAAS;AAG5B,QAAM,UACL,QAAQ,SAAS,QACjB,QAAQ,SAAS,UACjB,QAAQ,KAAK,aAAa;AAC3B,QAAM,aAAa,cAAc,QAAQ,QAAQ,OAAO;AACxD,MACC,WAAW,SAAS,gBAAgB,KACpC,CAAC,UAAU,QAAQ,SAAS,gBAAgB,GAC3C;AAGD,UAAM,IAAI;AAAA,MACT;AAAA,IAED;AAAA,EACD;AAGA,QAAM,UAAU,IAAI,QAAQ;AAC5B,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,QAAQ,OAAO,EAAG,SAAQ,IAAI,GAAG,CAAC;AACtE,QAAM,OAAoB,EAAE,QAAQ,QAAQ,QAAQ,QAAQ;AAG5D,MAAI,QAAS,MAAK,OAAO,QAAQ;AACjC,QAAM,MAAM,IAAI,QAAQ,QAAQ,KAAK,IAAI;AAIzC,QAAM,SAA0B;AAAA,IAC/B;AAAA,IACA;AAAA,IACA,KAAK;AAAA,IACL;AAAA,IACA,GAAI,KAAK,UAAU,SAAY,EAAE,OAAO,KAAK,MAAM,IAAI,CAAC;AAAA,EACzD;AACA,QAAM,SAAS,UAAM,sCAAiB,KAAK,SAAS,QAAQ,GAAG,MAAM;AAGrE,UAAQ,QAAQ,YAAY,OAAO;AACnC,UAAQ,QAAQ,iBAAiB,IAAI,OAAO,iBAAiB;AAE7D,SAAO;AAAA,IACN,WAAW,OAAO;AAAA,IAClB,gBAAgB,OAAO,iBAAiB;AAAA,IACxC,gBAAgB;AAAA,EACjB;AACD;;;ACpKA,yBAA2B;AAE3B,IAAMA,kBAAiB,oBAAI,IAAI,CAAC,QAAQ,OAAO,OAAO,CAAC;AAGvD,IAAM,qBAAqB;AAE3B,SAAS,WACR,SACA,MACqB;AACrB,QAAM,IAAI,KAAK,YAAY;AAC3B,SAAO,OAAO,KAAK,OAAO,EAAE,KAAK,CAAC,MAAM,EAAE,YAAY,MAAM,CAAC;AAC9D;AAQO,SAAS,oBACf,QACA,SACA,MACO;AACP,MAAI,CAAC,QAAQ,KAAK,eAAe,EAAG;AACpC,MAAI,CAACA,gBAAe,IAAI,OAAO,YAAY,CAAC,EAAG;AAC/C,MAAI,WAAW,SAAS,gBAAgB,EAAG;AAC3C,QAAM,UAAM,+BAAW,QAAQ,EAAE,OAAO,IAAI,EAAE,OAAO,QAAQ;AAC7D,UAAQ,gBAAgB,IAAI,YAAY,GAAG;AAC5C;AAMO,SAAS,eACf,SACA,WACO;AACP,MAAI,CAAC,UAAW;AAChB,QAAM,MAAM,WAAW,SAAS,YAAY;AAC5C,MAAI,QAAQ,QAAW;AACtB,YAAQ,YAAY,IAAI;AACxB;AAAA,EACD;AACA,MAAI,mBAAmB,KAAK,QAAQ,GAAG,KAAK,EAAE,GAAG;AAChD,WAAO,QAAQ,GAAG;AAClB,YAAQ,YAAY,IAAI;AAAA,EACzB;AACD;AAMO,SAAS,YACf,UACA,OACU;AACV,QAAM,IAAI,SAAS,YAAY,EAAE,QAAQ,OAAO,EAAE;AAClD,SAAO,MAAM,KAAK,CAAC,QAAQ;AAC1B,UAAM,IAAI,IAAI,YAAY,EAAE,QAAQ,OAAO,EAAE;AAC7C,QAAI,EAAE,WAAW,IAAI,GAAG;AACvB,YAAM,OAAO,EAAE,MAAM,CAAC;AACtB,aAAO,MAAM,QAAQ,EAAE,SAAS,IAAI,IAAI,EAAE;AAAA,IAC3C;AACA,WAAO,MAAM;AAAA,EACd,CAAC;AACF;;;ACrDO,SAAS,kBACf,UACA,UAA8B,CAAC,GAChB;AACf,SAAO,eAAe,YACrB,OACA,OAAoB,CAAC,GACD;AACpB,UAAM,MAAM,IAAI,QAAQ,OAAO,IAAI;AACnC,UAAM,SAAS,IAAI;AACnB,UAAM,MAAM,IAAI;AAGhB,QAAI,OAA0B;AAC9B,QAAI,IAAI,MAAM;AACb,YAAM,MAAM,MAAM,IAAI,MAAM,EAAE,YAAY;AAC1C,aAAO,IAAI,aAAa,IAAI,IAAI,WAAW,GAAG,IAAI;AAAA,IACnD;AAGA,UAAM,UAAkC,CAAC;AACzC,QAAI,QAAQ,QAAQ,CAAC,OAAO,QAAQ;AACnC,cAAQ,GAAG,IAAI;AAAA,IAChB,CAAC;AAGD,wBAAoB,QAAQ,SAAS,IAAI;AAGzC,UAAM,KAAK,EAAE,QAAQ,KAAK,SAAS,KAAK,GAAG,QAAQ;AACnD,mBAAe,SAAS,SAAS,SAAS;AAE1C,UAAM,UAAuB;AAAA,MAC5B,GAAG;AAAA,MACH;AAAA,MACA;AAAA;AAAA,MAEA,MAAO,QAAQ;AAAA,MACf,QAAQ,KAAK,UAAU,IAAI;AAAA,MAC3B,UAAU,KAAK,YAAY,IAAI;AAAA,MAC/B,aAAa,KAAK,eAAe,IAAI;AAAA,MACrC,OAAO,KAAK,SAAS,IAAI;AAAA,MACzB,WAAW,KAAK,aAAa,IAAI;AAAA,IAClC;AACA,UAAM,YAAY,KAAK,aAAa,IAAI;AACxC,QAAI,UAAW,SAAQ,YAAY;AACnC,UAAM,iBAAiB,KAAK,kBAAkB,IAAI;AAClD,QAAI,eAAgB,SAAQ,iBAAiB;AAE7C,UAAM,UAAU,QAAQ,SAAS,WAAW;AAC5C,WAAO,QAAQ,KAAK,OAAO;AAAA,EAC5B;AACD;;;ACjDA,eAAsB,QACrB,MACA,UACA,UAA0B,CAAC,GACX;AAChB,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,SAAS,CAAC,QAAyB;AACxC,QAAI;AACJ,QAAI;AACH,UAAI,IAAI,IAAI,GAAG;AAAA,IAChB,QAAQ;AACP,aAAO;AAAA,IACR;AACA,QAAI,EAAE,aAAa,YAAY,EAAE,aAAa,QAAS,QAAO;AAC9D,QAAI,QAAQ,WAAY,QAAO,QAAQ,WAAW,CAAC;AACnD,QAAI,QAAQ,MAAO,QAAO,YAAY,EAAE,UAAU,QAAQ,KAAK;AAC/D,WAAO;AAAA,EACR;AAEA,QAAM,KAAK,MAAM,SAAS,OAAO,OAAc,YAAuB;AACrE,UAAM,MAAM,QAAQ,IAAI;AACxB,QAAI,CAAC,OAAO,GAAG,GAAG;AACjB,YAAM,IAAI;AACV,aAAO,EAAE,WAAW,EAAE,SAAS,IAAI,MAAM,SAAS;AAClD;AAAA,IACD;AACA,UAAM,UAAU,MAAM,QAAQ,WAAW;AACzC,UAAM,WAAW,QAAQ,eAAe;AACxC,UAAM,OAAO,WAAW,IAAI,WAAW,QAAQ,IAAI;AACnD,wBAAoB,QAAQ,OAAO,GAAG,SAAS,IAAI;AACnD,UAAM,KAAK,EAAE,QAAQ,QAAQ,OAAO,GAAG,KAAK,SAAS,KAAK,GAAG,QAAQ;AACrE,mBAAe,SAAS,SAAS,SAAS;AAC1C,UAAM,MAAM,SAAS,EAAE,QAAQ,CAAC;AAAA,EACjC,CAAC;AACF;;;ACzCA,IAAAC,sBAAsD;AACtD,qBAQO;AACP,qBAAwB;AACxB,uBAAiC;AAEjC,oBAA8B;AAKvB,IAAM,uBAAmB;AAAA,MAC/B,wBAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA;AACD;AAOA,IAAM,eAA2B;AAAA,EAChC,KAAK;AAAA,EACL,KAAK;AAAA,EACL,GAAG;AAAA,EACH,GAAG;AACJ;AAwBO,IAAM,WAAN,MAAM,UAAS;AAAA,EACb;AAAA,EACA;AAAA,EACC;AAAA,EACA;AAAA,EAED,YACP,QACA,MACA,WAA2B,MAC1B;AACD,QAAI,CAAC,KAAK,kBAAkB,WAAW,UAAU,GAAG;AACnD,YAAM,IAAI;AAAA,QACT,4CAA4C,KAAK,iBAAiB;AAAA,MACnE;AAAA,IACD;AACA,SAAK,UAAU;AACf,SAAK,YAAY;AACjB,SAAK,oBAAoB,KAAK;AAC9B,SAAK,YAAY,KAAK;AAAA,EACvB;AAAA;AAAA;AAAA,EAKA,IAAI,MAAc;AACjB,WAAO,KAAK,QAAQ;AAAA,EACrB;AAAA;AAAA,EAGA,UAAkB;AACjB,WAAO,KAAK,QAAQ;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,aAA4D;AAC3D,UAAM,OAA4C,CAAC,KAAK,QAAQ,SAAS;AACzE,QAAI,KAAK,UAAW,MAAK,KAAK,KAAK,UAAU,SAAS;AACtD,WAAO,EAAE,KAAK;AAAA,EACf;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,OAAO,UAAkB,kBAAqC;AACnE,UAAM,UAAU,MAAM,kBAAkB,WAAW,OAAO,CAAC;AAC3D,WAAO,IAAI;AAAA,MACV;AAAA,MACA,EAAE,mBAAmB,KAAK,mBAAmB,WAAW,KAAK,UAAU;AAAA,MACvE,KAAK;AAAA,IACN;AAAA,EACD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,aAAa,eACZ,OAAe,kBACf,MACoB;AACpB,UAAM,WAAW,WAAW,IAAI;AAChC,UAAM,WAAO,2BAAW,QAAQ,IAC7B,MAAM,YAAY,QAAQ,IAC1B,MAAM,kBAAkB,QAAQ;AACnC,WAAO,IAAI,UAAS,MAAM,IAAI;AAAA,EAC/B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,aAAa,YACZ,mBACA,OAAmD,CAAC,GAChC;AACpB,UAAM,OAAO,MAAM,eAAe,YAAY;AAC9C,WAAO,IAAI,UAAS,MAAM,EAAE,GAAG,MAAM,kBAAkB,CAAC;AAAA,EACzD;AAAA;AAAA,EAIA,WAAmB;AAClB,WAAO,2BAA2B,KAAK,GAAG,gBAAgB,KAAK,iBAAiB;AAAA,EACjF;AAAA;AAAA,EAGA,CAAC,uBAAO,IAAI,4BAA4B,CAAC,IAAY;AACpD,WAAO,KAAK,SAAS;AAAA,EACtB;AACD;AAIA,SAAS,WAAW,MAAsB;AACzC,aAAO,0BAAQ,KAAK,WAAW,GAAG,IAAI,KAAK,QAAQ,UAAM,wBAAQ,CAAC,IAAI,IAAI;AAC3E;AAEA,eAAe,eAAe,YAA0C;AACvE,QAAM,SAAS,UAAM,6BAAc,UAAU;AAG7C,QAAM,MAAM,OAAO;AACnB,QAAM,YAA0C;AAAA,IAC/C,KAAK,WAAW;AAAA,IAChB,KAAK,WAAW;AAAA,IAChB,GAAG,WAAW;AAAA,IACd;AAAA,EACD;AACA,SAAO,EAAE,YAAY,WAAW,KAAK,OAAO;AAC7C;AAEA,eAAe,YAAY,MAAgC;AAG1D,MAAI,QAAQ,aAAa,SAAS;AACjC,UAAM,WAAO,yBAAS,IAAI,EAAE,OAAO;AACnC,SAAK,OAAO,QAAW,GAAG;AACzB,YAAM,IAAI;AAAA,QACT,YAAY,IAAI,eAAe,KAAK,SAAS,CAAC,CAAC,oCAAoC,IAAI;AAAA,MACxF;AAAA,IACD;AAAA,EACD,OAAO;AACN,YAAQ;AAAA,MACP,+CAA0C,IAAI;AAAA,IAC/C;AAAA,EACD;AACA,QAAM,UAAM,6BAAa,MAAM,MAAM;AACrC,QAAM,UAAM,sCAAiB,GAAG,EAAE,OAAO,EAAE,QAAQ,MAAM,CAAC;AAC1D,MAAI,IAAI,QAAQ,SAAS,IAAI,QAAQ,WAAW;AAC/C,UAAM,IAAI,UAAU,6BAA6B,IAAI,GAAG,SAAS,IAAI,GAAG,EAAE;AAAA,EAC3E;AACA,SAAO,eAAe,GAAG;AAC1B;AAEA,eAAe,kBAAkB,MAAgC;AAChE,QAAM,UAAM,0BAAQ,IAAI;AACxB,gCAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAClC,UAAI,2BAAW,IAAI,GAAG;AAGrB,UAAM,IAAI,MAAM,yBAAyB,IAAI,oBAAoB;AAAA,EAClE;AACA,QAAM,EAAE,WAAW,QAAI,yCAAoB,SAAS;AACpD,QAAM,MAAM,WAAW,OAAO,EAAE,QAAQ,OAAO,MAAM,QAAQ,CAAC;AAG9D,QAAM,SAAK,yBAAS,MAAM,MAAM,GAAK;AACrC,MAAI;AACH,sCAAc,IAAI,GAAG;AAAA,EACtB,UAAE;AACD,kCAAU,EAAE;AAAA,EACb;AACA,QAAM,MAAM,WAAW,OAAO,EAAE,QAAQ,MAAM,CAAC;AAC/C,SAAO,eAAe,GAAG;AAC1B;;;AL/NO,IAAM,UAAU;","names":["DIGEST_METHODS","import_node_crypto"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/signer.ts","../src/adapters/_utils.ts","../src/adapters/fetch.ts","../src/adapters/playwright.ts","../src/identity.ts"],"sourcesContent":["/**\n * wbauth: Web Bot Auth (RFC 9421) TypeScript SDK — Phase 4 public surface.\n *\n * Per CONTEXT D-74: flat root exports. Subpath exports (`wb-auth/adapters`)\n * are a v1.x optimization, not required for Phase 4.\n *\n * Quickstart:\n * import { Identity, createSignedFetch } from \"wb-auth\";\n * const id = await Identity.loadOrGenerate(\"./key.pem\", {\n * signatureAgentUrl: \"https://example.com/.well-known/.../<kid>\",\n * });\n * const sf = createSignedFetch(id);\n * const res = await sf(\"https://api.example.com/data\");\n */\n\nexport type { SignedFetchOptions } from \"./adapters/fetch.js\";\nexport { createSignedFetch } from \"./adapters/fetch.js\";\nexport type { ApplyToOptions } from \"./adapters/playwright.js\";\nexport { applyTo } from \"./adapters/playwright.js\";\nexport type { IdentityOptions, KeyPair } from \"./identity.js\";\nexport { DEFAULT_KEY_PATH, Identity } from \"./identity.js\";\nexport type { NormalizedRequest } from \"./normalized-request.js\";\nexport type { SignatureHeaders, SignOptions } from \"./signer.js\";\nexport { sign } from \"./signer.js\";\n\nexport const VERSION = \"0.3.0\";\n","/**\n * Pure-function signer for Web Bot Auth (RFC 9421).\n *\n * Mirrors python/src/wbauth/signer.py — same defaults (Ed25519, tag=\n * \"web-bot-auth\", expires=created+60s, covered components [\"@authority\",\n * \"signature-agent\"] + \"content-digest\" for bodies).\n *\n * The function delegates all RFC 9421 mechanics (signing-base canonicalization,\n * structured-field encoding, Ed25519 signing) to `web-bot-auth` 0.1.3's\n * `signatureHeaders`. We only own the Web Bot Auth profile glue: pre-setting\n * Signature-Agent (Pitfall 6), picking covered components, defaulting expires.\n */\nimport { type SignatureParams, signatureHeaders } from \"web-bot-auth\";\nimport type { Identity } from \"./identity.js\";\nimport type { NormalizedRequest } from \"./normalized-request.js\";\n\n// ---------- Module constants (HARD-CODED — typos = silent verifier reject) ----------\n\n/** IETF Web Bot Auth profile mandate. NEVER change. Pitfall 6. */\nconst WEB_BOT_AUTH_TAG = \"web-bot-auth\";\n/** Canonical Signature label. Most verifier examples assume \"sig1\". */\nconst DEFAULT_LABEL = \"sig1\";\n/** Default expires window. 60s leaves NTP-skew + network-latency headroom. */\nconst DEFAULT_EXPIRES_SECONDS = 60;\n/** Methods that may carry a body and therefore want content-digest covered. */\n/** Hard upper bound on the validity window (parity with Python). */\nexport const MAX_EXPIRES_SECONDS = 3600;\n// WEB_BOT_AUTH_TAG is enforced internally by web-bot-auth's signatureHeaders;\n// we keep the constant for greppability of the profile invariant.\nvoid WEB_BOT_AUTH_TAG;\n\n// ---------- Public types ----------\n\nexport interface SignatureHeaders {\n\tsignature: string;\n\tsignatureInput: string;\n\tsignatureAgent: string;\n}\n\nexport interface SignOptions {\n\t/** Signing timestamp. Defaults to `new Date()`. Override for vector reproducibility. */\n\tcreated?: Date;\n\t/** Validity window in seconds. Defaults to 60. */\n\texpiresAfterSeconds?: number;\n\t/** Anti-replay nonce. Defaults to web-bot-auth's `generateNonce()` (64-byte b64). */\n\tnonce?: string;\n\t/** Signature label (the `sig1=` prefix). Defaults to \"sig1\". */\n\tlabel?: string;\n}\n\n// ---------- Components selector (mirror Python `_components_for`) ----------\n\nfunction componentsFor(_method: string, hasBody: boolean): string[] {\n\tconst base = [\"@authority\", \"signature-agent\"];\n\tif (hasBody) {\n\t\tbase.push(\"content-digest\");\n\t}\n\treturn base;\n}\n\n/** Headers regenerated on every sign() call. */\nconst RESIGN_HEADERS = [\"signature\", \"signature-input\", \"signature-agent\"];\n\nfunction hasHeader(headers: Record<string, string>, name: string): boolean {\n\tconst n = name.toLowerCase();\n\treturn Object.keys(headers).some((k) => k.toLowerCase() === n);\n}\n\nfunction removeHeaders(headers: Record<string, string>, names: string[]): void {\n\tfor (const k of Object.keys(headers)) {\n\t\tif (names.includes(k.toLowerCase())) delete headers[k];\n\t}\n}\n\n// ---------- Public API ----------\n\n/**\n * Sign a NormalizedRequest with the Web Bot Auth profile.\n *\n * Mutates `request.headers` in place: writes Signature-Agent BEFORE building\n * the signing base (Pitfall 6 — without pre-set, web-bot-auth defaults to\n * `[\"@authority\"]` only and silently breaks byte-equality with the Python\n * signer). After signatureHeaders returns, also writes Signature and\n * Signature-Input back into `request.headers`.\n *\n * @returns the same three values as a typed dataclass for callers that prefer\n * a return value over header inspection.\n */\nexport async function sign(\n\trequest: NormalizedRequest,\n\tidentity: Identity,\n\topts: SignOptions = {},\n): Promise<SignatureHeaders> {\n\t// 1. Defensive https:// check — Identity ctor enforces it too, but a future\n\t// bypassed ctor would otherwise leak http://. Pitfall 1.\n\tif (!identity.signatureAgentUrl.startsWith(\"https://\")) {\n\t\tthrow new Error(\n\t\t\t`signatureAgentUrl must be https://, got: ${identity.signatureAgentUrl}`,\n\t\t);\n\t}\n\n\t// 2. Validate options up-front (parity with Python signer).\n\tconst expiresAfterSeconds =\n\t\topts.expiresAfterSeconds ?? DEFAULT_EXPIRES_SECONDS;\n\tif (\n\t\ttypeof expiresAfterSeconds !== \"number\" ||\n\t\t!Number.isFinite(expiresAfterSeconds) ||\n\t\texpiresAfterSeconds <= 0 ||\n\t\texpiresAfterSeconds > MAX_EXPIRES_SECONDS\n\t) {\n\t\tthrow new RangeError(\n\t\t\t`expiresAfterSeconds must be a number in (0, ${MAX_EXPIRES_SECONDS}], got: ${expiresAfterSeconds}`,\n\t\t);\n\t}\n\n\t// 3. Drop stale signature headers in ANY casing. A retried / redirected\n\t// request still carries the previous hop's values; leaving them in place\n\t// produces merged `sig1=..., sig1=...` headers that every verifier rejects.\n\tremoveHeaders(request.headers, RESIGN_HEADERS);\n\n\t// 4. Set Signature-Agent header (RFC 8941 string in double quotes).\n\t// CRITICAL: must be set BEFORE signatureHeaders inspects the message,\n\t// otherwise web-bot-auth picks REQUEST_COMPONENTS_WITHOUT_SIGNATURE_AGENT.\n\tconst signatureAgentHeader = `\"${identity.signatureAgentUrl}\"`;\n\trequest.headers[\"Signature-Agent\"] = signatureAgentHeader;\n\n\t// 5. Defaults.\n\tconst created = opts.created ?? new Date();\n\tconst expires = new Date(created.getTime() + expiresAfterSeconds * 1000);\n\tconst label = opts.label ?? DEFAULT_LABEL;\n\n\t// 6. Components based on body presence.\n\tconst hasBody =\n\t\trequest.body !== null &&\n\t\trequest.body !== undefined &&\n\t\trequest.body.byteLength > 0;\n\tconst components = componentsFor(request.method, hasBody);\n\tif (\n\t\tcomponents.includes(\"content-digest\") &&\n\t\t!hasHeader(request.headers, \"content-digest\")\n\t) {\n\t\t// Python raises here too. Signing a missing header would yield a\n\t\t// signature that no verifier can ever validate.\n\t\tthrow new Error(\n\t\t\t'Covered header field \"content-digest\" not found in the message. ' +\n\t\t\t\t\"Call ensureContentDigest() or use createSignedFetch()/applyTo(), which add it automatically.\",\n\t\t);\n\t}\n\n\t// 7. Build a Web `Request` for web-bot-auth's `signatureHeaders`.\n\tconst headers = new Headers();\n\tfor (const [k, v] of Object.entries(request.headers)) headers.set(k, v);\n\tconst init: RequestInit = { method: request.method, headers };\n\t// Cast: Uint8Array is a valid BodyInit at runtime (web fetch spec) but DOM\n\t// typing wants ArrayBufferView/string/Blob etc. The cast is safe.\n\t// GET/HEAD cannot carry a body in a WHATWG Request; the body itself is not\n\t// part of the signature base (content-digest is), so it is safe to omit.\n\tconst m = request.method.toUpperCase();\n\tif (hasBody && m !== \"GET\" && m !== \"HEAD\") {\n\t\tinit.body = request.body as unknown as BodyInit;\n\t}\n\tconst req = new Request(request.url, init);\n\n\t// 8. Delegate to web-bot-auth. The signer is the active key's pre-resolved\n\t// Signer (cached in Identity at construction time).\n\tconst params: SignatureParams = {\n\t\tcreated,\n\t\texpires,\n\t\tkey: label,\n\t\tcomponents,\n\t\t...(opts.nonce !== undefined ? { nonce: opts.nonce } : {}),\n\t};\n\tconst result = await signatureHeaders(req, identity._signer(), params);\n\n\t// 9. Mutate request.headers (mirror Python signer behavior).\n\trequest.headers.Signature = result.Signature;\n\trequest.headers[\"Signature-Input\"] = result[\"Signature-Input\"];\n\n\treturn {\n\t\tsignature: result.Signature,\n\t\tsignatureInput: result[\"Signature-Input\"],\n\t\tsignatureAgent: signatureAgentHeader,\n\t};\n}\n","/**\n * Shared adapter helpers (internal — not part of the public surface).\n *\n * Mirror python/src/wbauth/adapters/_utils.py. The signer auto-includes\n * `content-digest` in covered components for any request with a body;\n * adapters call ensureContentDigest() before sign() so that precondition\n * is met. RFC 9530 sha-256 in structured-fields form (`sha-256=:<b64>:`).\n */\nimport { createHash } from \"node:crypto\";\n\n/** Default UAs injected by HTTP stacks; an Identity UA should replace them. */\nconst LIBRARY_DEFAULT_UA = /^(node|undici|node-fetch)(\\/|$)/i;\n\nfunction findHeader(\n\theaders: Record<string, string>,\n\tname: string,\n): string | undefined {\n\tconst n = name.toLowerCase();\n\treturn Object.keys(headers).find((k) => k.toLowerCase() === n);\n}\n\n/**\n * Mutate `headers` in place to add Content-Digest when warranted. No-op if:\n * - body is null/empty,\n * - the request already carries a Content-Digest header (case-insensitive).\n */\nexport function ensureContentDigest(\n\tmethod: string,\n\theaders: Record<string, string>,\n\tbody: Uint8Array | null,\n): void {\n\tif (!body || body.byteLength === 0) return;\n\tvoid method; // any method with a body is covered (DELETE, QUERY, …)\n\tif (findHeader(headers, \"content-digest\")) return;\n\tconst b64 = createHash(\"sha256\").update(body).digest(\"base64\");\n\theaders[\"Content-Digest\"] = `sha-256=:${b64}:`;\n}\n\n/**\n * Apply the Identity's User-Agent unless the caller explicitly set a\n * non-library UA. Library defaults (node, undici) are replaced.\n */\nexport function applyUserAgent(\n\theaders: Record<string, string>,\n\tuserAgent: string | undefined,\n): void {\n\tif (!userAgent) return;\n\tconst key = findHeader(headers, \"user-agent\");\n\tif (key === undefined) {\n\t\theaders[\"User-Agent\"] = userAgent;\n\t\treturn;\n\t}\n\tif (LIBRARY_DEFAULT_UA.test(headers[key] ?? \"\")) {\n\t\tdelete headers[key];\n\t\theaders[\"User-Agent\"] = userAgent;\n\t}\n}\n\n/**\n * Host allow-list matcher. Entries are exact hostnames (`api.example.com`)\n * or wildcard subdomains (`*.example.com`, which also matches `example.com`).\n */\nexport function hostMatches(\n\thostname: string,\n\tallow: readonly string[],\n): boolean {\n\tconst h = hostname.toLowerCase().replace(/\\.$/, \"\");\n\treturn allow.some((raw) => {\n\t\tconst a = raw.toLowerCase().replace(/\\.$/, \"\");\n\t\tif (a.startsWith(\"*.\")) {\n\t\t\tconst base = a.slice(2);\n\t\t\treturn h === base || h.endsWith(`.${base}`);\n\t\t}\n\t\treturn h === a;\n\t});\n}\n","/**\n * createSignedFetch — drop-in `fetch` wrapper that signs every outgoing request.\n *\n * Mirrors python/src/wbauth/adapters/httpx_auth.py. Stateless: closure over\n * `identity` only. Returns a function whose signature matches `typeof fetch`,\n * so user code can swap `fetch` for `signedFetch` without other changes.\n *\n * - Request-object inputs keep their `signal`, `redirect`, `credentials`,\n * `cache`, `keepalive`, `integrity` and `referrerPolicy`.\n * - Re-sending an already signed request (retry wrappers) produces exactly one\n * fresh signature — stale Signature* headers are replaced, never merged.\n * - **Redirects** (`redirect: \"follow\"`, the default) are followed here, not\n * by the underlying fetch, so each hop is re-signed for its own\n * `@authority`. Native fetch would otherwise replay hop 1's signature to the\n * redirect target. Method/body rewriting and cross-origin credential\n * stripping follow the WHATWG fetch spec.\n *\n * v1 limitation: streaming bodies are buffered (`req.clone().arrayBuffer()`)\n * so Content-Digest can be computed.\n */\n\nimport type { Identity } from \"../identity.js\";\nimport { sign } from \"../signer.js\";\nimport { applyUserAgent, ensureContentDigest } from \"./_utils.js\";\n\nexport interface SignedFetchOptions {\n\t/** Underlying fetch implementation. Defaults to `globalThis.fetch` at call time. */\n\tfetch?: typeof fetch;\n\t/** Maximum redirects followed (and re-signed). Default 20, like fetch. */\n\tmaxRedirects?: number;\n}\n\nconst REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);\n/** Request headers dropped when a redirect crosses origins (fetch spec §4.4). */\nconst CROSS_ORIGIN_STRIP = new Set([\n\t\"authorization\",\n\t\"cookie\",\n\t\"proxy-authorization\",\n]);\n/** Request-body headers dropped when a redirect turns the request into GET. */\nconst BODY_HEADERS = new Set([\n\t\"content-type\",\n\t\"content-length\",\n\t\"content-encoding\",\n\t\"content-language\",\n\t\"content-location\",\n\t\"content-digest\",\n]);\n\nfunction dropHeaders(headers: Record<string, string>, names: Set<string>) {\n\tfor (const k of Object.keys(headers)) {\n\t\tif (names.has(k.toLowerCase())) delete headers[k];\n\t}\n}\n\nexport function createSignedFetch(\n\tidentity: Identity,\n\toptions: SignedFetchOptions = {},\n): typeof fetch {\n\treturn async function signedFetch(\n\t\tinput: RequestInfo | URL,\n\t\tinit: RequestInit = {},\n\t): Promise<Response> {\n\t\tconst req = new Request(input, init);\n\t\tlet method = req.method;\n\t\tlet url = req.url;\n\n\t\tlet body: Uint8Array | null = null;\n\t\tif (req.body) {\n\t\t\tconst buf = await req.clone().arrayBuffer();\n\t\t\tbody = buf.byteLength > 0 ? new Uint8Array(buf) : null;\n\t\t}\n\n\t\tconst baseHeaders: Record<string, string> = {};\n\t\treq.headers.forEach((value, key) => {\n\t\t\tbaseHeaders[key] = value;\n\t\t});\n\n\t\tconst redirectMode = init.redirect ?? req.redirect;\n\t\tconst followHere = redirectMode === \"follow\";\n\t\tconst maxRedirects = options.maxRedirects ?? 20;\n\t\tconst doFetch = options.fetch ?? globalThis.fetch;\n\n\t\tfor (let hop = 0; ; hop++) {\n\t\t\tconst headers: Record<string, string> = { ...baseHeaders };\n\t\t\tensureContentDigest(method, headers, body);\n\t\t\tawait sign({ method, url, headers, body }, identity);\n\t\t\tapplyUserAgent(headers, identity.userAgent);\n\n\t\t\tconst outInit: RequestInit = {\n\t\t\t\t...init,\n\t\t\t\tmethod,\n\t\t\t\theaders,\n\t\t\t\tbody: (body ?? undefined) as BodyInit | undefined,\n\t\t\t\tsignal: init.signal ?? req.signal,\n\t\t\t\tredirect: followHere ? \"manual\" : redirectMode,\n\t\t\t\tcredentials: init.credentials ?? req.credentials,\n\t\t\t\tcache: init.cache ?? req.cache,\n\t\t\t\tkeepalive: init.keepalive ?? req.keepalive,\n\t\t\t};\n\t\t\tconst integrity = init.integrity ?? req.integrity;\n\t\t\tif (integrity) outInit.integrity = integrity;\n\t\t\tconst referrerPolicy = init.referrerPolicy ?? req.referrerPolicy;\n\t\t\tif (referrerPolicy) outInit.referrerPolicy = referrerPolicy;\n\n\t\t\tconst res = await doFetch(url, outInit);\n\t\t\tif (!followHere || !REDIRECT_STATUSES.has(res.status)) return res;\n\t\t\tconst location = res.headers.get(\"location\");\n\t\t\tif (location === null) return res;\n\t\t\tif (hop >= maxRedirects) {\n\t\t\t\tthrow new TypeError(`redirect count exceeded (${maxRedirects})`);\n\t\t\t}\n\t\t\tconst next = new URL(location, url);\n\t\t\tif (next.protocol !== \"http:\" && next.protocol !== \"https:\") {\n\t\t\t\tthrow new TypeError(`unsupported redirect scheme: ${next.protocol}`);\n\t\t\t}\n\t\t\tconst upper = method.toUpperCase();\n\t\t\tif (\n\t\t\t\t(res.status === 303 && upper !== \"GET\" && upper !== \"HEAD\") ||\n\t\t\t\t((res.status === 301 || res.status === 302) && upper === \"POST\")\n\t\t\t) {\n\t\t\t\tmethod = \"GET\";\n\t\t\t\tbody = null;\n\t\t\t\tdropHeaders(baseHeaders, BODY_HEADERS);\n\t\t\t}\n\t\t\tif (new URL(url).origin !== next.origin) {\n\t\t\t\tdropHeaders(baseHeaders, CROSS_ORIGIN_STRIP);\n\t\t\t}\n\t\t\tawait res.body?.cancel().catch(() => {});\n\t\t\turl = next.href;\n\t\t}\n\t};\n}\n","/**\n * applyTo — sign every matching request a Playwright page sends.\n *\n * Mirrors python/src/wbauth/adapters/playwright.py.\n *\n * Privacy: by default every http(s) request is signed, including third-party\n * analytics/CDN hosts, which reveals your agent identity to them. Pass\n * `hosts` to restrict signing to the sites you actually target:\n *\n * await applyTo(page, identity, { hosts: [\"example.com\", \"*.example.org\"] });\n *\n * Non-http(s) requests (data:, blob:, chrome-extension:) are never signed.\n * Note: Playwright disables the HTTP cache while a route is active.\n */\nimport type { Page, Request as PWRequest, Route } from \"playwright\";\nimport type { Identity } from \"../identity.js\";\nimport { sign } from \"../signer.js\";\nimport { applyUserAgent, ensureContentDigest, hostMatches } from \"./_utils.js\";\n\nexport interface ApplyToOptions {\n\t/** Only sign requests to these hosts (`example.com`, `*.example.com`). */\n\thosts?: readonly string[];\n\t/** Custom predicate; takes precedence over `hosts`. */\n\tshouldSign?: (url: URL) => boolean;\n\t/** Route pattern passed to `page.route`. Default `\"**\\/*\"`. */\n\tpattern?: string;\n}\n\nexport async function applyTo(\n\tpage: Page,\n\tidentity: Identity,\n\toptions: ApplyToOptions = {},\n): Promise<void> {\n\tconst pattern = options.pattern ?? \"**/*\";\n\tconst decide = (raw: string): boolean => {\n\t\tlet u: URL;\n\t\ttry {\n\t\t\tu = new URL(raw);\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t\tif (u.protocol !== \"https:\" && u.protocol !== \"http:\") return false;\n\t\tif (options.shouldSign) return options.shouldSign(u);\n\t\tif (options.hosts) return hostMatches(u.hostname, options.hosts);\n\t\treturn true;\n\t};\n\n\tawait page.route(pattern, async (route: Route, request: PWRequest) => {\n\t\tconst url = request.url();\n\t\tif (!decide(url)) {\n\t\t\tconst r = route as Route & { fallback?: () => Promise<void> };\n\t\t\tawait (r.fallback ? r.fallback() : route.continue());\n\t\t\treturn;\n\t\t}\n\t\tconst headers = await request.allHeaders();\n\t\tconst postData = request.postDataBuffer();\n\t\tconst body = postData ? new Uint8Array(postData) : null;\n\t\tensureContentDigest(request.method(), headers, body);\n\t\tawait sign({ method: request.method(), url, headers, body }, identity);\n\t\tapplyUserAgent(headers, identity.userAgent);\n\t\tawait route.continue({ headers });\n\t});\n}\n","/**\n * Identity, KeyPair, and key-file helpers for the wbauth TypeScript SDK.\n *\n * Mirrors python/src/wbauth/identity.py — same on-disk format (PKCS8\n * NoEncryption PEM at 0o600), same kid (RFC 7638 JWK thumbprint), same\n * multi-key rotation semantics (active + retiring), same REDACTED repr\n * (IDENT-08).\n *\n * Cross-language guarantee (D-60): the PEM file Python writes via\n * `Ed25519PrivateKey.private_bytes(PEM, PKCS8, NoEncryption)` is byte-equal\n * to what Node's `crypto.generateKeyPairSync('ed25519').privateKey.export({\n * format: 'pem', type: 'pkcs8' })` writes. Both runtimes can load each\n * other's keyfile and produce the same kid + signatures.\n *\n * Key implementation choice (Pitfall 1): PEM→JWK is done via Node 20+\n * stdlib `node:crypto.createPrivateKey(pem).export({format:'jwk'})`. We do\n * NOT use `jose` or any third-party JOSE library — the stdlib output is\n * verified byte-identical to what Python's cryptography library produces\n * for the same PKCS8 PEM (RESEARCH §2.2 live verification 2026-05-10).\n */\n\nimport { createPrivateKey, generateKeyPairSync } from \"node:crypto\";\nimport {\n\tcloseSync,\n\texistsSync,\n\tmkdirSync,\n\topenSync,\n\treadFileSync,\n\tstatSync,\n\twriteFileSync,\n} from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, resolve } from \"node:path\";\nimport type { Signer } from \"http-message-sig\";\nimport { signerFromJWK } from \"web-bot-auth/crypto\";\n\n// ---------- Module constants ----------\n\n/** Default path for the active signing key. Matches Python DEFAULT_KEY_PATH. */\nexport const DEFAULT_KEY_PATH = resolve(\n\thomedir(),\n\t\".config\",\n\t\"wbauth\",\n\t\"key.pem\",\n);\n\n/**\n * RFC 9421 Appendix B.1.4 publicly-known test key — NEVER use in production.\n * The private \"d\" component is published in the RFC and known to everyone.\n * `Identity.fromTestKey()` returns an Identity using these bytes.\n */\nconst TEST_KEY_JWK: JsonWebKey = {\n\tkty: \"OKP\",\n\tcrv: \"Ed25519\",\n\td: \"n4Ni-HpISpVObnQMW0wOhCKROaIKqKtW_2ZYb2p9KcU\",\n\tx: \"JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs\",\n};\n\n// ---------- Public types ----------\n\nexport interface IdentityOptions {\n\t/** Stable HTTPS URL where this agent's JWKS is published. Must start with `https://`. */\n\tsignatureAgentUrl: string;\n\t/** Optional User-Agent string adapters inject when caller did not supply one. */\n\tuserAgent?: string;\n}\n\nexport interface KeyPair {\n\t/** Private JWK (includes \"d\") — used to mint the signer. Never logged. */\n\tprivateJwk: JsonWebKey;\n\t/** Public JWK ({kty, crv, x, kid}) — what exportJwks returns per key. */\n\tpublicJwk: JsonWebKey & { kid: string };\n\t/** RFC 7638 base64url-no-pad thumbprint of the public key. */\n\tkid: string;\n\t/** Pre-resolved web-bot-auth Signer (caches the imported CryptoKey). */\n\tsigner: Signer;\n}\n\n// ---------- Identity class ----------\n\nexport class Identity {\n\tprivate _active: KeyPair;\n\tprivate _retiring: KeyPair | null;\n\treadonly signatureAgentUrl: string;\n\treadonly userAgent: string | undefined;\n\n\tprivate constructor(\n\t\tactive: KeyPair,\n\t\topts: IdentityOptions,\n\t\tretiring: KeyPair | null = null,\n\t) {\n\t\tif (!opts.signatureAgentUrl.startsWith(\"https://\")) {\n\t\t\tthrow new Error(\n\t\t\t\t`signatureAgentUrl must be https://, got: ${opts.signatureAgentUrl}`,\n\t\t\t);\n\t\t}\n\t\tthis._active = active;\n\t\tthis._retiring = retiring;\n\t\tthis.signatureAgentUrl = opts.signatureAgentUrl;\n\t\tthis.userAgent = opts.userAgent;\n\t}\n\n\t// ---------- Public read-only API ----------\n\n\t/** The active key's kid (used as `keyid=` param in Signature-Input). */\n\tget kid(): string {\n\t\treturn this._active.kid;\n\t}\n\n\t/** Module-internal: returns the resolved Signer for sign() to use. */\n\t_signer(): Signer {\n\t\treturn this._active.signer;\n\t}\n\n\t/**\n\t * Export the public JWKS document.\n\t *\n\t * Returns `{keys: [active_jwk]}` if no retiring key is held, or\n\t * `{keys: [active_jwk, retiring_jwk]}` after a rotation. Active is always\n\t * first. Verifiers select by `kid`. Mirror of Python `export_jwks`.\n\t */\n\texportJwks(): { keys: Array<JsonWebKey & { kid: string }> } {\n\t\tconst keys: Array<JsonWebKey & { kid: string }> = [this._active.publicJwk];\n\t\tif (this._retiring) keys.push(this._retiring.publicJwk);\n\t\treturn { keys };\n\t}\n\n\t/**\n\t * IDENT-07: generate a new active key, demote current to retiring.\n\t *\n\t * Returns a NEW Identity (immutable update — the old Identity is unchanged).\n\t * Any previously-retiring key is dropped (only one overlap window).\n\t */\n\tasync rotate(newPath: string = DEFAULT_KEY_PATH): Promise<Identity> {\n\t\tconst newPair = await generateKeypairTo(expandUser(newPath));\n\t\treturn new Identity(\n\t\t\tnewPair,\n\t\t\t{ signatureAgentUrl: this.signatureAgentUrl, userAgent: this.userAgent },\n\t\t\tthis._active,\n\t\t);\n\t}\n\n\t// ---------- Constructors ----------\n\n\t/**\n\t * Primary entry point. Loads `path` if it exists, generates a fresh key\n\t * there otherwise. The on-disk file is PKCS8 NoEncryption PEM at 0o600,\n\t * byte-equal to what Python's `Identity.load_or_generate` writes (D-60).\n\t */\n\tstatic async loadOrGenerate(\n\t\tpath: string = DEFAULT_KEY_PATH,\n\t\topts: IdentityOptions,\n\t): Promise<Identity> {\n\t\tconst resolved = expandUser(path);\n\t\tconst pair = existsSync(resolved)\n\t\t\t? await loadKeypair(resolved)\n\t\t\t: await generateKeypairTo(resolved);\n\t\treturn new Identity(pair, opts);\n\t}\n\n\t/**\n\t * Construct from the RFC 9421 Appendix B.1.4 publicly-known test key.\n\t * For tests + Cloudflare debug smoke checks. NEVER use in production.\n\t * Does NOT write any file to disk.\n\t */\n\tstatic async fromTestKey(\n\t\tsignatureAgentUrl: string,\n\t\topts: Omit<IdentityOptions, \"signatureAgentUrl\"> = {},\n\t): Promise<Identity> {\n\t\tconst pair = await keyPairFromJwk(TEST_KEY_JWK);\n\t\treturn new Identity(pair, { ...opts, signatureAgentUrl });\n\t}\n\n\t// ---------- Redaction guarantees (IDENT-08) ----------\n\n\ttoString(): string {\n\t\treturn `<Identity REDACTED kid='${this.kid}' sig_agent='${this.signatureAgentUrl}'>`;\n\t}\n\n\t/** util.inspect / console.log render the same REDACTED form (IDENT-08). */\n\t[Symbol.for(\"nodejs.util.inspect.custom\")](): string {\n\t\treturn this.toString();\n\t}\n}\n\n// ---------- Helpers (module-private) ----------\n\nfunction expandUser(path: string): string {\n\treturn resolve(path.startsWith(\"~\") ? path.replace(/^~/, homedir()) : path);\n}\n\nasync function keyPairFromJwk(privateJwk: JsonWebKey): Promise<KeyPair> {\n\tconst signer = await signerFromJWK(privateJwk);\n\t// signerFromJWK pre-computes the kid (RFC 7638 thumbprint) into signer.keyid.\n\t// Reuse it instead of calling jwkToKeyID separately (avoids the 3-arg helper).\n\tconst kid = signer.keyid;\n\tconst publicJwk: JsonWebKey & { kid: string } = {\n\t\tkty: privateJwk.kty,\n\t\tcrv: privateJwk.crv,\n\t\tx: privateJwk.x,\n\t\tkid,\n\t};\n\treturn { privateJwk, publicJwk, kid, signer };\n}\n\nasync function loadKeypair(path: string): Promise<KeyPair> {\n\t// POSIX permission check (mirror Python — refuse wider-than-0o600).\n\t// Pitfall 8: Windows lacks POSIX mode bits → emit warning, do not enforce.\n\tif (process.platform !== \"win32\") {\n\t\tconst mode = statSync(path).mode & 0o777;\n\t\tif ((mode & 0o077) !== 0) {\n\t\t\tthrow new Error(\n\t\t\t\t`Key file ${path} has mode 0o${mode.toString(8)}; expected 0o600. Fix: chmod 600 ${path}`,\n\t\t\t);\n\t\t}\n\t} else {\n\t\tprocess.emitWarning(\n\t\t\t`Windows detected — file permissions on ${path} cannot be enforced via POSIX mode. Use NTFS ACLs or store the key in a per-user secrets vault.`,\n\t\t);\n\t}\n\tconst pem = readFileSync(path, \"utf8\");\n\tconst jwk = createPrivateKey(pem).export({ format: \"jwk\" }) as JsonWebKey;\n\tif (jwk.kty !== \"OKP\" || jwk.crv !== \"Ed25519\") {\n\t\tthrow new TypeError(`Expected Ed25519, got kty=${jwk.kty}, crv=${jwk.crv}`);\n\t}\n\treturn keyPairFromJwk(jwk);\n}\n\nasync function generateKeypairTo(path: string): Promise<KeyPair> {\n\tconst dir = dirname(path);\n\tmkdirSync(dir, { recursive: true });\n\tif (existsSync(path)) {\n\t\t// Belt + suspenders: O_EXCL would catch this too, but raising here gives\n\t\t// a cleaner error than the low-level EEXIST.\n\t\tthrow new Error(`Key already exists at ${path}; refuse overwrite`);\n\t}\n\tconst { privateKey } = generateKeyPairSync(\"ed25519\");\n\tconst pem = privateKey.export({ format: \"pem\", type: \"pkcs8\" }) as string;\n\t// Race-free 0o600 creation: openSync with \"wx\" = O_WRONLY|O_CREAT|O_EXCL.\n\t// Mirrors Python `os.open(path, O_WRONLY|O_CREAT|O_EXCL, 0o600)`.\n\tconst fd = openSync(path, \"wx\", 0o600);\n\ttry {\n\t\twriteFileSync(fd, pem);\n\t} finally {\n\t\tcloseSync(fd);\n\t}\n\tconst jwk = privateKey.export({ format: \"jwk\" }) as JsonWebKey;\n\treturn keyPairFromJwk(jwk);\n}\n"],"mappings":"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","names":["import_node_crypto"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -97,6 +97,11 @@ declare class Identity {
|
|
|
97
97
|
* `cache`, `keepalive`, `integrity` and `referrerPolicy`.
|
|
98
98
|
* - Re-sending an already signed request (retry wrappers) produces exactly one
|
|
99
99
|
* fresh signature — stale Signature* headers are replaced, never merged.
|
|
100
|
+
* - **Redirects** (`redirect: "follow"`, the default) are followed here, not
|
|
101
|
+
* by the underlying fetch, so each hop is re-signed for its own
|
|
102
|
+
* `@authority`. Native fetch would otherwise replay hop 1's signature to the
|
|
103
|
+
* redirect target. Method/body rewriting and cross-origin credential
|
|
104
|
+
* stripping follow the WHATWG fetch spec.
|
|
100
105
|
*
|
|
101
106
|
* v1 limitation: streaming bodies are buffered (`req.clone().arrayBuffer()`)
|
|
102
107
|
* so Content-Digest can be computed.
|
|
@@ -105,6 +110,8 @@ declare class Identity {
|
|
|
105
110
|
interface SignedFetchOptions {
|
|
106
111
|
/** Underlying fetch implementation. Defaults to `globalThis.fetch` at call time. */
|
|
107
112
|
fetch?: typeof fetch;
|
|
113
|
+
/** Maximum redirects followed (and re-signed). Default 20, like fetch. */
|
|
114
|
+
maxRedirects?: number;
|
|
108
115
|
}
|
|
109
116
|
declare function createSignedFetch(identity: Identity, options?: SignedFetchOptions): typeof fetch;
|
|
110
117
|
|
|
@@ -199,6 +206,6 @@ declare function sign(request: NormalizedRequest, identity: Identity, opts?: Sig
|
|
|
199
206
|
* const res = await sf("https://api.example.com/data");
|
|
200
207
|
*/
|
|
201
208
|
|
|
202
|
-
declare const VERSION = "0.
|
|
209
|
+
declare const VERSION = "0.3.0";
|
|
203
210
|
|
|
204
211
|
export { type ApplyToOptions, DEFAULT_KEY_PATH, Identity, type IdentityOptions, type KeyPair, type NormalizedRequest, type SignOptions, type SignatureHeaders, type SignedFetchOptions, VERSION, applyTo, createSignedFetch, sign };
|
package/dist/index.d.ts
CHANGED
|
@@ -97,6 +97,11 @@ declare class Identity {
|
|
|
97
97
|
* `cache`, `keepalive`, `integrity` and `referrerPolicy`.
|
|
98
98
|
* - Re-sending an already signed request (retry wrappers) produces exactly one
|
|
99
99
|
* fresh signature — stale Signature* headers are replaced, never merged.
|
|
100
|
+
* - **Redirects** (`redirect: "follow"`, the default) are followed here, not
|
|
101
|
+
* by the underlying fetch, so each hop is re-signed for its own
|
|
102
|
+
* `@authority`. Native fetch would otherwise replay hop 1's signature to the
|
|
103
|
+
* redirect target. Method/body rewriting and cross-origin credential
|
|
104
|
+
* stripping follow the WHATWG fetch spec.
|
|
100
105
|
*
|
|
101
106
|
* v1 limitation: streaming bodies are buffered (`req.clone().arrayBuffer()`)
|
|
102
107
|
* so Content-Digest can be computed.
|
|
@@ -105,6 +110,8 @@ declare class Identity {
|
|
|
105
110
|
interface SignedFetchOptions {
|
|
106
111
|
/** Underlying fetch implementation. Defaults to `globalThis.fetch` at call time. */
|
|
107
112
|
fetch?: typeof fetch;
|
|
113
|
+
/** Maximum redirects followed (and re-signed). Default 20, like fetch. */
|
|
114
|
+
maxRedirects?: number;
|
|
108
115
|
}
|
|
109
116
|
declare function createSignedFetch(identity: Identity, options?: SignedFetchOptions): typeof fetch;
|
|
110
117
|
|
|
@@ -199,6 +206,6 @@ declare function sign(request: NormalizedRequest, identity: Identity, opts?: Sig
|
|
|
199
206
|
* const res = await sf("https://api.example.com/data");
|
|
200
207
|
*/
|
|
201
208
|
|
|
202
|
-
declare const VERSION = "0.
|
|
209
|
+
declare const VERSION = "0.3.0";
|
|
203
210
|
|
|
204
211
|
export { type ApplyToOptions, DEFAULT_KEY_PATH, Identity, type IdentityOptions, type KeyPair, type NormalizedRequest, type SignOptions, type SignatureHeaders, type SignedFetchOptions, VERSION, applyTo, createSignedFetch, sign };
|
package/dist/index.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import { signatureHeaders } from "web-bot-auth";
|
|
3
3
|
var DEFAULT_LABEL = "sig1";
|
|
4
4
|
var DEFAULT_EXPIRES_SECONDS = 60;
|
|
5
|
-
var
|
|
6
|
-
function componentsFor(
|
|
5
|
+
var MAX_EXPIRES_SECONDS = 3600;
|
|
6
|
+
function componentsFor(_method, hasBody) {
|
|
7
7
|
const base = ["@authority", "signature-agent"];
|
|
8
|
-
if (hasBody
|
|
8
|
+
if (hasBody) {
|
|
9
9
|
base.push("content-digest");
|
|
10
10
|
}
|
|
11
11
|
return base;
|
|
@@ -27,9 +27,9 @@ async function sign(request, identity, opts = {}) {
|
|
|
27
27
|
);
|
|
28
28
|
}
|
|
29
29
|
const expiresAfterSeconds = opts.expiresAfterSeconds ?? DEFAULT_EXPIRES_SECONDS;
|
|
30
|
-
if (!Number.isFinite(expiresAfterSeconds) || expiresAfterSeconds <= 0) {
|
|
30
|
+
if (typeof expiresAfterSeconds !== "number" || !Number.isFinite(expiresAfterSeconds) || expiresAfterSeconds <= 0 || expiresAfterSeconds > MAX_EXPIRES_SECONDS) {
|
|
31
31
|
throw new RangeError(
|
|
32
|
-
`expiresAfterSeconds must be a
|
|
32
|
+
`expiresAfterSeconds must be a number in (0, ${MAX_EXPIRES_SECONDS}], got: ${expiresAfterSeconds}`
|
|
33
33
|
);
|
|
34
34
|
}
|
|
35
35
|
removeHeaders(request.headers, RESIGN_HEADERS);
|
|
@@ -48,7 +48,10 @@ async function sign(request, identity, opts = {}) {
|
|
|
48
48
|
const headers = new Headers();
|
|
49
49
|
for (const [k, v] of Object.entries(request.headers)) headers.set(k, v);
|
|
50
50
|
const init = { method: request.method, headers };
|
|
51
|
-
|
|
51
|
+
const m = request.method.toUpperCase();
|
|
52
|
+
if (hasBody && m !== "GET" && m !== "HEAD") {
|
|
53
|
+
init.body = request.body;
|
|
54
|
+
}
|
|
52
55
|
const req = new Request(request.url, init);
|
|
53
56
|
const params = {
|
|
54
57
|
created,
|
|
@@ -69,7 +72,6 @@ async function sign(request, identity, opts = {}) {
|
|
|
69
72
|
|
|
70
73
|
// src/adapters/_utils.ts
|
|
71
74
|
import { createHash } from "crypto";
|
|
72
|
-
var DIGEST_METHODS2 = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
|
|
73
75
|
var LIBRARY_DEFAULT_UA = /^(node|undici|node-fetch)(\/|$)/i;
|
|
74
76
|
function findHeader(headers, name) {
|
|
75
77
|
const n = name.toLowerCase();
|
|
@@ -77,7 +79,7 @@ function findHeader(headers, name) {
|
|
|
77
79
|
}
|
|
78
80
|
function ensureContentDigest(method, headers, body) {
|
|
79
81
|
if (!body || body.byteLength === 0) return;
|
|
80
|
-
|
|
82
|
+
void method;
|
|
81
83
|
if (findHeader(headers, "content-digest")) return;
|
|
82
84
|
const b64 = createHash("sha256").update(body).digest("base64");
|
|
83
85
|
headers["Content-Digest"] = `sha-256=:${b64}:`;
|
|
@@ -107,41 +109,87 @@ function hostMatches(hostname, allow) {
|
|
|
107
109
|
}
|
|
108
110
|
|
|
109
111
|
// src/adapters/fetch.ts
|
|
112
|
+
var REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
113
|
+
var CROSS_ORIGIN_STRIP = /* @__PURE__ */ new Set([
|
|
114
|
+
"authorization",
|
|
115
|
+
"cookie",
|
|
116
|
+
"proxy-authorization"
|
|
117
|
+
]);
|
|
118
|
+
var BODY_HEADERS = /* @__PURE__ */ new Set([
|
|
119
|
+
"content-type",
|
|
120
|
+
"content-length",
|
|
121
|
+
"content-encoding",
|
|
122
|
+
"content-language",
|
|
123
|
+
"content-location",
|
|
124
|
+
"content-digest"
|
|
125
|
+
]);
|
|
126
|
+
function dropHeaders(headers, names) {
|
|
127
|
+
for (const k of Object.keys(headers)) {
|
|
128
|
+
if (names.has(k.toLowerCase())) delete headers[k];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
110
131
|
function createSignedFetch(identity, options = {}) {
|
|
111
132
|
return async function signedFetch(input, init = {}) {
|
|
112
133
|
const req = new Request(input, init);
|
|
113
|
-
|
|
114
|
-
|
|
134
|
+
let method = req.method;
|
|
135
|
+
let url = req.url;
|
|
115
136
|
let body = null;
|
|
116
137
|
if (req.body) {
|
|
117
138
|
const buf = await req.clone().arrayBuffer();
|
|
118
139
|
body = buf.byteLength > 0 ? new Uint8Array(buf) : null;
|
|
119
140
|
}
|
|
120
|
-
const
|
|
141
|
+
const baseHeaders = {};
|
|
121
142
|
req.headers.forEach((value, key) => {
|
|
122
|
-
|
|
143
|
+
baseHeaders[key] = value;
|
|
123
144
|
});
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const outInit = {
|
|
128
|
-
...init,
|
|
129
|
-
method,
|
|
130
|
-
headers,
|
|
131
|
-
// Uint8Array is a valid BodyInit at runtime; DOM typings disagree.
|
|
132
|
-
body: body ?? void 0,
|
|
133
|
-
signal: init.signal ?? req.signal,
|
|
134
|
-
redirect: init.redirect ?? req.redirect,
|
|
135
|
-
credentials: init.credentials ?? req.credentials,
|
|
136
|
-
cache: init.cache ?? req.cache,
|
|
137
|
-
keepalive: init.keepalive ?? req.keepalive
|
|
138
|
-
};
|
|
139
|
-
const integrity = init.integrity ?? req.integrity;
|
|
140
|
-
if (integrity) outInit.integrity = integrity;
|
|
141
|
-
const referrerPolicy = init.referrerPolicy ?? req.referrerPolicy;
|
|
142
|
-
if (referrerPolicy) outInit.referrerPolicy = referrerPolicy;
|
|
145
|
+
const redirectMode = init.redirect ?? req.redirect;
|
|
146
|
+
const followHere = redirectMode === "follow";
|
|
147
|
+
const maxRedirects = options.maxRedirects ?? 20;
|
|
143
148
|
const doFetch = options.fetch ?? globalThis.fetch;
|
|
144
|
-
|
|
149
|
+
for (let hop = 0; ; hop++) {
|
|
150
|
+
const headers = { ...baseHeaders };
|
|
151
|
+
ensureContentDigest(method, headers, body);
|
|
152
|
+
await sign({ method, url, headers, body }, identity);
|
|
153
|
+
applyUserAgent(headers, identity.userAgent);
|
|
154
|
+
const outInit = {
|
|
155
|
+
...init,
|
|
156
|
+
method,
|
|
157
|
+
headers,
|
|
158
|
+
body: body ?? void 0,
|
|
159
|
+
signal: init.signal ?? req.signal,
|
|
160
|
+
redirect: followHere ? "manual" : redirectMode,
|
|
161
|
+
credentials: init.credentials ?? req.credentials,
|
|
162
|
+
cache: init.cache ?? req.cache,
|
|
163
|
+
keepalive: init.keepalive ?? req.keepalive
|
|
164
|
+
};
|
|
165
|
+
const integrity = init.integrity ?? req.integrity;
|
|
166
|
+
if (integrity) outInit.integrity = integrity;
|
|
167
|
+
const referrerPolicy = init.referrerPolicy ?? req.referrerPolicy;
|
|
168
|
+
if (referrerPolicy) outInit.referrerPolicy = referrerPolicy;
|
|
169
|
+
const res = await doFetch(url, outInit);
|
|
170
|
+
if (!followHere || !REDIRECT_STATUSES.has(res.status)) return res;
|
|
171
|
+
const location = res.headers.get("location");
|
|
172
|
+
if (location === null) return res;
|
|
173
|
+
if (hop >= maxRedirects) {
|
|
174
|
+
throw new TypeError(`redirect count exceeded (${maxRedirects})`);
|
|
175
|
+
}
|
|
176
|
+
const next = new URL(location, url);
|
|
177
|
+
if (next.protocol !== "http:" && next.protocol !== "https:") {
|
|
178
|
+
throw new TypeError(`unsupported redirect scheme: ${next.protocol}`);
|
|
179
|
+
}
|
|
180
|
+
const upper = method.toUpperCase();
|
|
181
|
+
if (res.status === 303 && upper !== "GET" && upper !== "HEAD" || (res.status === 301 || res.status === 302) && upper === "POST") {
|
|
182
|
+
method = "GET";
|
|
183
|
+
body = null;
|
|
184
|
+
dropHeaders(baseHeaders, BODY_HEADERS);
|
|
185
|
+
}
|
|
186
|
+
if (new URL(url).origin !== next.origin) {
|
|
187
|
+
dropHeaders(baseHeaders, CROSS_ORIGIN_STRIP);
|
|
188
|
+
}
|
|
189
|
+
await res.body?.cancel().catch(() => {
|
|
190
|
+
});
|
|
191
|
+
url = next.href;
|
|
192
|
+
}
|
|
145
193
|
};
|
|
146
194
|
}
|
|
147
195
|
|
|
@@ -336,7 +384,7 @@ async function generateKeypairTo(path) {
|
|
|
336
384
|
}
|
|
337
385
|
|
|
338
386
|
// src/index.ts
|
|
339
|
-
var VERSION = "0.
|
|
387
|
+
var VERSION = "0.3.0";
|
|
340
388
|
export {
|
|
341
389
|
DEFAULT_KEY_PATH,
|
|
342
390
|
Identity,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/signer.ts","../src/adapters/_utils.ts","../src/adapters/fetch.ts","../src/adapters/playwright.ts","../src/identity.ts","../src/index.ts"],"sourcesContent":["/**\n * Pure-function signer for Web Bot Auth (RFC 9421).\n *\n * Mirrors python/src/wbauth/signer.py — same defaults (Ed25519, tag=\n * \"web-bot-auth\", expires=created+60s, covered components [\"@authority\",\n * \"signature-agent\"] + \"content-digest\" for bodies).\n *\n * The function delegates all RFC 9421 mechanics (signing-base canonicalization,\n * structured-field encoding, Ed25519 signing) to `web-bot-auth` 0.1.3's\n * `signatureHeaders`. We only own the Web Bot Auth profile glue: pre-setting\n * Signature-Agent (Pitfall 6), picking covered components, defaulting expires.\n */\nimport { type SignatureParams, signatureHeaders } from \"web-bot-auth\";\nimport type { Identity } from \"./identity.js\";\nimport type { NormalizedRequest } from \"./normalized-request.js\";\n\n// ---------- Module constants (HARD-CODED — typos = silent verifier reject) ----------\n\n/** IETF Web Bot Auth profile mandate. NEVER change. Pitfall 6. */\nconst WEB_BOT_AUTH_TAG = \"web-bot-auth\";\n/** Canonical Signature label. Most verifier examples assume \"sig1\". */\nconst DEFAULT_LABEL = \"sig1\";\n/** Default expires window. 60s leaves NTP-skew + network-latency headroom. */\nconst DEFAULT_EXPIRES_SECONDS = 60;\n/** Methods that may carry a body and therefore want content-digest covered. */\nconst DIGEST_METHODS = new Set([\"POST\", \"PUT\", \"PATCH\"]);\n// WEB_BOT_AUTH_TAG is enforced internally by web-bot-auth's signatureHeaders;\n// we keep the constant for greppability of the profile invariant.\nvoid WEB_BOT_AUTH_TAG;\n\n// ---------- Public types ----------\n\nexport interface SignatureHeaders {\n\tsignature: string;\n\tsignatureInput: string;\n\tsignatureAgent: string;\n}\n\nexport interface SignOptions {\n\t/** Signing timestamp. Defaults to `new Date()`. Override for vector reproducibility. */\n\tcreated?: Date;\n\t/** Validity window in seconds. Defaults to 60. */\n\texpiresAfterSeconds?: number;\n\t/** Anti-replay nonce. Defaults to web-bot-auth's `generateNonce()` (64-byte b64). */\n\tnonce?: string;\n\t/** Signature label (the `sig1=` prefix). Defaults to \"sig1\". */\n\tlabel?: string;\n}\n\n// ---------- Components selector (mirror Python `_components_for`) ----------\n\nfunction componentsFor(method: string, hasBody: boolean): string[] {\n\tconst base = [\"@authority\", \"signature-agent\"];\n\tif (hasBody && DIGEST_METHODS.has(method.toUpperCase())) {\n\t\tbase.push(\"content-digest\");\n\t}\n\treturn base;\n}\n\n/** Headers regenerated on every sign() call. */\nconst RESIGN_HEADERS = [\"signature\", \"signature-input\", \"signature-agent\"];\n\nfunction hasHeader(headers: Record<string, string>, name: string): boolean {\n\tconst n = name.toLowerCase();\n\treturn Object.keys(headers).some((k) => k.toLowerCase() === n);\n}\n\nfunction removeHeaders(headers: Record<string, string>, names: string[]): void {\n\tfor (const k of Object.keys(headers)) {\n\t\tif (names.includes(k.toLowerCase())) delete headers[k];\n\t}\n}\n\n// ---------- Public API ----------\n\n/**\n * Sign a NormalizedRequest with the Web Bot Auth profile.\n *\n * Mutates `request.headers` in place: writes Signature-Agent BEFORE building\n * the signing base (Pitfall 6 — without pre-set, web-bot-auth defaults to\n * `[\"@authority\"]` only and silently breaks byte-equality with the Python\n * signer). After signatureHeaders returns, also writes Signature and\n * Signature-Input back into `request.headers`.\n *\n * @returns the same three values as a typed dataclass for callers that prefer\n * a return value over header inspection.\n */\nexport async function sign(\n\trequest: NormalizedRequest,\n\tidentity: Identity,\n\topts: SignOptions = {},\n): Promise<SignatureHeaders> {\n\t// 1. Defensive https:// check — Identity ctor enforces it too, but a future\n\t// bypassed ctor would otherwise leak http://. Pitfall 1.\n\tif (!identity.signatureAgentUrl.startsWith(\"https://\")) {\n\t\tthrow new Error(\n\t\t\t`signatureAgentUrl must be https://, got: ${identity.signatureAgentUrl}`,\n\t\t);\n\t}\n\n\t// 2. Validate options up-front (parity with Python signer).\n\tconst expiresAfterSeconds =\n\t\topts.expiresAfterSeconds ?? DEFAULT_EXPIRES_SECONDS;\n\tif (!Number.isFinite(expiresAfterSeconds) || expiresAfterSeconds <= 0) {\n\t\tthrow new RangeError(\n\t\t\t`expiresAfterSeconds must be a positive number, got: ${expiresAfterSeconds}`,\n\t\t);\n\t}\n\n\t// 3. Drop stale signature headers in ANY casing. A retried / redirected\n\t// request still carries the previous hop's values; leaving them in place\n\t// produces merged `sig1=..., sig1=...` headers that every verifier rejects.\n\tremoveHeaders(request.headers, RESIGN_HEADERS);\n\n\t// 4. Set Signature-Agent header (RFC 8941 string in double quotes).\n\t// CRITICAL: must be set BEFORE signatureHeaders inspects the message,\n\t// otherwise web-bot-auth picks REQUEST_COMPONENTS_WITHOUT_SIGNATURE_AGENT.\n\tconst signatureAgentHeader = `\"${identity.signatureAgentUrl}\"`;\n\trequest.headers[\"Signature-Agent\"] = signatureAgentHeader;\n\n\t// 5. Defaults.\n\tconst created = opts.created ?? new Date();\n\tconst expires = new Date(created.getTime() + expiresAfterSeconds * 1000);\n\tconst label = opts.label ?? DEFAULT_LABEL;\n\n\t// 6. Components based on body presence.\n\tconst hasBody =\n\t\trequest.body !== null &&\n\t\trequest.body !== undefined &&\n\t\trequest.body.byteLength > 0;\n\tconst components = componentsFor(request.method, hasBody);\n\tif (\n\t\tcomponents.includes(\"content-digest\") &&\n\t\t!hasHeader(request.headers, \"content-digest\")\n\t) {\n\t\t// Python raises here too. Signing a missing header would yield a\n\t\t// signature that no verifier can ever validate.\n\t\tthrow new Error(\n\t\t\t'Covered header field \"content-digest\" not found in the message. ' +\n\t\t\t\t\"Call ensureContentDigest() or use createSignedFetch()/applyTo(), which add it automatically.\",\n\t\t);\n\t}\n\n\t// 7. Build a Web `Request` for web-bot-auth's `signatureHeaders`.\n\tconst headers = new Headers();\n\tfor (const [k, v] of Object.entries(request.headers)) headers.set(k, v);\n\tconst init: RequestInit = { method: request.method, headers };\n\t// Cast: Uint8Array is a valid BodyInit at runtime (web fetch spec) but DOM\n\t// typing wants ArrayBufferView/string/Blob etc. The cast is safe.\n\tif (hasBody) init.body = request.body as unknown as BodyInit;\n\tconst req = new Request(request.url, init);\n\n\t// 8. Delegate to web-bot-auth. The signer is the active key's pre-resolved\n\t// Signer (cached in Identity at construction time).\n\tconst params: SignatureParams = {\n\t\tcreated,\n\t\texpires,\n\t\tkey: label,\n\t\tcomponents,\n\t\t...(opts.nonce !== undefined ? { nonce: opts.nonce } : {}),\n\t};\n\tconst result = await signatureHeaders(req, identity._signer(), params);\n\n\t// 9. Mutate request.headers (mirror Python signer behavior).\n\trequest.headers.Signature = result.Signature;\n\trequest.headers[\"Signature-Input\"] = result[\"Signature-Input\"];\n\n\treturn {\n\t\tsignature: result.Signature,\n\t\tsignatureInput: result[\"Signature-Input\"],\n\t\tsignatureAgent: signatureAgentHeader,\n\t};\n}\n","/**\n * Shared adapter helpers (internal — not part of the public surface).\n *\n * Mirror python/src/wbauth/adapters/_utils.py. The signer auto-includes\n * `content-digest` in covered components for POST/PUT/PATCH with a body;\n * adapters call ensureContentDigest() before sign() so that precondition\n * is met. RFC 9530 sha-256 in structured-fields form (`sha-256=:<b64>:`).\n */\nimport { createHash } from \"node:crypto\";\n\nconst DIGEST_METHODS = new Set([\"POST\", \"PUT\", \"PATCH\"]);\n\n/** Default UAs injected by HTTP stacks; an Identity UA should replace them. */\nconst LIBRARY_DEFAULT_UA = /^(node|undici|node-fetch)(\\/|$)/i;\n\nfunction findHeader(\n\theaders: Record<string, string>,\n\tname: string,\n): string | undefined {\n\tconst n = name.toLowerCase();\n\treturn Object.keys(headers).find((k) => k.toLowerCase() === n);\n}\n\n/**\n * Mutate `headers` in place to add Content-Digest when warranted. No-op if:\n * - body is null/empty,\n * - method is not POST/PUT/PATCH,\n * - the request already carries a Content-Digest header (case-insensitive).\n */\nexport function ensureContentDigest(\n\tmethod: string,\n\theaders: Record<string, string>,\n\tbody: Uint8Array | null,\n): void {\n\tif (!body || body.byteLength === 0) return;\n\tif (!DIGEST_METHODS.has(method.toUpperCase())) return;\n\tif (findHeader(headers, \"content-digest\")) return;\n\tconst b64 = createHash(\"sha256\").update(body).digest(\"base64\");\n\theaders[\"Content-Digest\"] = `sha-256=:${b64}:`;\n}\n\n/**\n * Apply the Identity's User-Agent unless the caller explicitly set a\n * non-library UA. Library defaults (node, undici) are replaced.\n */\nexport function applyUserAgent(\n\theaders: Record<string, string>,\n\tuserAgent: string | undefined,\n): void {\n\tif (!userAgent) return;\n\tconst key = findHeader(headers, \"user-agent\");\n\tif (key === undefined) {\n\t\theaders[\"User-Agent\"] = userAgent;\n\t\treturn;\n\t}\n\tif (LIBRARY_DEFAULT_UA.test(headers[key] ?? \"\")) {\n\t\tdelete headers[key];\n\t\theaders[\"User-Agent\"] = userAgent;\n\t}\n}\n\n/**\n * Host allow-list matcher. Entries are exact hostnames (`api.example.com`)\n * or wildcard subdomains (`*.example.com`, which also matches `example.com`).\n */\nexport function hostMatches(\n\thostname: string,\n\tallow: readonly string[],\n): boolean {\n\tconst h = hostname.toLowerCase().replace(/\\.$/, \"\");\n\treturn allow.some((raw) => {\n\t\tconst a = raw.toLowerCase().replace(/\\.$/, \"\");\n\t\tif (a.startsWith(\"*.\")) {\n\t\t\tconst base = a.slice(2);\n\t\t\treturn h === base || h.endsWith(`.${base}`);\n\t\t}\n\t\treturn h === a;\n\t});\n}\n","/**\n * createSignedFetch — drop-in `fetch` wrapper that signs every outgoing request.\n *\n * Mirrors python/src/wbauth/adapters/httpx_auth.py. Stateless: closure over\n * `identity` only. Returns a function whose signature matches `typeof fetch`,\n * so user code can swap `fetch` for `signedFetch` without other changes.\n *\n * - Request-object inputs keep their `signal`, `redirect`, `credentials`,\n * `cache`, `keepalive`, `integrity` and `referrerPolicy`.\n * - Re-sending an already signed request (retry wrappers) produces exactly one\n * fresh signature — stale Signature* headers are replaced, never merged.\n *\n * v1 limitation: streaming bodies are buffered (`req.clone().arrayBuffer()`)\n * so Content-Digest can be computed.\n */\n\nimport type { Identity } from \"../identity.js\";\nimport { sign } from \"../signer.js\";\nimport { applyUserAgent, ensureContentDigest } from \"./_utils.js\";\n\nexport interface SignedFetchOptions {\n\t/** Underlying fetch implementation. Defaults to `globalThis.fetch` at call time. */\n\tfetch?: typeof fetch;\n}\n\nexport function createSignedFetch(\n\tidentity: Identity,\n\toptions: SignedFetchOptions = {},\n): typeof fetch {\n\treturn async function signedFetch(\n\t\tinput: RequestInfo | URL,\n\t\tinit: RequestInit = {},\n\t): Promise<Response> {\n\t\tconst req = new Request(input, init);\n\t\tconst method = req.method;\n\t\tconst url = req.url;\n\n\t\t// Read body once — Request consumes its body stream; clone first.\n\t\tlet body: Uint8Array | null = null;\n\t\tif (req.body) {\n\t\t\tconst buf = await req.clone().arrayBuffer();\n\t\t\tbody = buf.byteLength > 0 ? new Uint8Array(buf) : null;\n\t\t}\n\n\t\t// Mutable header dict (Headers already lower-cases + de-duplicates).\n\t\tconst headers: Record<string, string> = {};\n\t\treq.headers.forEach((value, key) => {\n\t\t\theaders[key] = value;\n\t\t});\n\n\t\t// Auto-Content-Digest BEFORE sign() (signer precondition for POST/PUT/PATCH + body).\n\t\tensureContentDigest(method, headers, body);\n\n\t\t// Sign — replaces stale Signature* headers, then writes fresh ones.\n\t\tawait sign({ method, url, headers, body }, identity);\n\t\tapplyUserAgent(headers, identity.userAgent);\n\n\t\tconst outInit: RequestInit = {\n\t\t\t...init,\n\t\t\tmethod,\n\t\t\theaders,\n\t\t\t// Uint8Array is a valid BodyInit at runtime; DOM typings disagree.\n\t\t\tbody: (body ?? undefined) as BodyInit | undefined,\n\t\t\tsignal: init.signal ?? req.signal,\n\t\t\tredirect: init.redirect ?? req.redirect,\n\t\t\tcredentials: init.credentials ?? req.credentials,\n\t\t\tcache: init.cache ?? req.cache,\n\t\t\tkeepalive: init.keepalive ?? req.keepalive,\n\t\t};\n\t\tconst integrity = init.integrity ?? req.integrity;\n\t\tif (integrity) outInit.integrity = integrity;\n\t\tconst referrerPolicy = init.referrerPolicy ?? req.referrerPolicy;\n\t\tif (referrerPolicy) outInit.referrerPolicy = referrerPolicy;\n\n\t\tconst doFetch = options.fetch ?? globalThis.fetch;\n\t\treturn doFetch(url, outInit);\n\t};\n}\n","/**\n * applyTo — sign every matching request a Playwright page sends.\n *\n * Mirrors python/src/wbauth/adapters/playwright.py.\n *\n * Privacy: by default every http(s) request is signed, including third-party\n * analytics/CDN hosts, which reveals your agent identity to them. Pass\n * `hosts` to restrict signing to the sites you actually target:\n *\n * await applyTo(page, identity, { hosts: [\"example.com\", \"*.example.org\"] });\n *\n * Non-http(s) requests (data:, blob:, chrome-extension:) are never signed.\n * Note: Playwright disables the HTTP cache while a route is active.\n */\nimport type { Page, Request as PWRequest, Route } from \"playwright\";\nimport type { Identity } from \"../identity.js\";\nimport { sign } from \"../signer.js\";\nimport { applyUserAgent, ensureContentDigest, hostMatches } from \"./_utils.js\";\n\nexport interface ApplyToOptions {\n\t/** Only sign requests to these hosts (`example.com`, `*.example.com`). */\n\thosts?: readonly string[];\n\t/** Custom predicate; takes precedence over `hosts`. */\n\tshouldSign?: (url: URL) => boolean;\n\t/** Route pattern passed to `page.route`. Default `\"**\\/*\"`. */\n\tpattern?: string;\n}\n\nexport async function applyTo(\n\tpage: Page,\n\tidentity: Identity,\n\toptions: ApplyToOptions = {},\n): Promise<void> {\n\tconst pattern = options.pattern ?? \"**/*\";\n\tconst decide = (raw: string): boolean => {\n\t\tlet u: URL;\n\t\ttry {\n\t\t\tu = new URL(raw);\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t\tif (u.protocol !== \"https:\" && u.protocol !== \"http:\") return false;\n\t\tif (options.shouldSign) return options.shouldSign(u);\n\t\tif (options.hosts) return hostMatches(u.hostname, options.hosts);\n\t\treturn true;\n\t};\n\n\tawait page.route(pattern, async (route: Route, request: PWRequest) => {\n\t\tconst url = request.url();\n\t\tif (!decide(url)) {\n\t\t\tconst r = route as Route & { fallback?: () => Promise<void> };\n\t\t\tawait (r.fallback ? r.fallback() : route.continue());\n\t\t\treturn;\n\t\t}\n\t\tconst headers = await request.allHeaders();\n\t\tconst postData = request.postDataBuffer();\n\t\tconst body = postData ? new Uint8Array(postData) : null;\n\t\tensureContentDigest(request.method(), headers, body);\n\t\tawait sign({ method: request.method(), url, headers, body }, identity);\n\t\tapplyUserAgent(headers, identity.userAgent);\n\t\tawait route.continue({ headers });\n\t});\n}\n","/**\n * Identity, KeyPair, and key-file helpers for the wbauth TypeScript SDK.\n *\n * Mirrors python/src/wbauth/identity.py — same on-disk format (PKCS8\n * NoEncryption PEM at 0o600), same kid (RFC 7638 JWK thumbprint), same\n * multi-key rotation semantics (active + retiring), same REDACTED repr\n * (IDENT-08).\n *\n * Cross-language guarantee (D-60): the PEM file Python writes via\n * `Ed25519PrivateKey.private_bytes(PEM, PKCS8, NoEncryption)` is byte-equal\n * to what Node's `crypto.generateKeyPairSync('ed25519').privateKey.export({\n * format: 'pem', type: 'pkcs8' })` writes. Both runtimes can load each\n * other's keyfile and produce the same kid + signatures.\n *\n * Key implementation choice (Pitfall 1): PEM→JWK is done via Node 20+\n * stdlib `node:crypto.createPrivateKey(pem).export({format:'jwk'})`. We do\n * NOT use `jose` or any third-party JOSE library — the stdlib output is\n * verified byte-identical to what Python's cryptography library produces\n * for the same PKCS8 PEM (RESEARCH §2.2 live verification 2026-05-10).\n */\n\nimport { createPrivateKey, generateKeyPairSync } from \"node:crypto\";\nimport {\n\tcloseSync,\n\texistsSync,\n\tmkdirSync,\n\topenSync,\n\treadFileSync,\n\tstatSync,\n\twriteFileSync,\n} from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, resolve } from \"node:path\";\nimport type { Signer } from \"http-message-sig\";\nimport { signerFromJWK } from \"web-bot-auth/crypto\";\n\n// ---------- Module constants ----------\n\n/** Default path for the active signing key. Matches Python DEFAULT_KEY_PATH. */\nexport const DEFAULT_KEY_PATH = resolve(\n\thomedir(),\n\t\".config\",\n\t\"wbauth\",\n\t\"key.pem\",\n);\n\n/**\n * RFC 9421 Appendix B.1.4 publicly-known test key — NEVER use in production.\n * The private \"d\" component is published in the RFC and known to everyone.\n * `Identity.fromTestKey()` returns an Identity using these bytes.\n */\nconst TEST_KEY_JWK: JsonWebKey = {\n\tkty: \"OKP\",\n\tcrv: \"Ed25519\",\n\td: \"n4Ni-HpISpVObnQMW0wOhCKROaIKqKtW_2ZYb2p9KcU\",\n\tx: \"JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs\",\n};\n\n// ---------- Public types ----------\n\nexport interface IdentityOptions {\n\t/** Stable HTTPS URL where this agent's JWKS is published. Must start with `https://`. */\n\tsignatureAgentUrl: string;\n\t/** Optional User-Agent string adapters inject when caller did not supply one. */\n\tuserAgent?: string;\n}\n\nexport interface KeyPair {\n\t/** Private JWK (includes \"d\") — used to mint the signer. Never logged. */\n\tprivateJwk: JsonWebKey;\n\t/** Public JWK ({kty, crv, x, kid}) — what exportJwks returns per key. */\n\tpublicJwk: JsonWebKey & { kid: string };\n\t/** RFC 7638 base64url-no-pad thumbprint of the public key. */\n\tkid: string;\n\t/** Pre-resolved web-bot-auth Signer (caches the imported CryptoKey). */\n\tsigner: Signer;\n}\n\n// ---------- Identity class ----------\n\nexport class Identity {\n\tprivate _active: KeyPair;\n\tprivate _retiring: KeyPair | null;\n\treadonly signatureAgentUrl: string;\n\treadonly userAgent: string | undefined;\n\n\tprivate constructor(\n\t\tactive: KeyPair,\n\t\topts: IdentityOptions,\n\t\tretiring: KeyPair | null = null,\n\t) {\n\t\tif (!opts.signatureAgentUrl.startsWith(\"https://\")) {\n\t\t\tthrow new Error(\n\t\t\t\t`signatureAgentUrl must be https://, got: ${opts.signatureAgentUrl}`,\n\t\t\t);\n\t\t}\n\t\tthis._active = active;\n\t\tthis._retiring = retiring;\n\t\tthis.signatureAgentUrl = opts.signatureAgentUrl;\n\t\tthis.userAgent = opts.userAgent;\n\t}\n\n\t// ---------- Public read-only API ----------\n\n\t/** The active key's kid (used as `keyid=` param in Signature-Input). */\n\tget kid(): string {\n\t\treturn this._active.kid;\n\t}\n\n\t/** Module-internal: returns the resolved Signer for sign() to use. */\n\t_signer(): Signer {\n\t\treturn this._active.signer;\n\t}\n\n\t/**\n\t * Export the public JWKS document.\n\t *\n\t * Returns `{keys: [active_jwk]}` if no retiring key is held, or\n\t * `{keys: [active_jwk, retiring_jwk]}` after a rotation. Active is always\n\t * first. Verifiers select by `kid`. Mirror of Python `export_jwks`.\n\t */\n\texportJwks(): { keys: Array<JsonWebKey & { kid: string }> } {\n\t\tconst keys: Array<JsonWebKey & { kid: string }> = [this._active.publicJwk];\n\t\tif (this._retiring) keys.push(this._retiring.publicJwk);\n\t\treturn { keys };\n\t}\n\n\t/**\n\t * IDENT-07: generate a new active key, demote current to retiring.\n\t *\n\t * Returns a NEW Identity (immutable update — the old Identity is unchanged).\n\t * Any previously-retiring key is dropped (only one overlap window).\n\t */\n\tasync rotate(newPath: string = DEFAULT_KEY_PATH): Promise<Identity> {\n\t\tconst newPair = await generateKeypairTo(expandUser(newPath));\n\t\treturn new Identity(\n\t\t\tnewPair,\n\t\t\t{ signatureAgentUrl: this.signatureAgentUrl, userAgent: this.userAgent },\n\t\t\tthis._active,\n\t\t);\n\t}\n\n\t// ---------- Constructors ----------\n\n\t/**\n\t * Primary entry point. Loads `path` if it exists, generates a fresh key\n\t * there otherwise. The on-disk file is PKCS8 NoEncryption PEM at 0o600,\n\t * byte-equal to what Python's `Identity.load_or_generate` writes (D-60).\n\t */\n\tstatic async loadOrGenerate(\n\t\tpath: string = DEFAULT_KEY_PATH,\n\t\topts: IdentityOptions,\n\t): Promise<Identity> {\n\t\tconst resolved = expandUser(path);\n\t\tconst pair = existsSync(resolved)\n\t\t\t? await loadKeypair(resolved)\n\t\t\t: await generateKeypairTo(resolved);\n\t\treturn new Identity(pair, opts);\n\t}\n\n\t/**\n\t * Construct from the RFC 9421 Appendix B.1.4 publicly-known test key.\n\t * For tests + Cloudflare debug smoke checks. NEVER use in production.\n\t * Does NOT write any file to disk.\n\t */\n\tstatic async fromTestKey(\n\t\tsignatureAgentUrl: string,\n\t\topts: Omit<IdentityOptions, \"signatureAgentUrl\"> = {},\n\t): Promise<Identity> {\n\t\tconst pair = await keyPairFromJwk(TEST_KEY_JWK);\n\t\treturn new Identity(pair, { ...opts, signatureAgentUrl });\n\t}\n\n\t// ---------- Redaction guarantees (IDENT-08) ----------\n\n\ttoString(): string {\n\t\treturn `<Identity REDACTED kid='${this.kid}' sig_agent='${this.signatureAgentUrl}'>`;\n\t}\n\n\t/** util.inspect / console.log render the same REDACTED form (IDENT-08). */\n\t[Symbol.for(\"nodejs.util.inspect.custom\")](): string {\n\t\treturn this.toString();\n\t}\n}\n\n// ---------- Helpers (module-private) ----------\n\nfunction expandUser(path: string): string {\n\treturn resolve(path.startsWith(\"~\") ? path.replace(/^~/, homedir()) : path);\n}\n\nasync function keyPairFromJwk(privateJwk: JsonWebKey): Promise<KeyPair> {\n\tconst signer = await signerFromJWK(privateJwk);\n\t// signerFromJWK pre-computes the kid (RFC 7638 thumbprint) into signer.keyid.\n\t// Reuse it instead of calling jwkToKeyID separately (avoids the 3-arg helper).\n\tconst kid = signer.keyid;\n\tconst publicJwk: JsonWebKey & { kid: string } = {\n\t\tkty: privateJwk.kty,\n\t\tcrv: privateJwk.crv,\n\t\tx: privateJwk.x,\n\t\tkid,\n\t};\n\treturn { privateJwk, publicJwk, kid, signer };\n}\n\nasync function loadKeypair(path: string): Promise<KeyPair> {\n\t// POSIX permission check (mirror Python — refuse wider-than-0o600).\n\t// Pitfall 8: Windows lacks POSIX mode bits → emit warning, do not enforce.\n\tif (process.platform !== \"win32\") {\n\t\tconst mode = statSync(path).mode & 0o777;\n\t\tif ((mode & 0o077) !== 0) {\n\t\t\tthrow new Error(\n\t\t\t\t`Key file ${path} has mode 0o${mode.toString(8)}; expected 0o600. Fix: chmod 600 ${path}`,\n\t\t\t);\n\t\t}\n\t} else {\n\t\tprocess.emitWarning(\n\t\t\t`Windows detected — file permissions on ${path} cannot be enforced via POSIX mode. Use NTFS ACLs or store the key in a per-user secrets vault.`,\n\t\t);\n\t}\n\tconst pem = readFileSync(path, \"utf8\");\n\tconst jwk = createPrivateKey(pem).export({ format: \"jwk\" }) as JsonWebKey;\n\tif (jwk.kty !== \"OKP\" || jwk.crv !== \"Ed25519\") {\n\t\tthrow new TypeError(`Expected Ed25519, got kty=${jwk.kty}, crv=${jwk.crv}`);\n\t}\n\treturn keyPairFromJwk(jwk);\n}\n\nasync function generateKeypairTo(path: string): Promise<KeyPair> {\n\tconst dir = dirname(path);\n\tmkdirSync(dir, { recursive: true });\n\tif (existsSync(path)) {\n\t\t// Belt + suspenders: O_EXCL would catch this too, but raising here gives\n\t\t// a cleaner error than the low-level EEXIST.\n\t\tthrow new Error(`Key already exists at ${path}; refuse overwrite`);\n\t}\n\tconst { privateKey } = generateKeyPairSync(\"ed25519\");\n\tconst pem = privateKey.export({ format: \"pem\", type: \"pkcs8\" }) as string;\n\t// Race-free 0o600 creation: openSync with \"wx\" = O_WRONLY|O_CREAT|O_EXCL.\n\t// Mirrors Python `os.open(path, O_WRONLY|O_CREAT|O_EXCL, 0o600)`.\n\tconst fd = openSync(path, \"wx\", 0o600);\n\ttry {\n\t\twriteFileSync(fd, pem);\n\t} finally {\n\t\tcloseSync(fd);\n\t}\n\tconst jwk = privateKey.export({ format: \"jwk\" }) as JsonWebKey;\n\treturn keyPairFromJwk(jwk);\n}\n","/**\n * wbauth: Web Bot Auth (RFC 9421) TypeScript SDK — Phase 4 public surface.\n *\n * Per CONTEXT D-74: flat root exports. Subpath exports (`wb-auth/adapters`)\n * are a v1.x optimization, not required for Phase 4.\n *\n * Quickstart:\n * import { Identity, createSignedFetch } from \"wb-auth\";\n * const id = await Identity.loadOrGenerate(\"./key.pem\", {\n * signatureAgentUrl: \"https://example.com/.well-known/.../<kid>\",\n * });\n * const sf = createSignedFetch(id);\n * const res = await sf(\"https://api.example.com/data\");\n */\n\nexport type { SignedFetchOptions } from \"./adapters/fetch.js\";\nexport { createSignedFetch } from \"./adapters/fetch.js\";\nexport type { ApplyToOptions } from \"./adapters/playwright.js\";\nexport { applyTo } from \"./adapters/playwright.js\";\nexport type { IdentityOptions, KeyPair } from \"./identity.js\";\nexport { DEFAULT_KEY_PATH, Identity } from \"./identity.js\";\nexport type { NormalizedRequest } from \"./normalized-request.js\";\nexport type { SignatureHeaders, SignOptions } from \"./signer.js\";\nexport { sign } from \"./signer.js\";\n\nexport const VERSION = \"0.2.0\";\n"],"mappings":"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","names":["DIGEST_METHODS"]}
|
|
1
|
+
{"version":3,"sources":["../src/signer.ts","../src/adapters/_utils.ts","../src/adapters/fetch.ts","../src/adapters/playwright.ts","../src/identity.ts","../src/index.ts"],"sourcesContent":["/**\n * Pure-function signer for Web Bot Auth (RFC 9421).\n *\n * Mirrors python/src/wbauth/signer.py — same defaults (Ed25519, tag=\n * \"web-bot-auth\", expires=created+60s, covered components [\"@authority\",\n * \"signature-agent\"] + \"content-digest\" for bodies).\n *\n * The function delegates all RFC 9421 mechanics (signing-base canonicalization,\n * structured-field encoding, Ed25519 signing) to `web-bot-auth` 0.1.3's\n * `signatureHeaders`. We only own the Web Bot Auth profile glue: pre-setting\n * Signature-Agent (Pitfall 6), picking covered components, defaulting expires.\n */\nimport { type SignatureParams, signatureHeaders } from \"web-bot-auth\";\nimport type { Identity } from \"./identity.js\";\nimport type { NormalizedRequest } from \"./normalized-request.js\";\n\n// ---------- Module constants (HARD-CODED — typos = silent verifier reject) ----------\n\n/** IETF Web Bot Auth profile mandate. NEVER change. Pitfall 6. */\nconst WEB_BOT_AUTH_TAG = \"web-bot-auth\";\n/** Canonical Signature label. Most verifier examples assume \"sig1\". */\nconst DEFAULT_LABEL = \"sig1\";\n/** Default expires window. 60s leaves NTP-skew + network-latency headroom. */\nconst DEFAULT_EXPIRES_SECONDS = 60;\n/** Methods that may carry a body and therefore want content-digest covered. */\n/** Hard upper bound on the validity window (parity with Python). */\nexport const MAX_EXPIRES_SECONDS = 3600;\n// WEB_BOT_AUTH_TAG is enforced internally by web-bot-auth's signatureHeaders;\n// we keep the constant for greppability of the profile invariant.\nvoid WEB_BOT_AUTH_TAG;\n\n// ---------- Public types ----------\n\nexport interface SignatureHeaders {\n\tsignature: string;\n\tsignatureInput: string;\n\tsignatureAgent: string;\n}\n\nexport interface SignOptions {\n\t/** Signing timestamp. Defaults to `new Date()`. Override for vector reproducibility. */\n\tcreated?: Date;\n\t/** Validity window in seconds. Defaults to 60. */\n\texpiresAfterSeconds?: number;\n\t/** Anti-replay nonce. Defaults to web-bot-auth's `generateNonce()` (64-byte b64). */\n\tnonce?: string;\n\t/** Signature label (the `sig1=` prefix). Defaults to \"sig1\". */\n\tlabel?: string;\n}\n\n// ---------- Components selector (mirror Python `_components_for`) ----------\n\nfunction componentsFor(_method: string, hasBody: boolean): string[] {\n\tconst base = [\"@authority\", \"signature-agent\"];\n\tif (hasBody) {\n\t\tbase.push(\"content-digest\");\n\t}\n\treturn base;\n}\n\n/** Headers regenerated on every sign() call. */\nconst RESIGN_HEADERS = [\"signature\", \"signature-input\", \"signature-agent\"];\n\nfunction hasHeader(headers: Record<string, string>, name: string): boolean {\n\tconst n = name.toLowerCase();\n\treturn Object.keys(headers).some((k) => k.toLowerCase() === n);\n}\n\nfunction removeHeaders(headers: Record<string, string>, names: string[]): void {\n\tfor (const k of Object.keys(headers)) {\n\t\tif (names.includes(k.toLowerCase())) delete headers[k];\n\t}\n}\n\n// ---------- Public API ----------\n\n/**\n * Sign a NormalizedRequest with the Web Bot Auth profile.\n *\n * Mutates `request.headers` in place: writes Signature-Agent BEFORE building\n * the signing base (Pitfall 6 — without pre-set, web-bot-auth defaults to\n * `[\"@authority\"]` only and silently breaks byte-equality with the Python\n * signer). After signatureHeaders returns, also writes Signature and\n * Signature-Input back into `request.headers`.\n *\n * @returns the same three values as a typed dataclass for callers that prefer\n * a return value over header inspection.\n */\nexport async function sign(\n\trequest: NormalizedRequest,\n\tidentity: Identity,\n\topts: SignOptions = {},\n): Promise<SignatureHeaders> {\n\t// 1. Defensive https:// check — Identity ctor enforces it too, but a future\n\t// bypassed ctor would otherwise leak http://. Pitfall 1.\n\tif (!identity.signatureAgentUrl.startsWith(\"https://\")) {\n\t\tthrow new Error(\n\t\t\t`signatureAgentUrl must be https://, got: ${identity.signatureAgentUrl}`,\n\t\t);\n\t}\n\n\t// 2. Validate options up-front (parity with Python signer).\n\tconst expiresAfterSeconds =\n\t\topts.expiresAfterSeconds ?? DEFAULT_EXPIRES_SECONDS;\n\tif (\n\t\ttypeof expiresAfterSeconds !== \"number\" ||\n\t\t!Number.isFinite(expiresAfterSeconds) ||\n\t\texpiresAfterSeconds <= 0 ||\n\t\texpiresAfterSeconds > MAX_EXPIRES_SECONDS\n\t) {\n\t\tthrow new RangeError(\n\t\t\t`expiresAfterSeconds must be a number in (0, ${MAX_EXPIRES_SECONDS}], got: ${expiresAfterSeconds}`,\n\t\t);\n\t}\n\n\t// 3. Drop stale signature headers in ANY casing. A retried / redirected\n\t// request still carries the previous hop's values; leaving them in place\n\t// produces merged `sig1=..., sig1=...` headers that every verifier rejects.\n\tremoveHeaders(request.headers, RESIGN_HEADERS);\n\n\t// 4. Set Signature-Agent header (RFC 8941 string in double quotes).\n\t// CRITICAL: must be set BEFORE signatureHeaders inspects the message,\n\t// otherwise web-bot-auth picks REQUEST_COMPONENTS_WITHOUT_SIGNATURE_AGENT.\n\tconst signatureAgentHeader = `\"${identity.signatureAgentUrl}\"`;\n\trequest.headers[\"Signature-Agent\"] = signatureAgentHeader;\n\n\t// 5. Defaults.\n\tconst created = opts.created ?? new Date();\n\tconst expires = new Date(created.getTime() + expiresAfterSeconds * 1000);\n\tconst label = opts.label ?? DEFAULT_LABEL;\n\n\t// 6. Components based on body presence.\n\tconst hasBody =\n\t\trequest.body !== null &&\n\t\trequest.body !== undefined &&\n\t\trequest.body.byteLength > 0;\n\tconst components = componentsFor(request.method, hasBody);\n\tif (\n\t\tcomponents.includes(\"content-digest\") &&\n\t\t!hasHeader(request.headers, \"content-digest\")\n\t) {\n\t\t// Python raises here too. Signing a missing header would yield a\n\t\t// signature that no verifier can ever validate.\n\t\tthrow new Error(\n\t\t\t'Covered header field \"content-digest\" not found in the message. ' +\n\t\t\t\t\"Call ensureContentDigest() or use createSignedFetch()/applyTo(), which add it automatically.\",\n\t\t);\n\t}\n\n\t// 7. Build a Web `Request` for web-bot-auth's `signatureHeaders`.\n\tconst headers = new Headers();\n\tfor (const [k, v] of Object.entries(request.headers)) headers.set(k, v);\n\tconst init: RequestInit = { method: request.method, headers };\n\t// Cast: Uint8Array is a valid BodyInit at runtime (web fetch spec) but DOM\n\t// typing wants ArrayBufferView/string/Blob etc. The cast is safe.\n\t// GET/HEAD cannot carry a body in a WHATWG Request; the body itself is not\n\t// part of the signature base (content-digest is), so it is safe to omit.\n\tconst m = request.method.toUpperCase();\n\tif (hasBody && m !== \"GET\" && m !== \"HEAD\") {\n\t\tinit.body = request.body as unknown as BodyInit;\n\t}\n\tconst req = new Request(request.url, init);\n\n\t// 8. Delegate to web-bot-auth. The signer is the active key's pre-resolved\n\t// Signer (cached in Identity at construction time).\n\tconst params: SignatureParams = {\n\t\tcreated,\n\t\texpires,\n\t\tkey: label,\n\t\tcomponents,\n\t\t...(opts.nonce !== undefined ? { nonce: opts.nonce } : {}),\n\t};\n\tconst result = await signatureHeaders(req, identity._signer(), params);\n\n\t// 9. Mutate request.headers (mirror Python signer behavior).\n\trequest.headers.Signature = result.Signature;\n\trequest.headers[\"Signature-Input\"] = result[\"Signature-Input\"];\n\n\treturn {\n\t\tsignature: result.Signature,\n\t\tsignatureInput: result[\"Signature-Input\"],\n\t\tsignatureAgent: signatureAgentHeader,\n\t};\n}\n","/**\n * Shared adapter helpers (internal — not part of the public surface).\n *\n * Mirror python/src/wbauth/adapters/_utils.py. The signer auto-includes\n * `content-digest` in covered components for any request with a body;\n * adapters call ensureContentDigest() before sign() so that precondition\n * is met. RFC 9530 sha-256 in structured-fields form (`sha-256=:<b64>:`).\n */\nimport { createHash } from \"node:crypto\";\n\n/** Default UAs injected by HTTP stacks; an Identity UA should replace them. */\nconst LIBRARY_DEFAULT_UA = /^(node|undici|node-fetch)(\\/|$)/i;\n\nfunction findHeader(\n\theaders: Record<string, string>,\n\tname: string,\n): string | undefined {\n\tconst n = name.toLowerCase();\n\treturn Object.keys(headers).find((k) => k.toLowerCase() === n);\n}\n\n/**\n * Mutate `headers` in place to add Content-Digest when warranted. No-op if:\n * - body is null/empty,\n * - the request already carries a Content-Digest header (case-insensitive).\n */\nexport function ensureContentDigest(\n\tmethod: string,\n\theaders: Record<string, string>,\n\tbody: Uint8Array | null,\n): void {\n\tif (!body || body.byteLength === 0) return;\n\tvoid method; // any method with a body is covered (DELETE, QUERY, …)\n\tif (findHeader(headers, \"content-digest\")) return;\n\tconst b64 = createHash(\"sha256\").update(body).digest(\"base64\");\n\theaders[\"Content-Digest\"] = `sha-256=:${b64}:`;\n}\n\n/**\n * Apply the Identity's User-Agent unless the caller explicitly set a\n * non-library UA. Library defaults (node, undici) are replaced.\n */\nexport function applyUserAgent(\n\theaders: Record<string, string>,\n\tuserAgent: string | undefined,\n): void {\n\tif (!userAgent) return;\n\tconst key = findHeader(headers, \"user-agent\");\n\tif (key === undefined) {\n\t\theaders[\"User-Agent\"] = userAgent;\n\t\treturn;\n\t}\n\tif (LIBRARY_DEFAULT_UA.test(headers[key] ?? \"\")) {\n\t\tdelete headers[key];\n\t\theaders[\"User-Agent\"] = userAgent;\n\t}\n}\n\n/**\n * Host allow-list matcher. Entries are exact hostnames (`api.example.com`)\n * or wildcard subdomains (`*.example.com`, which also matches `example.com`).\n */\nexport function hostMatches(\n\thostname: string,\n\tallow: readonly string[],\n): boolean {\n\tconst h = hostname.toLowerCase().replace(/\\.$/, \"\");\n\treturn allow.some((raw) => {\n\t\tconst a = raw.toLowerCase().replace(/\\.$/, \"\");\n\t\tif (a.startsWith(\"*.\")) {\n\t\t\tconst base = a.slice(2);\n\t\t\treturn h === base || h.endsWith(`.${base}`);\n\t\t}\n\t\treturn h === a;\n\t});\n}\n","/**\n * createSignedFetch — drop-in `fetch` wrapper that signs every outgoing request.\n *\n * Mirrors python/src/wbauth/adapters/httpx_auth.py. Stateless: closure over\n * `identity` only. Returns a function whose signature matches `typeof fetch`,\n * so user code can swap `fetch` for `signedFetch` without other changes.\n *\n * - Request-object inputs keep their `signal`, `redirect`, `credentials`,\n * `cache`, `keepalive`, `integrity` and `referrerPolicy`.\n * - Re-sending an already signed request (retry wrappers) produces exactly one\n * fresh signature — stale Signature* headers are replaced, never merged.\n * - **Redirects** (`redirect: \"follow\"`, the default) are followed here, not\n * by the underlying fetch, so each hop is re-signed for its own\n * `@authority`. Native fetch would otherwise replay hop 1's signature to the\n * redirect target. Method/body rewriting and cross-origin credential\n * stripping follow the WHATWG fetch spec.\n *\n * v1 limitation: streaming bodies are buffered (`req.clone().arrayBuffer()`)\n * so Content-Digest can be computed.\n */\n\nimport type { Identity } from \"../identity.js\";\nimport { sign } from \"../signer.js\";\nimport { applyUserAgent, ensureContentDigest } from \"./_utils.js\";\n\nexport interface SignedFetchOptions {\n\t/** Underlying fetch implementation. Defaults to `globalThis.fetch` at call time. */\n\tfetch?: typeof fetch;\n\t/** Maximum redirects followed (and re-signed). Default 20, like fetch. */\n\tmaxRedirects?: number;\n}\n\nconst REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);\n/** Request headers dropped when a redirect crosses origins (fetch spec §4.4). */\nconst CROSS_ORIGIN_STRIP = new Set([\n\t\"authorization\",\n\t\"cookie\",\n\t\"proxy-authorization\",\n]);\n/** Request-body headers dropped when a redirect turns the request into GET. */\nconst BODY_HEADERS = new Set([\n\t\"content-type\",\n\t\"content-length\",\n\t\"content-encoding\",\n\t\"content-language\",\n\t\"content-location\",\n\t\"content-digest\",\n]);\n\nfunction dropHeaders(headers: Record<string, string>, names: Set<string>) {\n\tfor (const k of Object.keys(headers)) {\n\t\tif (names.has(k.toLowerCase())) delete headers[k];\n\t}\n}\n\nexport function createSignedFetch(\n\tidentity: Identity,\n\toptions: SignedFetchOptions = {},\n): typeof fetch {\n\treturn async function signedFetch(\n\t\tinput: RequestInfo | URL,\n\t\tinit: RequestInit = {},\n\t): Promise<Response> {\n\t\tconst req = new Request(input, init);\n\t\tlet method = req.method;\n\t\tlet url = req.url;\n\n\t\tlet body: Uint8Array | null = null;\n\t\tif (req.body) {\n\t\t\tconst buf = await req.clone().arrayBuffer();\n\t\t\tbody = buf.byteLength > 0 ? new Uint8Array(buf) : null;\n\t\t}\n\n\t\tconst baseHeaders: Record<string, string> = {};\n\t\treq.headers.forEach((value, key) => {\n\t\t\tbaseHeaders[key] = value;\n\t\t});\n\n\t\tconst redirectMode = init.redirect ?? req.redirect;\n\t\tconst followHere = redirectMode === \"follow\";\n\t\tconst maxRedirects = options.maxRedirects ?? 20;\n\t\tconst doFetch = options.fetch ?? globalThis.fetch;\n\n\t\tfor (let hop = 0; ; hop++) {\n\t\t\tconst headers: Record<string, string> = { ...baseHeaders };\n\t\t\tensureContentDigest(method, headers, body);\n\t\t\tawait sign({ method, url, headers, body }, identity);\n\t\t\tapplyUserAgent(headers, identity.userAgent);\n\n\t\t\tconst outInit: RequestInit = {\n\t\t\t\t...init,\n\t\t\t\tmethod,\n\t\t\t\theaders,\n\t\t\t\tbody: (body ?? undefined) as BodyInit | undefined,\n\t\t\t\tsignal: init.signal ?? req.signal,\n\t\t\t\tredirect: followHere ? \"manual\" : redirectMode,\n\t\t\t\tcredentials: init.credentials ?? req.credentials,\n\t\t\t\tcache: init.cache ?? req.cache,\n\t\t\t\tkeepalive: init.keepalive ?? req.keepalive,\n\t\t\t};\n\t\t\tconst integrity = init.integrity ?? req.integrity;\n\t\t\tif (integrity) outInit.integrity = integrity;\n\t\t\tconst referrerPolicy = init.referrerPolicy ?? req.referrerPolicy;\n\t\t\tif (referrerPolicy) outInit.referrerPolicy = referrerPolicy;\n\n\t\t\tconst res = await doFetch(url, outInit);\n\t\t\tif (!followHere || !REDIRECT_STATUSES.has(res.status)) return res;\n\t\t\tconst location = res.headers.get(\"location\");\n\t\t\tif (location === null) return res;\n\t\t\tif (hop >= maxRedirects) {\n\t\t\t\tthrow new TypeError(`redirect count exceeded (${maxRedirects})`);\n\t\t\t}\n\t\t\tconst next = new URL(location, url);\n\t\t\tif (next.protocol !== \"http:\" && next.protocol !== \"https:\") {\n\t\t\t\tthrow new TypeError(`unsupported redirect scheme: ${next.protocol}`);\n\t\t\t}\n\t\t\tconst upper = method.toUpperCase();\n\t\t\tif (\n\t\t\t\t(res.status === 303 && upper !== \"GET\" && upper !== \"HEAD\") ||\n\t\t\t\t((res.status === 301 || res.status === 302) && upper === \"POST\")\n\t\t\t) {\n\t\t\t\tmethod = \"GET\";\n\t\t\t\tbody = null;\n\t\t\t\tdropHeaders(baseHeaders, BODY_HEADERS);\n\t\t\t}\n\t\t\tif (new URL(url).origin !== next.origin) {\n\t\t\t\tdropHeaders(baseHeaders, CROSS_ORIGIN_STRIP);\n\t\t\t}\n\t\t\tawait res.body?.cancel().catch(() => {});\n\t\t\turl = next.href;\n\t\t}\n\t};\n}\n","/**\n * applyTo — sign every matching request a Playwright page sends.\n *\n * Mirrors python/src/wbauth/adapters/playwright.py.\n *\n * Privacy: by default every http(s) request is signed, including third-party\n * analytics/CDN hosts, which reveals your agent identity to them. Pass\n * `hosts` to restrict signing to the sites you actually target:\n *\n * await applyTo(page, identity, { hosts: [\"example.com\", \"*.example.org\"] });\n *\n * Non-http(s) requests (data:, blob:, chrome-extension:) are never signed.\n * Note: Playwright disables the HTTP cache while a route is active.\n */\nimport type { Page, Request as PWRequest, Route } from \"playwright\";\nimport type { Identity } from \"../identity.js\";\nimport { sign } from \"../signer.js\";\nimport { applyUserAgent, ensureContentDigest, hostMatches } from \"./_utils.js\";\n\nexport interface ApplyToOptions {\n\t/** Only sign requests to these hosts (`example.com`, `*.example.com`). */\n\thosts?: readonly string[];\n\t/** Custom predicate; takes precedence over `hosts`. */\n\tshouldSign?: (url: URL) => boolean;\n\t/** Route pattern passed to `page.route`. Default `\"**\\/*\"`. */\n\tpattern?: string;\n}\n\nexport async function applyTo(\n\tpage: Page,\n\tidentity: Identity,\n\toptions: ApplyToOptions = {},\n): Promise<void> {\n\tconst pattern = options.pattern ?? \"**/*\";\n\tconst decide = (raw: string): boolean => {\n\t\tlet u: URL;\n\t\ttry {\n\t\t\tu = new URL(raw);\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t\tif (u.protocol !== \"https:\" && u.protocol !== \"http:\") return false;\n\t\tif (options.shouldSign) return options.shouldSign(u);\n\t\tif (options.hosts) return hostMatches(u.hostname, options.hosts);\n\t\treturn true;\n\t};\n\n\tawait page.route(pattern, async (route: Route, request: PWRequest) => {\n\t\tconst url = request.url();\n\t\tif (!decide(url)) {\n\t\t\tconst r = route as Route & { fallback?: () => Promise<void> };\n\t\t\tawait (r.fallback ? r.fallback() : route.continue());\n\t\t\treturn;\n\t\t}\n\t\tconst headers = await request.allHeaders();\n\t\tconst postData = request.postDataBuffer();\n\t\tconst body = postData ? new Uint8Array(postData) : null;\n\t\tensureContentDigest(request.method(), headers, body);\n\t\tawait sign({ method: request.method(), url, headers, body }, identity);\n\t\tapplyUserAgent(headers, identity.userAgent);\n\t\tawait route.continue({ headers });\n\t});\n}\n","/**\n * Identity, KeyPair, and key-file helpers for the wbauth TypeScript SDK.\n *\n * Mirrors python/src/wbauth/identity.py — same on-disk format (PKCS8\n * NoEncryption PEM at 0o600), same kid (RFC 7638 JWK thumbprint), same\n * multi-key rotation semantics (active + retiring), same REDACTED repr\n * (IDENT-08).\n *\n * Cross-language guarantee (D-60): the PEM file Python writes via\n * `Ed25519PrivateKey.private_bytes(PEM, PKCS8, NoEncryption)` is byte-equal\n * to what Node's `crypto.generateKeyPairSync('ed25519').privateKey.export({\n * format: 'pem', type: 'pkcs8' })` writes. Both runtimes can load each\n * other's keyfile and produce the same kid + signatures.\n *\n * Key implementation choice (Pitfall 1): PEM→JWK is done via Node 20+\n * stdlib `node:crypto.createPrivateKey(pem).export({format:'jwk'})`. We do\n * NOT use `jose` or any third-party JOSE library — the stdlib output is\n * verified byte-identical to what Python's cryptography library produces\n * for the same PKCS8 PEM (RESEARCH §2.2 live verification 2026-05-10).\n */\n\nimport { createPrivateKey, generateKeyPairSync } from \"node:crypto\";\nimport {\n\tcloseSync,\n\texistsSync,\n\tmkdirSync,\n\topenSync,\n\treadFileSync,\n\tstatSync,\n\twriteFileSync,\n} from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, resolve } from \"node:path\";\nimport type { Signer } from \"http-message-sig\";\nimport { signerFromJWK } from \"web-bot-auth/crypto\";\n\n// ---------- Module constants ----------\n\n/** Default path for the active signing key. Matches Python DEFAULT_KEY_PATH. */\nexport const DEFAULT_KEY_PATH = resolve(\n\thomedir(),\n\t\".config\",\n\t\"wbauth\",\n\t\"key.pem\",\n);\n\n/**\n * RFC 9421 Appendix B.1.4 publicly-known test key — NEVER use in production.\n * The private \"d\" component is published in the RFC and known to everyone.\n * `Identity.fromTestKey()` returns an Identity using these bytes.\n */\nconst TEST_KEY_JWK: JsonWebKey = {\n\tkty: \"OKP\",\n\tcrv: \"Ed25519\",\n\td: \"n4Ni-HpISpVObnQMW0wOhCKROaIKqKtW_2ZYb2p9KcU\",\n\tx: \"JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs\",\n};\n\n// ---------- Public types ----------\n\nexport interface IdentityOptions {\n\t/** Stable HTTPS URL where this agent's JWKS is published. Must start with `https://`. */\n\tsignatureAgentUrl: string;\n\t/** Optional User-Agent string adapters inject when caller did not supply one. */\n\tuserAgent?: string;\n}\n\nexport interface KeyPair {\n\t/** Private JWK (includes \"d\") — used to mint the signer. Never logged. */\n\tprivateJwk: JsonWebKey;\n\t/** Public JWK ({kty, crv, x, kid}) — what exportJwks returns per key. */\n\tpublicJwk: JsonWebKey & { kid: string };\n\t/** RFC 7638 base64url-no-pad thumbprint of the public key. */\n\tkid: string;\n\t/** Pre-resolved web-bot-auth Signer (caches the imported CryptoKey). */\n\tsigner: Signer;\n}\n\n// ---------- Identity class ----------\n\nexport class Identity {\n\tprivate _active: KeyPair;\n\tprivate _retiring: KeyPair | null;\n\treadonly signatureAgentUrl: string;\n\treadonly userAgent: string | undefined;\n\n\tprivate constructor(\n\t\tactive: KeyPair,\n\t\topts: IdentityOptions,\n\t\tretiring: KeyPair | null = null,\n\t) {\n\t\tif (!opts.signatureAgentUrl.startsWith(\"https://\")) {\n\t\t\tthrow new Error(\n\t\t\t\t`signatureAgentUrl must be https://, got: ${opts.signatureAgentUrl}`,\n\t\t\t);\n\t\t}\n\t\tthis._active = active;\n\t\tthis._retiring = retiring;\n\t\tthis.signatureAgentUrl = opts.signatureAgentUrl;\n\t\tthis.userAgent = opts.userAgent;\n\t}\n\n\t// ---------- Public read-only API ----------\n\n\t/** The active key's kid (used as `keyid=` param in Signature-Input). */\n\tget kid(): string {\n\t\treturn this._active.kid;\n\t}\n\n\t/** Module-internal: returns the resolved Signer for sign() to use. */\n\t_signer(): Signer {\n\t\treturn this._active.signer;\n\t}\n\n\t/**\n\t * Export the public JWKS document.\n\t *\n\t * Returns `{keys: [active_jwk]}` if no retiring key is held, or\n\t * `{keys: [active_jwk, retiring_jwk]}` after a rotation. Active is always\n\t * first. Verifiers select by `kid`. Mirror of Python `export_jwks`.\n\t */\n\texportJwks(): { keys: Array<JsonWebKey & { kid: string }> } {\n\t\tconst keys: Array<JsonWebKey & { kid: string }> = [this._active.publicJwk];\n\t\tif (this._retiring) keys.push(this._retiring.publicJwk);\n\t\treturn { keys };\n\t}\n\n\t/**\n\t * IDENT-07: generate a new active key, demote current to retiring.\n\t *\n\t * Returns a NEW Identity (immutable update — the old Identity is unchanged).\n\t * Any previously-retiring key is dropped (only one overlap window).\n\t */\n\tasync rotate(newPath: string = DEFAULT_KEY_PATH): Promise<Identity> {\n\t\tconst newPair = await generateKeypairTo(expandUser(newPath));\n\t\treturn new Identity(\n\t\t\tnewPair,\n\t\t\t{ signatureAgentUrl: this.signatureAgentUrl, userAgent: this.userAgent },\n\t\t\tthis._active,\n\t\t);\n\t}\n\n\t// ---------- Constructors ----------\n\n\t/**\n\t * Primary entry point. Loads `path` if it exists, generates a fresh key\n\t * there otherwise. The on-disk file is PKCS8 NoEncryption PEM at 0o600,\n\t * byte-equal to what Python's `Identity.load_or_generate` writes (D-60).\n\t */\n\tstatic async loadOrGenerate(\n\t\tpath: string = DEFAULT_KEY_PATH,\n\t\topts: IdentityOptions,\n\t): Promise<Identity> {\n\t\tconst resolved = expandUser(path);\n\t\tconst pair = existsSync(resolved)\n\t\t\t? await loadKeypair(resolved)\n\t\t\t: await generateKeypairTo(resolved);\n\t\treturn new Identity(pair, opts);\n\t}\n\n\t/**\n\t * Construct from the RFC 9421 Appendix B.1.4 publicly-known test key.\n\t * For tests + Cloudflare debug smoke checks. NEVER use in production.\n\t * Does NOT write any file to disk.\n\t */\n\tstatic async fromTestKey(\n\t\tsignatureAgentUrl: string,\n\t\topts: Omit<IdentityOptions, \"signatureAgentUrl\"> = {},\n\t): Promise<Identity> {\n\t\tconst pair = await keyPairFromJwk(TEST_KEY_JWK);\n\t\treturn new Identity(pair, { ...opts, signatureAgentUrl });\n\t}\n\n\t// ---------- Redaction guarantees (IDENT-08) ----------\n\n\ttoString(): string {\n\t\treturn `<Identity REDACTED kid='${this.kid}' sig_agent='${this.signatureAgentUrl}'>`;\n\t}\n\n\t/** util.inspect / console.log render the same REDACTED form (IDENT-08). */\n\t[Symbol.for(\"nodejs.util.inspect.custom\")](): string {\n\t\treturn this.toString();\n\t}\n}\n\n// ---------- Helpers (module-private) ----------\n\nfunction expandUser(path: string): string {\n\treturn resolve(path.startsWith(\"~\") ? path.replace(/^~/, homedir()) : path);\n}\n\nasync function keyPairFromJwk(privateJwk: JsonWebKey): Promise<KeyPair> {\n\tconst signer = await signerFromJWK(privateJwk);\n\t// signerFromJWK pre-computes the kid (RFC 7638 thumbprint) into signer.keyid.\n\t// Reuse it instead of calling jwkToKeyID separately (avoids the 3-arg helper).\n\tconst kid = signer.keyid;\n\tconst publicJwk: JsonWebKey & { kid: string } = {\n\t\tkty: privateJwk.kty,\n\t\tcrv: privateJwk.crv,\n\t\tx: privateJwk.x,\n\t\tkid,\n\t};\n\treturn { privateJwk, publicJwk, kid, signer };\n}\n\nasync function loadKeypair(path: string): Promise<KeyPair> {\n\t// POSIX permission check (mirror Python — refuse wider-than-0o600).\n\t// Pitfall 8: Windows lacks POSIX mode bits → emit warning, do not enforce.\n\tif (process.platform !== \"win32\") {\n\t\tconst mode = statSync(path).mode & 0o777;\n\t\tif ((mode & 0o077) !== 0) {\n\t\t\tthrow new Error(\n\t\t\t\t`Key file ${path} has mode 0o${mode.toString(8)}; expected 0o600. Fix: chmod 600 ${path}`,\n\t\t\t);\n\t\t}\n\t} else {\n\t\tprocess.emitWarning(\n\t\t\t`Windows detected — file permissions on ${path} cannot be enforced via POSIX mode. Use NTFS ACLs or store the key in a per-user secrets vault.`,\n\t\t);\n\t}\n\tconst pem = readFileSync(path, \"utf8\");\n\tconst jwk = createPrivateKey(pem).export({ format: \"jwk\" }) as JsonWebKey;\n\tif (jwk.kty !== \"OKP\" || jwk.crv !== \"Ed25519\") {\n\t\tthrow new TypeError(`Expected Ed25519, got kty=${jwk.kty}, crv=${jwk.crv}`);\n\t}\n\treturn keyPairFromJwk(jwk);\n}\n\nasync function generateKeypairTo(path: string): Promise<KeyPair> {\n\tconst dir = dirname(path);\n\tmkdirSync(dir, { recursive: true });\n\tif (existsSync(path)) {\n\t\t// Belt + suspenders: O_EXCL would catch this too, but raising here gives\n\t\t// a cleaner error than the low-level EEXIST.\n\t\tthrow new Error(`Key already exists at ${path}; refuse overwrite`);\n\t}\n\tconst { privateKey } = generateKeyPairSync(\"ed25519\");\n\tconst pem = privateKey.export({ format: \"pem\", type: \"pkcs8\" }) as string;\n\t// Race-free 0o600 creation: openSync with \"wx\" = O_WRONLY|O_CREAT|O_EXCL.\n\t// Mirrors Python `os.open(path, O_WRONLY|O_CREAT|O_EXCL, 0o600)`.\n\tconst fd = openSync(path, \"wx\", 0o600);\n\ttry {\n\t\twriteFileSync(fd, pem);\n\t} finally {\n\t\tcloseSync(fd);\n\t}\n\tconst jwk = privateKey.export({ format: \"jwk\" }) as JsonWebKey;\n\treturn keyPairFromJwk(jwk);\n}\n","/**\n * wbauth: Web Bot Auth (RFC 9421) TypeScript SDK — Phase 4 public surface.\n *\n * Per CONTEXT D-74: flat root exports. Subpath exports (`wb-auth/adapters`)\n * are a v1.x optimization, not required for Phase 4.\n *\n * Quickstart:\n * import { Identity, createSignedFetch } from \"wb-auth\";\n * const id = await Identity.loadOrGenerate(\"./key.pem\", {\n * signatureAgentUrl: \"https://example.com/.well-known/.../<kid>\",\n * });\n * const sf = createSignedFetch(id);\n * const res = await sf(\"https://api.example.com/data\");\n */\n\nexport type { SignedFetchOptions } from \"./adapters/fetch.js\";\nexport { createSignedFetch } from \"./adapters/fetch.js\";\nexport type { ApplyToOptions } from \"./adapters/playwright.js\";\nexport { applyTo } from \"./adapters/playwright.js\";\nexport type { IdentityOptions, KeyPair } from \"./identity.js\";\nexport { DEFAULT_KEY_PATH, Identity } from \"./identity.js\";\nexport type { NormalizedRequest } from \"./normalized-request.js\";\nexport type { SignatureHeaders, SignOptions } from \"./signer.js\";\nexport { sign } from \"./signer.js\";\n\nexport const VERSION = \"0.3.0\";\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wb-auth",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Web Bot Auth (RFC 9421) TypeScript SDK
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Web Bot Auth (RFC 9421) TypeScript SDK — signed identity for AI agents and crawlers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
7
7
|
"module": "./dist/index.js",
|
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
"@types/node": "^20",
|
|
64
64
|
"playwright": "^1.59",
|
|
65
65
|
"tsup": "^8",
|
|
66
|
+
"tsx": "^4.23.15",
|
|
66
67
|
"typescript": "^6",
|
|
67
68
|
"vitest": "^4"
|
|
68
69
|
},
|