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/README.md +61 -4
- package/dist/index.cjs +420 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +44 -22
- package/dist/index.d.ts +44 -22
- package/dist/index.js +191 -95
- package/dist/index.js.map +1 -0
- package/package.json +77 -44
- package/dist/index.mjs +0 -276
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
|
-
*
|
|
95
|
-
*
|
|
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
|
-
|
|
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 —
|
|
119
|
+
* applyTo — sign every matching request a Playwright page sends.
|
|
102
120
|
*
|
|
103
|
-
* Mirrors python/src/wbauth/adapters/playwright.py.
|
|
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
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
116
|
-
*
|
|
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
|
-
|
|
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 (`
|
|
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 "
|
|
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.
|
|
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
|
-
*
|
|
95
|
-
*
|
|
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
|
-
|
|
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 —
|
|
119
|
+
* applyTo — sign every matching request a Playwright page sends.
|
|
102
120
|
*
|
|
103
|
-
* Mirrors python/src/wbauth/adapters/playwright.py.
|
|
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
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
116
|
-
*
|
|
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
|
-
|
|
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 (`
|
|
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 "
|
|
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.
|
|
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
|
-
|
|
2
|
+
import { signatureHeaders } from "web-bot-auth";
|
|
34
3
|
var DEFAULT_LABEL = "sig1";
|
|
35
4
|
var DEFAULT_EXPIRES_SECONDS = 60;
|
|
36
|
-
var
|
|
37
|
-
function componentsFor(
|
|
5
|
+
var MAX_EXPIRES_SECONDS = 3600;
|
|
6
|
+
function componentsFor(_method, hasBody) {
|
|
38
7
|
const base = ["@authority", "signature-agent"];
|
|
39
|
-
if (hasBody
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
82
|
-
var
|
|
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
|
-
|
|
86
|
-
if (
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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
|
|
141
|
+
const baseHeaders = {};
|
|
105
142
|
req.headers.forEach((value, key) => {
|
|
106
|
-
|
|
143
|
+
baseHeaders[key] = value;
|
|
107
144
|
});
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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 =
|
|
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
|
|
335
|
+
return resolve(path.startsWith("~") ? path.replace(/^~/, homedir()) : path);
|
|
240
336
|
}
|
|
241
337
|
async function keyPairFromJwk(privateJwk) {
|
|
242
|
-
const signer = await
|
|
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 =
|
|
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 =
|
|
266
|
-
const 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 =
|
|
274
|
-
|
|
275
|
-
if (
|
|
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 } =
|
|
374
|
+
const { privateKey } = generateKeyPairSync("ed25519");
|
|
279
375
|
const pem = privateKey.export({ format: "pem", type: "pkcs8" });
|
|
280
|
-
const fd =
|
|
376
|
+
const fd = openSync(path, "wx", 384);
|
|
281
377
|
try {
|
|
282
|
-
|
|
378
|
+
writeFileSync(fd, pem);
|
|
283
379
|
} finally {
|
|
284
|
-
|
|
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.
|
|
292
|
-
|
|
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
|