wb-auth 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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,52 @@ 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
+ * - **Redirects** (`redirect: "follow"`, the default) are followed here, not
101
+ * by the underlying fetch, so each hop is re-signed for its own
102
+ * `@authority`. Native fetch would otherwise replay hop 1's signature to the
103
+ * redirect target. Method/body rewriting and cross-origin credential
104
+ * stripping follow the WHATWG fetch spec.
105
+ *
106
+ * v1 limitation: streaming bodies are buffered (`req.clone().arrayBuffer()`)
107
+ * so Content-Digest can be computed.
96
108
  */
97
109
 
98
- declare function createSignedFetch(identity: Identity): typeof fetch;
110
+ interface SignedFetchOptions {
111
+ /** Underlying fetch implementation. Defaults to `globalThis.fetch` at call time. */
112
+ fetch?: typeof fetch;
113
+ /** Maximum redirects followed (and re-signed). Default 20, like fetch. */
114
+ maxRedirects?: number;
115
+ }
116
+ declare function createSignedFetch(identity: Identity, options?: SignedFetchOptions): typeof fetch;
99
117
 
100
118
  /**
101
- * applyTo — Playwright async helper for Web Bot Auth (ADAPT-05).
119
+ * applyTo — sign every matching request a Playwright page sends.
102
120
  *
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.
121
+ * Mirrors python/src/wbauth/adapters/playwright.py.
106
122
  *
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.
123
+ * Privacy: by default every http(s) request is signed, including third-party
124
+ * analytics/CDN hosts, which reveals your agent identity to them. Pass
125
+ * `hosts` to restrict signing to the sites you actually target:
110
126
  *
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).
127
+ * await applyTo(page, identity, { hosts: ["example.com", "*.example.org"] });
114
128
  *
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.
129
+ * Non-http(s) requests (data:, blob:, chrome-extension:) are never signed.
130
+ * Note: Playwright disables the HTTP cache while a route is active.
117
131
  */
118
132
 
119
- declare function applyTo(page: Page, identity: Identity): Promise<void>;
133
+ interface ApplyToOptions {
134
+ /** Only sign requests to these hosts (`example.com`, `*.example.com`). */
135
+ hosts?: readonly string[];
136
+ /** Custom predicate; takes precedence over `hosts`. */
137
+ shouldSign?: (url: URL) => boolean;
138
+ /** Route pattern passed to `page.route`. Default `"**\/*"`. */
139
+ pattern?: string;
140
+ }
141
+ declare function applyTo(page: Page, identity: Identity, options?: ApplyToOptions): Promise<void>;
120
142
 
121
143
  /**
122
144
  * NormalizedRequest — the structural input to wbauth.sign().
@@ -172,11 +194,11 @@ declare function sign(request: NormalizedRequest, identity: Identity, opts?: Sig
172
194
  /**
173
195
  * wbauth: Web Bot Auth (RFC 9421) TypeScript SDK — Phase 4 public surface.
174
196
  *
175
- * Per CONTEXT D-74: flat root exports. Subpath exports (`wbauth/adapters`)
197
+ * Per CONTEXT D-74: flat root exports. Subpath exports (`wb-auth/adapters`)
176
198
  * are a v1.x optimization, not required for Phase 4.
177
199
  *
178
200
  * Quickstart:
179
- * import { Identity, createSignedFetch } from "wbauth";
201
+ * import { Identity, createSignedFetch } from "wb-auth";
180
202
  * const id = await Identity.loadOrGenerate("./key.pem", {
181
203
  * signatureAgentUrl: "https://example.com/.well-known/.../<kid>",
182
204
  * });
@@ -184,6 +206,6 @@ declare function sign(request: NormalizedRequest, identity: Identity, opts?: Sig
184
206
  * const res = await sf("https://api.example.com/data");
185
207
  */
186
208
 
187
- declare const VERSION = "0.1.0";
209
+ declare const VERSION = "0.3.0";
188
210
 
189
- export { DEFAULT_KEY_PATH, Identity, type IdentityOptions, type KeyPair, type NormalizedRequest, type SignOptions, type SignatureHeaders, VERSION, applyTo, createSignedFetch, sign };
211
+ export { type ApplyToOptions, DEFAULT_KEY_PATH, Identity, type IdentityOptions, type KeyPair, type NormalizedRequest, type SignOptions, type SignatureHeaders, type SignedFetchOptions, VERSION, applyTo, createSignedFetch, sign };
package/dist/index.d.ts CHANGED
@@ -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,52 @@ 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
+ * - **Redirects** (`redirect: "follow"`, the default) are followed here, not
101
+ * by the underlying fetch, so each hop is re-signed for its own
102
+ * `@authority`. Native fetch would otherwise replay hop 1's signature to the
103
+ * redirect target. Method/body rewriting and cross-origin credential
104
+ * stripping follow the WHATWG fetch spec.
105
+ *
106
+ * v1 limitation: streaming bodies are buffered (`req.clone().arrayBuffer()`)
107
+ * so Content-Digest can be computed.
96
108
  */
97
109
 
98
- declare function createSignedFetch(identity: Identity): typeof fetch;
110
+ interface SignedFetchOptions {
111
+ /** Underlying fetch implementation. Defaults to `globalThis.fetch` at call time. */
112
+ fetch?: typeof fetch;
113
+ /** Maximum redirects followed (and re-signed). Default 20, like fetch. */
114
+ maxRedirects?: number;
115
+ }
116
+ declare function createSignedFetch(identity: Identity, options?: SignedFetchOptions): typeof fetch;
99
117
 
100
118
  /**
101
- * applyTo — Playwright async helper for Web Bot Auth (ADAPT-05).
119
+ * applyTo — sign every matching request a Playwright page sends.
102
120
  *
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.
121
+ * Mirrors python/src/wbauth/adapters/playwright.py.
106
122
  *
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.
123
+ * Privacy: by default every http(s) request is signed, including third-party
124
+ * analytics/CDN hosts, which reveals your agent identity to them. Pass
125
+ * `hosts` to restrict signing to the sites you actually target:
110
126
  *
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).
127
+ * await applyTo(page, identity, { hosts: ["example.com", "*.example.org"] });
114
128
  *
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.
129
+ * Non-http(s) requests (data:, blob:, chrome-extension:) are never signed.
130
+ * Note: Playwright disables the HTTP cache while a route is active.
117
131
  */
118
132
 
119
- declare function applyTo(page: Page, identity: Identity): Promise<void>;
133
+ interface ApplyToOptions {
134
+ /** Only sign requests to these hosts (`example.com`, `*.example.com`). */
135
+ hosts?: readonly string[];
136
+ /** Custom predicate; takes precedence over `hosts`. */
137
+ shouldSign?: (url: URL) => boolean;
138
+ /** Route pattern passed to `page.route`. Default `"**\/*"`. */
139
+ pattern?: string;
140
+ }
141
+ declare function applyTo(page: Page, identity: Identity, options?: ApplyToOptions): Promise<void>;
120
142
 
121
143
  /**
122
144
  * NormalizedRequest — the structural input to wbauth.sign().
@@ -172,11 +194,11 @@ declare function sign(request: NormalizedRequest, identity: Identity, opts?: Sig
172
194
  /**
173
195
  * wbauth: Web Bot Auth (RFC 9421) TypeScript SDK — Phase 4 public surface.
174
196
  *
175
- * Per CONTEXT D-74: flat root exports. Subpath exports (`wbauth/adapters`)
197
+ * Per CONTEXT D-74: flat root exports. Subpath exports (`wb-auth/adapters`)
176
198
  * are a v1.x optimization, not required for Phase 4.
177
199
  *
178
200
  * Quickstart:
179
- * import { Identity, createSignedFetch } from "wbauth";
201
+ * import { Identity, createSignedFetch } from "wb-auth";
180
202
  * const id = await Identity.loadOrGenerate("./key.pem", {
181
203
  * signatureAgentUrl: "https://example.com/.well-known/.../<kid>",
182
204
  * });
@@ -184,6 +206,6 @@ declare function sign(request: NormalizedRequest, identity: Identity, opts?: Sig
184
206
  * const res = await sf("https://api.example.com/data");
185
207
  */
186
208
 
187
- declare const VERSION = "0.1.0";
209
+ declare const VERSION = "0.3.0";
188
210
 
189
- export { DEFAULT_KEY_PATH, Identity, type IdentityOptions, type KeyPair, type NormalizedRequest, type SignOptions, type SignatureHeaders, VERSION, applyTo, createSignedFetch, sign };
211
+ export { type ApplyToOptions, DEFAULT_KEY_PATH, Identity, type IdentityOptions, type KeyPair, type NormalizedRequest, type SignOptions, type SignatureHeaders, type SignedFetchOptions, VERSION, applyTo, createSignedFetch, sign };
package/dist/index.js CHANGED
@@ -1,64 +1,57 @@
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
- var DIGEST_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
37
- function componentsFor(method, hasBody) {
5
+ var MAX_EXPIRES_SECONDS = 3600;
6
+ function componentsFor(_method, hasBody) {
38
7
  const base = ["@authority", "signature-agent"];
39
- if (hasBody && DIGEST_METHODS.has(method.toUpperCase())) {
8
+ if (hasBody) {
40
9
  base.push("content-digest");
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 (typeof expiresAfterSeconds !== "number" || !Number.isFinite(expiresAfterSeconds) || expiresAfterSeconds <= 0 || expiresAfterSeconds > MAX_EXPIRES_SECONDS) {
31
+ throw new RangeError(
32
+ `expiresAfterSeconds must be a number in (0, ${MAX_EXPIRES_SECONDS}], 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 };
61
- if (hasBody) init.body = request.body;
51
+ const m = request.method.toUpperCase();
52
+ if (hasBody && m !== "GET" && m !== "HEAD") {
53
+ init.body = request.body;
54
+ }
62
55
  const req = new Request(request.url, init);
63
56
  const params = {
64
57
  created,
@@ -67,7 +60,7 @@ async function sign(request, identity, opts = {}) {
67
60
  components,
68
61
  ...opts.nonce !== void 0 ? { nonce: opts.nonce } : {}
69
62
  };
70
- const result = await (0, import_web_bot_auth.signatureHeaders)(req, identity._signer(), params);
63
+ const result = await signatureHeaders(req, identity._signer(), params);
71
64
  request.headers.Signature = result.Signature;
72
65
  request.headers["Signature-Input"] = result["Signature-Input"];
73
66
  return {
@@ -78,73 +71,176 @@ async function sign(request, identity, opts = {}) {
78
71
  }
79
72
 
80
73
  // src/adapters/_utils.ts
81
- var import_node_crypto = require("crypto");
82
- var DIGEST_METHODS2 = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
74
+ import { createHash } from "crypto";
75
+ var LIBRARY_DEFAULT_UA = /^(node|undici|node-fetch)(\/|$)/i;
76
+ function findHeader(headers, name) {
77
+ const n = name.toLowerCase();
78
+ return Object.keys(headers).find((k) => k.toLowerCase() === n);
79
+ }
83
80
  function ensureContentDigest(method, headers, body) {
84
81
  if (!body || body.byteLength === 0) return;
85
- if (!DIGEST_METHODS2.has(method.toUpperCase())) return;
86
- if (Object.keys(headers).some((k) => k.toLowerCase() === "content-digest")) {
82
+ void method;
83
+ if (findHeader(headers, "content-digest")) return;
84
+ const b64 = createHash("sha256").update(body).digest("base64");
85
+ headers["Content-Digest"] = `sha-256=:${b64}:`;
86
+ }
87
+ function applyUserAgent(headers, userAgent) {
88
+ if (!userAgent) return;
89
+ const key = findHeader(headers, "user-agent");
90
+ if (key === void 0) {
91
+ headers["User-Agent"] = userAgent;
87
92
  return;
88
93
  }
89
- const b64 = (0, import_node_crypto.createHash)("sha256").update(body).digest("base64");
90
- headers["Content-Digest"] = `sha-256=:${b64}:`;
94
+ if (LIBRARY_DEFAULT_UA.test(headers[key] ?? "")) {
95
+ delete headers[key];
96
+ headers["User-Agent"] = userAgent;
97
+ }
98
+ }
99
+ function hostMatches(hostname, allow) {
100
+ const h = hostname.toLowerCase().replace(/\.$/, "");
101
+ return allow.some((raw) => {
102
+ const a = raw.toLowerCase().replace(/\.$/, "");
103
+ if (a.startsWith("*.")) {
104
+ const base = a.slice(2);
105
+ return h === base || h.endsWith(`.${base}`);
106
+ }
107
+ return h === a;
108
+ });
91
109
  }
92
110
 
93
111
  // src/adapters/fetch.ts
94
- function createSignedFetch(identity) {
112
+ var REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
113
+ var CROSS_ORIGIN_STRIP = /* @__PURE__ */ new Set([
114
+ "authorization",
115
+ "cookie",
116
+ "proxy-authorization"
117
+ ]);
118
+ var BODY_HEADERS = /* @__PURE__ */ new Set([
119
+ "content-type",
120
+ "content-length",
121
+ "content-encoding",
122
+ "content-language",
123
+ "content-location",
124
+ "content-digest"
125
+ ]);
126
+ function dropHeaders(headers, names) {
127
+ for (const k of Object.keys(headers)) {
128
+ if (names.has(k.toLowerCase())) delete headers[k];
129
+ }
130
+ }
131
+ function createSignedFetch(identity, options = {}) {
95
132
  return async function signedFetch(input, init = {}) {
96
133
  const req = new Request(input, init);
97
- const method = req.method;
98
- const url = req.url;
134
+ let method = req.method;
135
+ let url = req.url;
99
136
  let body = null;
100
137
  if (req.body) {
101
138
  const buf = await req.clone().arrayBuffer();
102
139
  body = buf.byteLength > 0 ? new Uint8Array(buf) : null;
103
140
  }
104
- const headers = {};
141
+ const baseHeaders = {};
105
142
  req.headers.forEach((value, key) => {
106
- headers[key] = value;
143
+ baseHeaders[key] = value;
107
144
  });
108
- ensureContentDigest(method, headers, body);
109
- 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;
145
+ const redirectMode = init.redirect ?? req.redirect;
146
+ const followHere = redirectMode === "follow";
147
+ const maxRedirects = options.maxRedirects ?? 20;
148
+ const doFetch = options.fetch ?? globalThis.fetch;
149
+ for (let hop = 0; ; hop++) {
150
+ const headers = { ...baseHeaders };
151
+ ensureContentDigest(method, headers, body);
152
+ await sign({ method, url, headers, body }, identity);
153
+ applyUserAgent(headers, identity.userAgent);
154
+ const outInit = {
155
+ ...init,
156
+ method,
157
+ headers,
158
+ body: body ?? void 0,
159
+ signal: init.signal ?? req.signal,
160
+ redirect: followHere ? "manual" : redirectMode,
161
+ credentials: init.credentials ?? req.credentials,
162
+ cache: init.cache ?? req.cache,
163
+ keepalive: init.keepalive ?? req.keepalive
164
+ };
165
+ const integrity = init.integrity ?? req.integrity;
166
+ if (integrity) outInit.integrity = integrity;
167
+ const referrerPolicy = init.referrerPolicy ?? req.referrerPolicy;
168
+ if (referrerPolicy) outInit.referrerPolicy = referrerPolicy;
169
+ const res = await doFetch(url, outInit);
170
+ if (!followHere || !REDIRECT_STATUSES.has(res.status)) return res;
171
+ const location = res.headers.get("location");
172
+ if (location === null) return res;
173
+ if (hop >= maxRedirects) {
174
+ throw new TypeError(`redirect count exceeded (${maxRedirects})`);
175
+ }
176
+ const next = new URL(location, url);
177
+ if (next.protocol !== "http:" && next.protocol !== "https:") {
178
+ throw new TypeError(`unsupported redirect scheme: ${next.protocol}`);
179
+ }
180
+ const upper = method.toUpperCase();
181
+ if (res.status === 303 && upper !== "GET" && upper !== "HEAD" || (res.status === 301 || res.status === 302) && upper === "POST") {
182
+ method = "GET";
183
+ body = null;
184
+ dropHeaders(baseHeaders, BODY_HEADERS);
185
+ }
186
+ if (new URL(url).origin !== next.origin) {
187
+ dropHeaders(baseHeaders, CROSS_ORIGIN_STRIP);
188
+ }
189
+ await res.body?.cancel().catch(() => {
190
+ });
191
+ url = next.href;
112
192
  }
113
- return fetch(url, {
114
- ...init,
115
- method,
116
- headers,
117
- body: body ?? void 0
118
- });
119
193
  };
120
194
  }
121
195
 
122
196
  // src/adapters/playwright.ts
123
- async function applyTo(page, identity) {
124
- await page.route("**/*", async (route, request) => {
197
+ async function applyTo(page, identity, options = {}) {
198
+ const pattern = options.pattern ?? "**/*";
199
+ const decide = (raw) => {
200
+ let u;
201
+ try {
202
+ u = new URL(raw);
203
+ } catch {
204
+ return false;
205
+ }
206
+ if (u.protocol !== "https:" && u.protocol !== "http:") return false;
207
+ if (options.shouldSign) return options.shouldSign(u);
208
+ if (options.hosts) return hostMatches(u.hostname, options.hosts);
209
+ return true;
210
+ };
211
+ await page.route(pattern, async (route, request) => {
212
+ const url = request.url();
213
+ if (!decide(url)) {
214
+ const r = route;
215
+ await (r.fallback ? r.fallback() : route.continue());
216
+ return;
217
+ }
125
218
  const headers = await request.allHeaders();
126
219
  const postData = request.postDataBuffer();
127
220
  const body = postData ? new Uint8Array(postData) : null;
128
221
  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
- }
222
+ await sign({ method: request.method(), url, headers, body }, identity);
223
+ applyUserAgent(headers, identity.userAgent);
136
224
  await route.continue({ headers });
137
225
  });
138
226
  }
139
227
 
140
228
  // 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)(),
229
+ import { createPrivateKey, generateKeyPairSync } from "crypto";
230
+ import {
231
+ closeSync,
232
+ existsSync,
233
+ mkdirSync,
234
+ openSync,
235
+ readFileSync,
236
+ statSync,
237
+ writeFileSync
238
+ } from "fs";
239
+ import { homedir } from "os";
240
+ import { dirname, resolve } from "path";
241
+ import { signerFromJWK } from "web-bot-auth/crypto";
242
+ var DEFAULT_KEY_PATH = resolve(
243
+ homedir(),
148
244
  ".config",
149
245
  "wbauth",
150
246
  "key.pem"
@@ -214,7 +310,7 @@ var Identity = class _Identity {
214
310
  */
215
311
  static async loadOrGenerate(path = DEFAULT_KEY_PATH, opts) {
216
312
  const resolved = expandUser(path);
217
- const pair = (0, import_node_fs.existsSync)(resolved) ? await loadKeypair(resolved) : await generateKeypairTo(resolved);
313
+ const pair = existsSync(resolved) ? await loadKeypair(resolved) : await generateKeypairTo(resolved);
218
314
  return new _Identity(pair, opts);
219
315
  }
220
316
  /**
@@ -222,9 +318,9 @@ var Identity = class _Identity {
222
318
  * For tests + Cloudflare debug smoke checks. NEVER use in production.
223
319
  * Does NOT write any file to disk.
224
320
  */
225
- static async fromTestKey(signatureAgentUrl) {
321
+ static async fromTestKey(signatureAgentUrl, opts = {}) {
226
322
  const pair = await keyPairFromJwk(TEST_KEY_JWK);
227
- return new _Identity(pair, { signatureAgentUrl });
323
+ return new _Identity(pair, { ...opts, signatureAgentUrl });
228
324
  }
229
325
  // ---------- Redaction guarantees (IDENT-08) ----------
230
326
  toString() {
@@ -236,10 +332,10 @@ var Identity = class _Identity {
236
332
  }
237
333
  };
238
334
  function expandUser(path) {
239
- return (0, import_node_path.resolve)(path.startsWith("~") ? path.replace(/^~/, (0, import_node_os.homedir)()) : path);
335
+ return resolve(path.startsWith("~") ? path.replace(/^~/, homedir()) : path);
240
336
  }
241
337
  async function keyPairFromJwk(privateJwk) {
242
- const signer = await (0, import_crypto.signerFromJWK)(privateJwk);
338
+ const signer = await signerFromJWK(privateJwk);
243
339
  const kid = signer.keyid;
244
340
  const publicJwk = {
245
341
  kty: privateJwk.kty,
@@ -251,7 +347,7 @@ async function keyPairFromJwk(privateJwk) {
251
347
  }
252
348
  async function loadKeypair(path) {
253
349
  if (process.platform !== "win32") {
254
- const mode = (0, import_node_fs.statSync)(path).mode & 511;
350
+ const mode = statSync(path).mode & 511;
255
351
  if ((mode & 63) !== 0) {
256
352
  throw new Error(
257
353
  `Key file ${path} has mode 0o${mode.toString(8)}; expected 0o600. Fix: chmod 600 ${path}`
@@ -262,39 +358,39 @@ async function loadKeypair(path) {
262
358
  `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
359
  );
264
360
  }
265
- const pem = (0, import_node_fs.readFileSync)(path, "utf8");
266
- const jwk = (0, import_node_crypto2.createPrivateKey)(pem).export({ format: "jwk" });
361
+ const pem = readFileSync(path, "utf8");
362
+ const jwk = createPrivateKey(pem).export({ format: "jwk" });
267
363
  if (jwk.kty !== "OKP" || jwk.crv !== "Ed25519") {
268
364
  throw new TypeError(`Expected Ed25519, got kty=${jwk.kty}, crv=${jwk.crv}`);
269
365
  }
270
366
  return keyPairFromJwk(jwk);
271
367
  }
272
368
  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)) {
369
+ const dir = dirname(path);
370
+ mkdirSync(dir, { recursive: true });
371
+ if (existsSync(path)) {
276
372
  throw new Error(`Key already exists at ${path}; refuse overwrite`);
277
373
  }
278
- const { privateKey } = (0, import_node_crypto2.generateKeyPairSync)("ed25519");
374
+ const { privateKey } = generateKeyPairSync("ed25519");
279
375
  const pem = privateKey.export({ format: "pem", type: "pkcs8" });
280
- const fd = (0, import_node_fs.openSync)(path, "wx", 384);
376
+ const fd = openSync(path, "wx", 384);
281
377
  try {
282
- (0, import_node_fs.writeFileSync)(fd, pem);
378
+ writeFileSync(fd, pem);
283
379
  } finally {
284
- (0, import_node_fs.closeSync)(fd);
380
+ closeSync(fd);
285
381
  }
286
382
  const jwk = privateKey.export({ format: "jwk" });
287
383
  return keyPairFromJwk(jwk);
288
384
  }
289
385
 
290
386
  // src/index.ts
291
- var VERSION = "0.1.0";
292
- // Annotate the CommonJS export names for ESM import in node:
293
- 0 && (module.exports = {
387
+ var VERSION = "0.3.0";
388
+ export {
294
389
  DEFAULT_KEY_PATH,
295
390
  Identity,
296
391
  VERSION,
297
392
  applyTo,
298
393
  createSignedFetch,
299
394
  sign
300
- });
395
+ };
396
+ //# sourceMappingURL=index.js.map