wb-auth 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,65 @@
1
- # wbauth (TypeScript)
1
+ # wb-auth — Web Bot Auth for TypeScript
2
+
3
+ [![npm](https://img.shields.io/npm/v/wb-auth?style=flat-square&color=111111&label=npm)](https://www.npmjs.com/package/wb-auth)
4
+
5
+ Ed25519 [RFC 9421](https://www.rfc-editor.org/rfc/rfc9421) request signing with the IETF
6
+ Web Bot Auth profile. Byte-identical to the Python SDK (`pip install wbauth`).
2
7
 
3
8
  ```bash
4
- npm install wbauth # Phase 5 — not yet published
9
+ npm install wb-auth
10
+ ```
11
+
12
+ Node ≥ 20. Ships ESM (`import`) and CommonJS (`require`) builds with types.
13
+
14
+ ## Sign `fetch`
15
+
16
+ ```ts
17
+ import { Identity, createSignedFetch } from "wb-auth";
18
+
19
+ const identity = await Identity.loadOrGenerate("~/.config/wbauth/key.pem", {
20
+ signatureAgentUrl: "https://agent.example.com/.well-known/http-message-signatures-directory",
21
+ userAgent: "ExampleAgent/1.0 (+https://agent.example.com)",
22
+ });
23
+
24
+ const signedFetch = createSignedFetch(identity);
25
+ await signedFetch("https://example.com/articles/42");
26
+ ```
27
+
28
+ - POST/PUT/PATCH bodies get a `Content-Digest` (RFC 9530) that is covered by the signature.
29
+ - Retries are safe: stale `Signature*` headers are replaced, never merged.
30
+ - `signal`, `redirect`, `credentials`, … from a `Request` input are preserved.
31
+
32
+ ## Sign a Playwright page
33
+
34
+ ```ts
35
+ import { chromium } from "playwright";
36
+ import { Identity, applyTo } from "wb-auth";
37
+
38
+ const page = await (await chromium.launch()).newPage();
39
+ // Only sign requests to the sites you target — third parties never see your identity.
40
+ await applyTo(page, identity, { hosts: ["example.com", "*.example.com"] });
41
+ await page.goto("https://example.com");
42
+ ```
43
+
44
+ ## Low-level
45
+
46
+ ```ts
47
+ import { sign } from "wb-auth";
48
+
49
+ const headers: Record<string, string> = {};
50
+ await sign({ method: "GET", url: "https://example.com/", headers, body: null }, identity);
51
+ // headers now has Signature, Signature-Input, Signature-Agent
5
52
  ```
6
53
 
7
- Phase 1: stub package — only used as the cross-language vector check oracle.
8
- Phase 4: full SDK with createSignedFetch + Playwright applyTo.
54
+ `sign()` throws if a POST/PUT/PATCH body has no `Content-Digest` header
55
+ (use `createSignedFetch`/`applyTo`, which add it automatically).
56
+
57
+ ## Keys
58
+
59
+ `Identity.loadOrGenerate(path, opts)` creates an Ed25519 key with mode `0600`
60
+ and refuses to load keys readable by others. `identity.exportJwks()` returns
61
+ the public JWKS to publish; `identity.rotate()` keeps the previous key in the
62
+ JWKS during the overlap window.
63
+
64
+ See the [repository README](../README.md) for the policy inspector (Python),
65
+ CLI and the hosted JWKS directory.
@@ -1,5 +1,36 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ DEFAULT_KEY_PATH: () => DEFAULT_KEY_PATH,
24
+ Identity: () => Identity,
25
+ VERSION: () => VERSION,
26
+ applyTo: () => applyTo,
27
+ createSignedFetch: () => createSignedFetch,
28
+ sign: () => sign
29
+ });
30
+ module.exports = __toCommonJS(index_exports);
31
+
1
32
  // src/signer.ts
2
- import { signatureHeaders } from "web-bot-auth";
33
+ var import_web_bot_auth = require("web-bot-auth");
3
34
  var DEFAULT_LABEL = "sig1";
4
35
  var DEFAULT_EXPIRES_SECONDS = 60;
5
36
  var DIGEST_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
@@ -10,20 +41,41 @@ function componentsFor(method, hasBody) {
10
41
  }
11
42
  return base;
12
43
  }
44
+ var RESIGN_HEADERS = ["signature", "signature-input", "signature-agent"];
45
+ function hasHeader(headers, name) {
46
+ const n = name.toLowerCase();
47
+ return Object.keys(headers).some((k) => k.toLowerCase() === n);
48
+ }
49
+ function removeHeaders(headers, names) {
50
+ for (const k of Object.keys(headers)) {
51
+ if (names.includes(k.toLowerCase())) delete headers[k];
52
+ }
53
+ }
13
54
  async function sign(request, identity, opts = {}) {
14
55
  if (!identity.signatureAgentUrl.startsWith("https://")) {
15
56
  throw new Error(
16
57
  `signatureAgentUrl must be https://, got: ${identity.signatureAgentUrl}`
17
58
  );
18
59
  }
60
+ const expiresAfterSeconds = opts.expiresAfterSeconds ?? DEFAULT_EXPIRES_SECONDS;
61
+ if (!Number.isFinite(expiresAfterSeconds) || expiresAfterSeconds <= 0) {
62
+ throw new RangeError(
63
+ `expiresAfterSeconds must be a positive number, got: ${expiresAfterSeconds}`
64
+ );
65
+ }
66
+ removeHeaders(request.headers, RESIGN_HEADERS);
19
67
  const signatureAgentHeader = `"${identity.signatureAgentUrl}"`;
20
68
  request.headers["Signature-Agent"] = signatureAgentHeader;
21
69
  const created = opts.created ?? /* @__PURE__ */ new Date();
22
- const expiresAfterSeconds = opts.expiresAfterSeconds ?? DEFAULT_EXPIRES_SECONDS;
23
70
  const expires = new Date(created.getTime() + expiresAfterSeconds * 1e3);
24
71
  const label = opts.label ?? DEFAULT_LABEL;
25
- const hasBody = request.body !== null && request.body !== void 0;
72
+ const hasBody = request.body !== null && request.body !== void 0 && request.body.byteLength > 0;
26
73
  const components = componentsFor(request.method, hasBody);
74
+ if (components.includes("content-digest") && !hasHeader(request.headers, "content-digest")) {
75
+ throw new Error(
76
+ 'Covered header field "content-digest" not found in the message. Call ensureContentDigest() or use createSignedFetch()/applyTo(), which add it automatically.'
77
+ );
78
+ }
27
79
  const headers = new Headers();
28
80
  for (const [k, v] of Object.entries(request.headers)) headers.set(k, v);
29
81
  const init = { method: request.method, headers };
@@ -36,7 +88,7 @@ async function sign(request, identity, opts = {}) {
36
88
  components,
37
89
  ...opts.nonce !== void 0 ? { nonce: opts.nonce } : {}
38
90
  };
39
- const result = await signatureHeaders(req, identity._signer(), params);
91
+ const result = await (0, import_web_bot_auth.signatureHeaders)(req, identity._signer(), params);
40
92
  request.headers.Signature = result.Signature;
41
93
  request.headers["Signature-Input"] = result["Signature-Input"];
42
94
  return {
@@ -47,20 +99,46 @@ async function sign(request, identity, opts = {}) {
47
99
  }
48
100
 
49
101
  // src/adapters/_utils.ts
50
- import { createHash } from "crypto";
102
+ var import_node_crypto = require("crypto");
51
103
  var DIGEST_METHODS2 = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
104
+ var LIBRARY_DEFAULT_UA = /^(node|undici|node-fetch)(\/|$)/i;
105
+ function findHeader(headers, name) {
106
+ const n = name.toLowerCase();
107
+ return Object.keys(headers).find((k) => k.toLowerCase() === n);
108
+ }
52
109
  function ensureContentDigest(method, headers, body) {
53
110
  if (!body || body.byteLength === 0) return;
54
111
  if (!DIGEST_METHODS2.has(method.toUpperCase())) return;
55
- if (Object.keys(headers).some((k) => k.toLowerCase() === "content-digest")) {
112
+ if (findHeader(headers, "content-digest")) return;
113
+ const b64 = (0, import_node_crypto.createHash)("sha256").update(body).digest("base64");
114
+ headers["Content-Digest"] = `sha-256=:${b64}:`;
115
+ }
116
+ function applyUserAgent(headers, userAgent) {
117
+ if (!userAgent) return;
118
+ const key = findHeader(headers, "user-agent");
119
+ if (key === void 0) {
120
+ headers["User-Agent"] = userAgent;
56
121
  return;
57
122
  }
58
- const b64 = createHash("sha256").update(body).digest("base64");
59
- headers["Content-Digest"] = `sha-256=:${b64}:`;
123
+ if (LIBRARY_DEFAULT_UA.test(headers[key] ?? "")) {
124
+ delete headers[key];
125
+ headers["User-Agent"] = userAgent;
126
+ }
127
+ }
128
+ function hostMatches(hostname, allow) {
129
+ const h = hostname.toLowerCase().replace(/\.$/, "");
130
+ return allow.some((raw) => {
131
+ const a = raw.toLowerCase().replace(/\.$/, "");
132
+ if (a.startsWith("*.")) {
133
+ const base = a.slice(2);
134
+ return h === base || h.endsWith(`.${base}`);
135
+ }
136
+ return h === a;
137
+ });
60
138
  }
61
139
 
62
140
  // src/adapters/fetch.ts
63
- function createSignedFetch(identity) {
141
+ function createSignedFetch(identity, options = {}) {
64
142
  return async function signedFetch(input, init = {}) {
65
143
  const req = new Request(input, init);
66
144
  const method = req.method;
@@ -76,52 +154,68 @@ function createSignedFetch(identity) {
76
154
  });
77
155
  ensureContentDigest(method, headers, body);
78
156
  await sign({ method, url, headers, body }, identity);
79
- if (identity.userAgent && !Object.keys(headers).some((k) => k.toLowerCase() === "user-agent")) {
80
- headers["User-Agent"] = identity.userAgent;
81
- }
82
- return fetch(url, {
157
+ applyUserAgent(headers, identity.userAgent);
158
+ const outInit = {
83
159
  ...init,
84
160
  method,
85
161
  headers,
86
- body: body ?? void 0
87
- });
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;
174
+ const doFetch = options.fetch ?? globalThis.fetch;
175
+ return doFetch(url, outInit);
88
176
  };
89
177
  }
90
178
 
91
179
  // src/adapters/playwright.ts
92
- async function applyTo(page, identity) {
93
- await page.route("**/*", async (route, request) => {
180
+ async function applyTo(page, identity, options = {}) {
181
+ const pattern = options.pattern ?? "**/*";
182
+ const decide = (raw) => {
183
+ let u;
184
+ try {
185
+ u = new URL(raw);
186
+ } catch {
187
+ return false;
188
+ }
189
+ if (u.protocol !== "https:" && u.protocol !== "http:") return false;
190
+ if (options.shouldSign) return options.shouldSign(u);
191
+ if (options.hosts) return hostMatches(u.hostname, options.hosts);
192
+ return true;
193
+ };
194
+ await page.route(pattern, async (route, request) => {
195
+ const url = request.url();
196
+ if (!decide(url)) {
197
+ const r = route;
198
+ await (r.fallback ? r.fallback() : route.continue());
199
+ return;
200
+ }
94
201
  const headers = await request.allHeaders();
95
202
  const postData = request.postDataBuffer();
96
203
  const body = postData ? new Uint8Array(postData) : null;
97
204
  ensureContentDigest(request.method(), headers, body);
98
- await sign(
99
- { method: request.method(), url: request.url(), headers, body },
100
- identity
101
- );
102
- if (identity.userAgent && !Object.keys(headers).some((k) => k.toLowerCase() === "user-agent")) {
103
- headers["User-Agent"] = identity.userAgent;
104
- }
205
+ await sign({ method: request.method(), url, headers, body }, identity);
206
+ applyUserAgent(headers, identity.userAgent);
105
207
  await route.continue({ headers });
106
208
  });
107
209
  }
108
210
 
109
211
  // src/identity.ts
110
- import { createPrivateKey, generateKeyPairSync } from "crypto";
111
- import {
112
- closeSync,
113
- existsSync,
114
- mkdirSync,
115
- openSync,
116
- readFileSync,
117
- statSync,
118
- writeFileSync
119
- } from "fs";
120
- import { homedir } from "os";
121
- import { dirname, resolve } from "path";
122
- import { signerFromJWK } from "web-bot-auth/crypto";
123
- var DEFAULT_KEY_PATH = resolve(
124
- homedir(),
212
+ var import_node_crypto2 = require("crypto");
213
+ var import_node_fs = require("fs");
214
+ var import_node_os = require("os");
215
+ var import_node_path = require("path");
216
+ var import_crypto = require("web-bot-auth/crypto");
217
+ var DEFAULT_KEY_PATH = (0, import_node_path.resolve)(
218
+ (0, import_node_os.homedir)(),
125
219
  ".config",
126
220
  "wbauth",
127
221
  "key.pem"
@@ -191,7 +285,7 @@ var Identity = class _Identity {
191
285
  */
192
286
  static async loadOrGenerate(path = DEFAULT_KEY_PATH, opts) {
193
287
  const resolved = expandUser(path);
194
- const pair = existsSync(resolved) ? await loadKeypair(resolved) : await generateKeypairTo(resolved);
288
+ const pair = (0, import_node_fs.existsSync)(resolved) ? await loadKeypair(resolved) : await generateKeypairTo(resolved);
195
289
  return new _Identity(pair, opts);
196
290
  }
197
291
  /**
@@ -199,9 +293,9 @@ var Identity = class _Identity {
199
293
  * For tests + Cloudflare debug smoke checks. NEVER use in production.
200
294
  * Does NOT write any file to disk.
201
295
  */
202
- static async fromTestKey(signatureAgentUrl) {
296
+ static async fromTestKey(signatureAgentUrl, opts = {}) {
203
297
  const pair = await keyPairFromJwk(TEST_KEY_JWK);
204
- return new _Identity(pair, { signatureAgentUrl });
298
+ return new _Identity(pair, { ...opts, signatureAgentUrl });
205
299
  }
206
300
  // ---------- Redaction guarantees (IDENT-08) ----------
207
301
  toString() {
@@ -213,10 +307,10 @@ var Identity = class _Identity {
213
307
  }
214
308
  };
215
309
  function expandUser(path) {
216
- return resolve(path.startsWith("~") ? path.replace(/^~/, homedir()) : path);
310
+ return (0, import_node_path.resolve)(path.startsWith("~") ? path.replace(/^~/, (0, import_node_os.homedir)()) : path);
217
311
  }
218
312
  async function keyPairFromJwk(privateJwk) {
219
- const signer = await signerFromJWK(privateJwk);
313
+ const signer = await (0, import_crypto.signerFromJWK)(privateJwk);
220
314
  const kid = signer.keyid;
221
315
  const publicJwk = {
222
316
  kty: privateJwk.kty,
@@ -228,7 +322,7 @@ async function keyPairFromJwk(privateJwk) {
228
322
  }
229
323
  async function loadKeypair(path) {
230
324
  if (process.platform !== "win32") {
231
- const mode = statSync(path).mode & 511;
325
+ const mode = (0, import_node_fs.statSync)(path).mode & 511;
232
326
  if ((mode & 63) !== 0) {
233
327
  throw new Error(
234
328
  `Key file ${path} has mode 0o${mode.toString(8)}; expected 0o600. Fix: chmod 600 ${path}`
@@ -239,38 +333,40 @@ async function loadKeypair(path) {
239
333
  `Windows detected \u2014 file permissions on ${path} cannot be enforced via POSIX mode. Use NTFS ACLs or store the key in a per-user secrets vault.`
240
334
  );
241
335
  }
242
- const pem = readFileSync(path, "utf8");
243
- const jwk = createPrivateKey(pem).export({ format: "jwk" });
336
+ const pem = (0, import_node_fs.readFileSync)(path, "utf8");
337
+ const jwk = (0, import_node_crypto2.createPrivateKey)(pem).export({ format: "jwk" });
244
338
  if (jwk.kty !== "OKP" || jwk.crv !== "Ed25519") {
245
339
  throw new TypeError(`Expected Ed25519, got kty=${jwk.kty}, crv=${jwk.crv}`);
246
340
  }
247
341
  return keyPairFromJwk(jwk);
248
342
  }
249
343
  async function generateKeypairTo(path) {
250
- const dir = dirname(path);
251
- mkdirSync(dir, { recursive: true });
252
- if (existsSync(path)) {
344
+ const dir = (0, import_node_path.dirname)(path);
345
+ (0, import_node_fs.mkdirSync)(dir, { recursive: true });
346
+ if ((0, import_node_fs.existsSync)(path)) {
253
347
  throw new Error(`Key already exists at ${path}; refuse overwrite`);
254
348
  }
255
- const { privateKey } = generateKeyPairSync("ed25519");
349
+ const { privateKey } = (0, import_node_crypto2.generateKeyPairSync)("ed25519");
256
350
  const pem = privateKey.export({ format: "pem", type: "pkcs8" });
257
- const fd = openSync(path, "wx", 384);
351
+ const fd = (0, import_node_fs.openSync)(path, "wx", 384);
258
352
  try {
259
- writeFileSync(fd, pem);
353
+ (0, import_node_fs.writeFileSync)(fd, pem);
260
354
  } finally {
261
- closeSync(fd);
355
+ (0, import_node_fs.closeSync)(fd);
262
356
  }
263
357
  const jwk = privateKey.export({ format: "jwk" });
264
358
  return keyPairFromJwk(jwk);
265
359
  }
266
360
 
267
361
  // src/index.ts
268
- var VERSION = "0.1.0";
269
- export {
362
+ var VERSION = "0.2.0";
363
+ // Annotate the CommonJS export names for ESM import in node:
364
+ 0 && (module.exports = {
270
365
  DEFAULT_KEY_PATH,
271
366
  Identity,
272
367
  VERSION,
273
368
  applyTo,
274
369
  createSignedFetch,
275
370
  sign
276
- };
371
+ });
372
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +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":"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","names":["DIGEST_METHODS","import_node_crypto"]}
package/dist/index.d.cts CHANGED
@@ -60,7 +60,9 @@ declare class Identity {
60
60
  * first. Verifiers select by `kid`. Mirror of Python `export_jwks`.
61
61
  */
62
62
  exportJwks(): {
63
- keys: JsonWebKey[];
63
+ keys: Array<JsonWebKey & {
64
+ kid: string;
65
+ }>;
64
66
  };
65
67
  /**
66
68
  * IDENT-07: generate a new active key, demote current to retiring.
@@ -80,7 +82,7 @@ declare class Identity {
80
82
  * For tests + Cloudflare debug smoke checks. NEVER use in production.
81
83
  * Does NOT write any file to disk.
82
84
  */
83
- static fromTestKey(signatureAgentUrl: string): Promise<Identity>;
85
+ static fromTestKey(signatureAgentUrl: string, opts?: Omit<IdentityOptions, "signatureAgentUrl">): Promise<Identity>;
84
86
  toString(): string;
85
87
  }
86
88
 
@@ -91,32 +93,45 @@ declare class Identity {
91
93
  * `identity` only. Returns a function whose signature matches `typeof fetch`,
92
94
  * so user code can swap `fetch` for `signedFetch` without other changes.
93
95
  *
94
- * v1 limitation (Pitfall 4): streaming bodies are NOT supported. The adapter
95
- * reads the full body via `req.clone().arrayBuffer()` to compute Content-Digest.
96
+ * - Request-object inputs keep their `signal`, `redirect`, `credentials`,
97
+ * `cache`, `keepalive`, `integrity` and `referrerPolicy`.
98
+ * - Re-sending an already signed request (retry wrappers) produces exactly one
99
+ * fresh signature — stale Signature* headers are replaced, never merged.
100
+ *
101
+ * v1 limitation: streaming bodies are buffered (`req.clone().arrayBuffer()`)
102
+ * so Content-Digest can be computed.
96
103
  */
97
104
 
98
- declare function createSignedFetch(identity: Identity): typeof fetch;
105
+ interface SignedFetchOptions {
106
+ /** Underlying fetch implementation. Defaults to `globalThis.fetch` at call time. */
107
+ fetch?: typeof fetch;
108
+ }
109
+ declare function createSignedFetch(identity: Identity, options?: SignedFetchOptions): typeof fetch;
99
110
 
100
111
  /**
101
- * applyTo — Playwright async helper for Web Bot Auth (ADAPT-05).
112
+ * applyTo — sign every matching request a Playwright page sends.
102
113
  *
103
- * Mirrors python/src/wbauth/adapters/playwright.py. Registers a route
104
- * handler that intercepts every outgoing request, signs it via wbauth.sign(),
105
- * and continues with the signed headers.
114
+ * Mirrors python/src/wbauth/adapters/playwright.py.
106
115
  *
107
- * Pitfall 3: Call applyTo(page, identity) BEFORE the first page.goto()/click
108
- * that should produce signed requests. page.route must be registered before
109
- * navigation begins, or the navigation request leaves the browser unsigned.
116
+ * Privacy: by default every http(s) request is signed, including third-party
117
+ * analytics/CDN hosts, which reveals your agent identity to them. Pass
118
+ * `hosts` to restrict signing to the sites you actually target:
110
119
  *
111
- * Pitfall (iframe coverage): page.route('**\/*', handler) covers all
112
- * sub-frame requests by default. If the target site uses Service Workers,
113
- * set serviceWorkers="block" on the BrowserContext (Playwright option).
120
+ * await applyTo(page, identity, { hosts: ["example.com", "*.example.org"] });
114
121
  *
115
- * Note: `playwright` is a peerDependency (optional). End users only install
116
- * it if they actually use this adapter — fetch-only consumers pay zero cost.
122
+ * Non-http(s) requests (data:, blob:, chrome-extension:) are never signed.
123
+ * Note: Playwright disables the HTTP cache while a route is active.
117
124
  */
118
125
 
119
- declare function applyTo(page: Page, identity: Identity): Promise<void>;
126
+ interface ApplyToOptions {
127
+ /** Only sign requests to these hosts (`example.com`, `*.example.com`). */
128
+ hosts?: readonly string[];
129
+ /** Custom predicate; takes precedence over `hosts`. */
130
+ shouldSign?: (url: URL) => boolean;
131
+ /** Route pattern passed to `page.route`. Default `"**\/*"`. */
132
+ pattern?: string;
133
+ }
134
+ declare function applyTo(page: Page, identity: Identity, options?: ApplyToOptions): Promise<void>;
120
135
 
121
136
  /**
122
137
  * NormalizedRequest — the structural input to wbauth.sign().
@@ -172,11 +187,11 @@ declare function sign(request: NormalizedRequest, identity: Identity, opts?: Sig
172
187
  /**
173
188
  * wbauth: Web Bot Auth (RFC 9421) TypeScript SDK — Phase 4 public surface.
174
189
  *
175
- * Per CONTEXT D-74: flat root exports. Subpath exports (`wbauth/adapters`)
190
+ * Per CONTEXT D-74: flat root exports. Subpath exports (`wb-auth/adapters`)
176
191
  * are a v1.x optimization, not required for Phase 4.
177
192
  *
178
193
  * Quickstart:
179
- * import { Identity, createSignedFetch } from "wbauth";
194
+ * import { Identity, createSignedFetch } from "wb-auth";
180
195
  * const id = await Identity.loadOrGenerate("./key.pem", {
181
196
  * signatureAgentUrl: "https://example.com/.well-known/.../<kid>",
182
197
  * });
@@ -184,6 +199,6 @@ declare function sign(request: NormalizedRequest, identity: Identity, opts?: Sig
184
199
  * const res = await sf("https://api.example.com/data");
185
200
  */
186
201
 
187
- declare const VERSION = "0.1.0";
202
+ declare const VERSION = "0.2.0";
188
203
 
189
- export { DEFAULT_KEY_PATH, Identity, type IdentityOptions, type KeyPair, type NormalizedRequest, type SignOptions, type SignatureHeaders, VERSION, applyTo, createSignedFetch, sign };
204
+ 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
@@ -60,7 +60,9 @@ declare class Identity {
60
60
  * first. Verifiers select by `kid`. Mirror of Python `export_jwks`.
61
61
  */
62
62
  exportJwks(): {
63
- keys: JsonWebKey[];
63
+ keys: Array<JsonWebKey & {
64
+ kid: string;
65
+ }>;
64
66
  };
65
67
  /**
66
68
  * IDENT-07: generate a new active key, demote current to retiring.
@@ -80,7 +82,7 @@ declare class Identity {
80
82
  * For tests + Cloudflare debug smoke checks. NEVER use in production.
81
83
  * Does NOT write any file to disk.
82
84
  */
83
- static fromTestKey(signatureAgentUrl: string): Promise<Identity>;
85
+ static fromTestKey(signatureAgentUrl: string, opts?: Omit<IdentityOptions, "signatureAgentUrl">): Promise<Identity>;
84
86
  toString(): string;
85
87
  }
86
88
 
@@ -91,32 +93,45 @@ declare class Identity {
91
93
  * `identity` only. Returns a function whose signature matches `typeof fetch`,
92
94
  * so user code can swap `fetch` for `signedFetch` without other changes.
93
95
  *
94
- * v1 limitation (Pitfall 4): streaming bodies are NOT supported. The adapter
95
- * reads the full body via `req.clone().arrayBuffer()` to compute Content-Digest.
96
+ * - Request-object inputs keep their `signal`, `redirect`, `credentials`,
97
+ * `cache`, `keepalive`, `integrity` and `referrerPolicy`.
98
+ * - Re-sending an already signed request (retry wrappers) produces exactly one
99
+ * fresh signature — stale Signature* headers are replaced, never merged.
100
+ *
101
+ * v1 limitation: streaming bodies are buffered (`req.clone().arrayBuffer()`)
102
+ * so Content-Digest can be computed.
96
103
  */
97
104
 
98
- declare function createSignedFetch(identity: Identity): typeof fetch;
105
+ interface SignedFetchOptions {
106
+ /** Underlying fetch implementation. Defaults to `globalThis.fetch` at call time. */
107
+ fetch?: typeof fetch;
108
+ }
109
+ declare function createSignedFetch(identity: Identity, options?: SignedFetchOptions): typeof fetch;
99
110
 
100
111
  /**
101
- * applyTo — Playwright async helper for Web Bot Auth (ADAPT-05).
112
+ * applyTo — sign every matching request a Playwright page sends.
102
113
  *
103
- * Mirrors python/src/wbauth/adapters/playwright.py. Registers a route
104
- * handler that intercepts every outgoing request, signs it via wbauth.sign(),
105
- * and continues with the signed headers.
114
+ * Mirrors python/src/wbauth/adapters/playwright.py.
106
115
  *
107
- * Pitfall 3: Call applyTo(page, identity) BEFORE the first page.goto()/click
108
- * that should produce signed requests. page.route must be registered before
109
- * navigation begins, or the navigation request leaves the browser unsigned.
116
+ * Privacy: by default every http(s) request is signed, including third-party
117
+ * analytics/CDN hosts, which reveals your agent identity to them. Pass
118
+ * `hosts` to restrict signing to the sites you actually target:
110
119
  *
111
- * Pitfall (iframe coverage): page.route('**\/*', handler) covers all
112
- * sub-frame requests by default. If the target site uses Service Workers,
113
- * set serviceWorkers="block" on the BrowserContext (Playwright option).
120
+ * await applyTo(page, identity, { hosts: ["example.com", "*.example.org"] });
114
121
  *
115
- * Note: `playwright` is a peerDependency (optional). End users only install
116
- * it if they actually use this adapter — fetch-only consumers pay zero cost.
122
+ * Non-http(s) requests (data:, blob:, chrome-extension:) are never signed.
123
+ * Note: Playwright disables the HTTP cache while a route is active.
117
124
  */
118
125
 
119
- declare function applyTo(page: Page, identity: Identity): Promise<void>;
126
+ interface ApplyToOptions {
127
+ /** Only sign requests to these hosts (`example.com`, `*.example.com`). */
128
+ hosts?: readonly string[];
129
+ /** Custom predicate; takes precedence over `hosts`. */
130
+ shouldSign?: (url: URL) => boolean;
131
+ /** Route pattern passed to `page.route`. Default `"**\/*"`. */
132
+ pattern?: string;
133
+ }
134
+ declare function applyTo(page: Page, identity: Identity, options?: ApplyToOptions): Promise<void>;
120
135
 
121
136
  /**
122
137
  * NormalizedRequest — the structural input to wbauth.sign().
@@ -172,11 +187,11 @@ declare function sign(request: NormalizedRequest, identity: Identity, opts?: Sig
172
187
  /**
173
188
  * wbauth: Web Bot Auth (RFC 9421) TypeScript SDK — Phase 4 public surface.
174
189
  *
175
- * Per CONTEXT D-74: flat root exports. Subpath exports (`wbauth/adapters`)
190
+ * Per CONTEXT D-74: flat root exports. Subpath exports (`wb-auth/adapters`)
176
191
  * are a v1.x optimization, not required for Phase 4.
177
192
  *
178
193
  * Quickstart:
179
- * import { Identity, createSignedFetch } from "wbauth";
194
+ * import { Identity, createSignedFetch } from "wb-auth";
180
195
  * const id = await Identity.loadOrGenerate("./key.pem", {
181
196
  * signatureAgentUrl: "https://example.com/.well-known/.../<kid>",
182
197
  * });
@@ -184,6 +199,6 @@ declare function sign(request: NormalizedRequest, identity: Identity, opts?: Sig
184
199
  * const res = await sf("https://api.example.com/data");
185
200
  */
186
201
 
187
- declare const VERSION = "0.1.0";
202
+ declare const VERSION = "0.2.0";
188
203
 
189
- export { DEFAULT_KEY_PATH, Identity, type IdentityOptions, type KeyPair, type NormalizedRequest, type SignOptions, type SignatureHeaders, VERSION, applyTo, createSignedFetch, sign };
204
+ 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
@@ -1,36 +1,5 @@
1
- "use strict";
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
9
- };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
- }
16
- return to;
17
- };
18
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
-
20
- // src/index.ts
21
- var index_exports = {};
22
- __export(index_exports, {
23
- DEFAULT_KEY_PATH: () => DEFAULT_KEY_PATH,
24
- Identity: () => Identity,
25
- VERSION: () => VERSION,
26
- applyTo: () => applyTo,
27
- createSignedFetch: () => createSignedFetch,
28
- sign: () => sign
29
- });
30
- module.exports = __toCommonJS(index_exports);
31
-
32
1
  // src/signer.ts
33
- var import_web_bot_auth = require("web-bot-auth");
2
+ import { signatureHeaders } from "web-bot-auth";
34
3
  var DEFAULT_LABEL = "sig1";
35
4
  var DEFAULT_EXPIRES_SECONDS = 60;
36
5
  var DIGEST_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
@@ -41,20 +10,41 @@ function componentsFor(method, hasBody) {
41
10
  }
42
11
  return base;
43
12
  }
13
+ var RESIGN_HEADERS = ["signature", "signature-input", "signature-agent"];
14
+ function hasHeader(headers, name) {
15
+ const n = name.toLowerCase();
16
+ return Object.keys(headers).some((k) => k.toLowerCase() === n);
17
+ }
18
+ function removeHeaders(headers, names) {
19
+ for (const k of Object.keys(headers)) {
20
+ if (names.includes(k.toLowerCase())) delete headers[k];
21
+ }
22
+ }
44
23
  async function sign(request, identity, opts = {}) {
45
24
  if (!identity.signatureAgentUrl.startsWith("https://")) {
46
25
  throw new Error(
47
26
  `signatureAgentUrl must be https://, got: ${identity.signatureAgentUrl}`
48
27
  );
49
28
  }
29
+ const expiresAfterSeconds = opts.expiresAfterSeconds ?? DEFAULT_EXPIRES_SECONDS;
30
+ if (!Number.isFinite(expiresAfterSeconds) || expiresAfterSeconds <= 0) {
31
+ throw new RangeError(
32
+ `expiresAfterSeconds must be a positive number, got: ${expiresAfterSeconds}`
33
+ );
34
+ }
35
+ removeHeaders(request.headers, RESIGN_HEADERS);
50
36
  const signatureAgentHeader = `"${identity.signatureAgentUrl}"`;
51
37
  request.headers["Signature-Agent"] = signatureAgentHeader;
52
38
  const created = opts.created ?? /* @__PURE__ */ new Date();
53
- const expiresAfterSeconds = opts.expiresAfterSeconds ?? DEFAULT_EXPIRES_SECONDS;
54
39
  const expires = new Date(created.getTime() + expiresAfterSeconds * 1e3);
55
40
  const label = opts.label ?? DEFAULT_LABEL;
56
- const hasBody = request.body !== null && request.body !== void 0;
41
+ const hasBody = request.body !== null && request.body !== void 0 && request.body.byteLength > 0;
57
42
  const components = componentsFor(request.method, hasBody);
43
+ if (components.includes("content-digest") && !hasHeader(request.headers, "content-digest")) {
44
+ throw new Error(
45
+ 'Covered header field "content-digest" not found in the message. Call ensureContentDigest() or use createSignedFetch()/applyTo(), which add it automatically.'
46
+ );
47
+ }
58
48
  const headers = new Headers();
59
49
  for (const [k, v] of Object.entries(request.headers)) headers.set(k, v);
60
50
  const init = { method: request.method, headers };
@@ -67,7 +57,7 @@ async function sign(request, identity, opts = {}) {
67
57
  components,
68
58
  ...opts.nonce !== void 0 ? { nonce: opts.nonce } : {}
69
59
  };
70
- const result = await (0, import_web_bot_auth.signatureHeaders)(req, identity._signer(), params);
60
+ const result = await signatureHeaders(req, identity._signer(), params);
71
61
  request.headers.Signature = result.Signature;
72
62
  request.headers["Signature-Input"] = result["Signature-Input"];
73
63
  return {
@@ -78,20 +68,46 @@ async function sign(request, identity, opts = {}) {
78
68
  }
79
69
 
80
70
  // src/adapters/_utils.ts
81
- var import_node_crypto = require("crypto");
71
+ import { createHash } from "crypto";
82
72
  var DIGEST_METHODS2 = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
73
+ var LIBRARY_DEFAULT_UA = /^(node|undici|node-fetch)(\/|$)/i;
74
+ function findHeader(headers, name) {
75
+ const n = name.toLowerCase();
76
+ return Object.keys(headers).find((k) => k.toLowerCase() === n);
77
+ }
83
78
  function ensureContentDigest(method, headers, body) {
84
79
  if (!body || body.byteLength === 0) return;
85
80
  if (!DIGEST_METHODS2.has(method.toUpperCase())) return;
86
- if (Object.keys(headers).some((k) => k.toLowerCase() === "content-digest")) {
81
+ if (findHeader(headers, "content-digest")) return;
82
+ const b64 = createHash("sha256").update(body).digest("base64");
83
+ headers["Content-Digest"] = `sha-256=:${b64}:`;
84
+ }
85
+ function applyUserAgent(headers, userAgent) {
86
+ if (!userAgent) return;
87
+ const key = findHeader(headers, "user-agent");
88
+ if (key === void 0) {
89
+ headers["User-Agent"] = userAgent;
87
90
  return;
88
91
  }
89
- const b64 = (0, import_node_crypto.createHash)("sha256").update(body).digest("base64");
90
- headers["Content-Digest"] = `sha-256=:${b64}:`;
92
+ if (LIBRARY_DEFAULT_UA.test(headers[key] ?? "")) {
93
+ delete headers[key];
94
+ headers["User-Agent"] = userAgent;
95
+ }
96
+ }
97
+ function hostMatches(hostname, allow) {
98
+ const h = hostname.toLowerCase().replace(/\.$/, "");
99
+ return allow.some((raw) => {
100
+ const a = raw.toLowerCase().replace(/\.$/, "");
101
+ if (a.startsWith("*.")) {
102
+ const base = a.slice(2);
103
+ return h === base || h.endsWith(`.${base}`);
104
+ }
105
+ return h === a;
106
+ });
91
107
  }
92
108
 
93
109
  // src/adapters/fetch.ts
94
- function createSignedFetch(identity) {
110
+ function createSignedFetch(identity, options = {}) {
95
111
  return async function signedFetch(input, init = {}) {
96
112
  const req = new Request(input, init);
97
113
  const method = req.method;
@@ -107,44 +123,76 @@ function createSignedFetch(identity) {
107
123
  });
108
124
  ensureContentDigest(method, headers, body);
109
125
  await sign({ method, url, headers, body }, identity);
110
- if (identity.userAgent && !Object.keys(headers).some((k) => k.toLowerCase() === "user-agent")) {
111
- headers["User-Agent"] = identity.userAgent;
112
- }
113
- return fetch(url, {
126
+ applyUserAgent(headers, identity.userAgent);
127
+ const outInit = {
114
128
  ...init,
115
129
  method,
116
130
  headers,
117
- body: body ?? void 0
118
- });
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;
143
+ const doFetch = options.fetch ?? globalThis.fetch;
144
+ return doFetch(url, outInit);
119
145
  };
120
146
  }
121
147
 
122
148
  // src/adapters/playwright.ts
123
- async function applyTo(page, identity) {
124
- await page.route("**/*", async (route, request) => {
149
+ async function applyTo(page, identity, options = {}) {
150
+ const pattern = options.pattern ?? "**/*";
151
+ const decide = (raw) => {
152
+ let u;
153
+ try {
154
+ u = new URL(raw);
155
+ } catch {
156
+ return false;
157
+ }
158
+ if (u.protocol !== "https:" && u.protocol !== "http:") return false;
159
+ if (options.shouldSign) return options.shouldSign(u);
160
+ if (options.hosts) return hostMatches(u.hostname, options.hosts);
161
+ return true;
162
+ };
163
+ await page.route(pattern, async (route, request) => {
164
+ const url = request.url();
165
+ if (!decide(url)) {
166
+ const r = route;
167
+ await (r.fallback ? r.fallback() : route.continue());
168
+ return;
169
+ }
125
170
  const headers = await request.allHeaders();
126
171
  const postData = request.postDataBuffer();
127
172
  const body = postData ? new Uint8Array(postData) : null;
128
173
  ensureContentDigest(request.method(), headers, body);
129
- await sign(
130
- { method: request.method(), url: request.url(), headers, body },
131
- identity
132
- );
133
- if (identity.userAgent && !Object.keys(headers).some((k) => k.toLowerCase() === "user-agent")) {
134
- headers["User-Agent"] = identity.userAgent;
135
- }
174
+ await sign({ method: request.method(), url, headers, body }, identity);
175
+ applyUserAgent(headers, identity.userAgent);
136
176
  await route.continue({ headers });
137
177
  });
138
178
  }
139
179
 
140
180
  // src/identity.ts
141
- var import_node_crypto2 = require("crypto");
142
- var import_node_fs = require("fs");
143
- var import_node_os = require("os");
144
- var import_node_path = require("path");
145
- var import_crypto = require("web-bot-auth/crypto");
146
- var DEFAULT_KEY_PATH = (0, import_node_path.resolve)(
147
- (0, import_node_os.homedir)(),
181
+ import { createPrivateKey, generateKeyPairSync } from "crypto";
182
+ import {
183
+ closeSync,
184
+ existsSync,
185
+ mkdirSync,
186
+ openSync,
187
+ readFileSync,
188
+ statSync,
189
+ writeFileSync
190
+ } from "fs";
191
+ import { homedir } from "os";
192
+ import { dirname, resolve } from "path";
193
+ import { signerFromJWK } from "web-bot-auth/crypto";
194
+ var DEFAULT_KEY_PATH = resolve(
195
+ homedir(),
148
196
  ".config",
149
197
  "wbauth",
150
198
  "key.pem"
@@ -214,7 +262,7 @@ var Identity = class _Identity {
214
262
  */
215
263
  static async loadOrGenerate(path = DEFAULT_KEY_PATH, opts) {
216
264
  const resolved = expandUser(path);
217
- const pair = (0, import_node_fs.existsSync)(resolved) ? await loadKeypair(resolved) : await generateKeypairTo(resolved);
265
+ const pair = existsSync(resolved) ? await loadKeypair(resolved) : await generateKeypairTo(resolved);
218
266
  return new _Identity(pair, opts);
219
267
  }
220
268
  /**
@@ -222,9 +270,9 @@ var Identity = class _Identity {
222
270
  * For tests + Cloudflare debug smoke checks. NEVER use in production.
223
271
  * Does NOT write any file to disk.
224
272
  */
225
- static async fromTestKey(signatureAgentUrl) {
273
+ static async fromTestKey(signatureAgentUrl, opts = {}) {
226
274
  const pair = await keyPairFromJwk(TEST_KEY_JWK);
227
- return new _Identity(pair, { signatureAgentUrl });
275
+ return new _Identity(pair, { ...opts, signatureAgentUrl });
228
276
  }
229
277
  // ---------- Redaction guarantees (IDENT-08) ----------
230
278
  toString() {
@@ -236,10 +284,10 @@ var Identity = class _Identity {
236
284
  }
237
285
  };
238
286
  function expandUser(path) {
239
- return (0, import_node_path.resolve)(path.startsWith("~") ? path.replace(/^~/, (0, import_node_os.homedir)()) : path);
287
+ return resolve(path.startsWith("~") ? path.replace(/^~/, homedir()) : path);
240
288
  }
241
289
  async function keyPairFromJwk(privateJwk) {
242
- const signer = await (0, import_crypto.signerFromJWK)(privateJwk);
290
+ const signer = await signerFromJWK(privateJwk);
243
291
  const kid = signer.keyid;
244
292
  const publicJwk = {
245
293
  kty: privateJwk.kty,
@@ -251,7 +299,7 @@ async function keyPairFromJwk(privateJwk) {
251
299
  }
252
300
  async function loadKeypair(path) {
253
301
  if (process.platform !== "win32") {
254
- const mode = (0, import_node_fs.statSync)(path).mode & 511;
302
+ const mode = statSync(path).mode & 511;
255
303
  if ((mode & 63) !== 0) {
256
304
  throw new Error(
257
305
  `Key file ${path} has mode 0o${mode.toString(8)}; expected 0o600. Fix: chmod 600 ${path}`
@@ -262,39 +310,39 @@ async function loadKeypair(path) {
262
310
  `Windows detected \u2014 file permissions on ${path} cannot be enforced via POSIX mode. Use NTFS ACLs or store the key in a per-user secrets vault.`
263
311
  );
264
312
  }
265
- const pem = (0, import_node_fs.readFileSync)(path, "utf8");
266
- const jwk = (0, import_node_crypto2.createPrivateKey)(pem).export({ format: "jwk" });
313
+ const pem = readFileSync(path, "utf8");
314
+ const jwk = createPrivateKey(pem).export({ format: "jwk" });
267
315
  if (jwk.kty !== "OKP" || jwk.crv !== "Ed25519") {
268
316
  throw new TypeError(`Expected Ed25519, got kty=${jwk.kty}, crv=${jwk.crv}`);
269
317
  }
270
318
  return keyPairFromJwk(jwk);
271
319
  }
272
320
  async function generateKeypairTo(path) {
273
- const dir = (0, import_node_path.dirname)(path);
274
- (0, import_node_fs.mkdirSync)(dir, { recursive: true });
275
- if ((0, import_node_fs.existsSync)(path)) {
321
+ const dir = dirname(path);
322
+ mkdirSync(dir, { recursive: true });
323
+ if (existsSync(path)) {
276
324
  throw new Error(`Key already exists at ${path}; refuse overwrite`);
277
325
  }
278
- const { privateKey } = (0, import_node_crypto2.generateKeyPairSync)("ed25519");
326
+ const { privateKey } = generateKeyPairSync("ed25519");
279
327
  const pem = privateKey.export({ format: "pem", type: "pkcs8" });
280
- const fd = (0, import_node_fs.openSync)(path, "wx", 384);
328
+ const fd = openSync(path, "wx", 384);
281
329
  try {
282
- (0, import_node_fs.writeFileSync)(fd, pem);
330
+ writeFileSync(fd, pem);
283
331
  } finally {
284
- (0, import_node_fs.closeSync)(fd);
332
+ closeSync(fd);
285
333
  }
286
334
  const jwk = privateKey.export({ format: "jwk" });
287
335
  return keyPairFromJwk(jwk);
288
336
  }
289
337
 
290
338
  // src/index.ts
291
- var VERSION = "0.1.0";
292
- // Annotate the CommonJS export names for ESM import in node:
293
- 0 && (module.exports = {
339
+ var VERSION = "0.2.0";
340
+ export {
294
341
  DEFAULT_KEY_PATH,
295
342
  Identity,
296
343
  VERSION,
297
344
  applyTo,
298
345
  createSignedFetch,
299
346
  sign
300
- });
347
+ };
348
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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"]}
package/package.json CHANGED
@@ -1,45 +1,77 @@
1
1
  {
2
- "name": "wb-auth",
3
- "version": "0.1.1",
4
- "description": "Web Bot Auth (RFC 9421) TypeScript SDK",
5
- "type": "module",
6
- "main": "./dist/index.js",
7
- "module": "./dist/index.mjs",
8
- "types": "./dist/index.d.ts",
9
- "exports": {
10
- ".": {
11
- "types": "./dist/index.d.ts",
12
- "import": "./dist/index.mjs",
13
- "require": "./dist/index.js"
14
- }
15
- },
16
- "files": [
17
- "dist"
18
- ],
19
- "license": "Apache-2.0",
20
- "dependencies": {
21
- "http-message-sig": "^0.2.0",
22
- "web-bot-auth": "^0.1.3"
23
- },
24
- "devDependencies": {
25
- "@biomejs/biome": "^2",
26
- "@types/node": "^20",
27
- "playwright": "^1.59",
28
- "tsup": "^8",
29
- "typescript": "^6",
30
- "vitest": "^4"
31
- },
32
- "peerDependencies": {
33
- "playwright": "^1"
34
- },
35
- "peerDependenciesMeta": {
36
- "playwright": {
37
- "optional": true
38
- }
39
- },
40
- "scripts": {
41
- "build": "tsup",
42
- "test": "vitest run",
43
- "lint": "biome check ."
44
- }
45
- }
2
+ "name": "wb-auth",
3
+ "version": "0.2.0",
4
+ "description": "Web Bot Auth (RFC 9421) TypeScript SDK \u2014 signed identity for AI agents and crawlers",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ },
20
+ "./package.json": "./package.json"
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "sideEffects": false,
26
+ "license": "Apache-2.0",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/MrKoriy/wbauth.git",
30
+ "directory": "typescript"
31
+ },
32
+ "homepage": "https://github.com/MrKoriy/wbauth#readme",
33
+ "bugs": "https://github.com/MrKoriy/wbauth/issues",
34
+ "keywords": [
35
+ "web-bot-auth",
36
+ "rfc9421",
37
+ "http-message-signatures",
38
+ "ed25519",
39
+ "ai-agents",
40
+ "crawler",
41
+ "playwright"
42
+ ],
43
+ "engines": {
44
+ "node": ">=20"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public",
48
+ "provenance": true
49
+ },
50
+ "scripts": {
51
+ "build": "tsup",
52
+ "test": "vitest run",
53
+ "lint": "biome check .",
54
+ "prepublishOnly": "npm run build",
55
+ "typecheck": "tsc --noEmit -p ."
56
+ },
57
+ "dependencies": {
58
+ "http-message-sig": "^0.2.0",
59
+ "web-bot-auth": "^0.1.3"
60
+ },
61
+ "devDependencies": {
62
+ "@biomejs/biome": "^2",
63
+ "@types/node": "^20",
64
+ "playwright": "^1.59",
65
+ "tsup": "^8",
66
+ "typescript": "^6",
67
+ "vitest": "^4"
68
+ },
69
+ "peerDependencies": {
70
+ "playwright": "^1"
71
+ },
72
+ "peerDependenciesMeta": {
73
+ "playwright": {
74
+ "optional": true
75
+ }
76
+ }
77
+ }