wb-auth 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +786 -224
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +165 -69
- package/dist/index.d.ts +165 -69
- package/dist/index.js +786 -230
- package/dist/index.js.map +1 -1
- package/package.json +73 -76
package/dist/index.d.ts
CHANGED
|
@@ -1,90 +1,130 @@
|
|
|
1
|
-
import { Signer } from 'http-message-sig';
|
|
2
1
|
import { Page } from 'playwright';
|
|
3
2
|
|
|
3
|
+
interface SignScopeOptions {
|
|
4
|
+
/** Only sign requests to these hosts (`example.com`, `*.example.com`). */
|
|
5
|
+
hosts?: readonly string[];
|
|
6
|
+
/** Custom predicate; takes precedence over `hosts`. */
|
|
7
|
+
shouldSign?: (url: URL) => boolean;
|
|
8
|
+
/** Also sign plain `http://` requests (off by default: replayable in clear text). */
|
|
9
|
+
allowInsecure?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
4
12
|
/**
|
|
5
13
|
* Identity, KeyPair, and key-file helpers for the wbauth TypeScript SDK.
|
|
6
14
|
*
|
|
7
|
-
* Mirrors python/src/wbauth/identity.py — same on-disk format (PKCS8
|
|
8
|
-
*
|
|
9
|
-
* multi-key rotation semantics (active + retiring
|
|
10
|
-
* (
|
|
11
|
-
*
|
|
12
|
-
* Cross-language guarantee (D-60): the PEM file Python writes via
|
|
13
|
-
* `Ed25519PrivateKey.private_bytes(PEM, PKCS8, NoEncryption)` is byte-equal
|
|
14
|
-
* to what Node's `crypto.generateKeyPairSync('ed25519').privateKey.export({
|
|
15
|
-
* format: 'pem', type: 'pkcs8' })` writes. Both runtimes can load each
|
|
16
|
-
* other's keyfile and produce the same kid + signatures.
|
|
15
|
+
* Mirrors python/src/wbauth/identity.py — same on-disk format (PKCS8 PEM at
|
|
16
|
+
* 0o600, optionally passphrase-encrypted), same kid (RFC 7638 JWK
|
|
17
|
+
* thumbprint), same multi-key rotation semantics (active + retiring, in
|
|
18
|
+
* memory via `rotate()` or persisted via `rotateKeyring()`), same REDACTED
|
|
19
|
+
* repr (IDENT-08).
|
|
17
20
|
*
|
|
18
|
-
* Key
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* verified byte-identical to what Python's cryptography library produces
|
|
22
|
-
* for the same PKCS8 PEM (RESEARCH §2.2 live verification 2026-05-10).
|
|
21
|
+
* Key material is held in ECMAScript `#private` fields and only as a
|
|
22
|
+
* non-extractable `KeyObject`/`Signer`, so `JSON.stringify(identity)`,
|
|
23
|
+
* `structuredClone`, spreading or `Object.entries` never expose `d`.
|
|
23
24
|
*/
|
|
24
|
-
|
|
25
25
|
/** Default path for the active signing key. Matches Python DEFAULT_KEY_PATH. */
|
|
26
26
|
declare const DEFAULT_KEY_PATH: string;
|
|
27
|
+
/** Environment variable holding the passphrase for encrypted PEM key files. */
|
|
28
|
+
declare const KEY_PASSWORD_ENV = "WBAUTH_KEY_PASSWORD";
|
|
27
29
|
interface IdentityOptions {
|
|
28
30
|
/** Stable HTTPS URL where this agent's JWKS is published. Must start with `https://`. */
|
|
29
31
|
signatureAgentUrl: string;
|
|
30
32
|
/** Optional User-Agent string adapters inject when caller did not supply one. */
|
|
31
33
|
userAgent?: string;
|
|
32
34
|
}
|
|
35
|
+
interface KeyOptions {
|
|
36
|
+
/** Passphrase for an encrypted PEM. Defaults to `$WBAUTH_KEY_PASSWORD`. */
|
|
37
|
+
password?: string;
|
|
38
|
+
}
|
|
39
|
+
/** Public JWK of one key ({kty, crv, x, kid}). */
|
|
40
|
+
type PublicJwk = JsonWebKey & {
|
|
41
|
+
kid: string;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Pluggable Ed25519 signing backend (local key, KMS, HSM, keychain).
|
|
45
|
+
* `sign` receives the RFC 9421 signature base as UTF-8 bytes and must return
|
|
46
|
+
* the raw 64-byte Ed25519 signature.
|
|
47
|
+
*/
|
|
48
|
+
interface Ed25519SignerBackend {
|
|
49
|
+
/** Public key as a JWK ({kty:"OKP", crv:"Ed25519", x}). */
|
|
50
|
+
publicJwk: JsonWebKey;
|
|
51
|
+
sign(data: Uint8Array): Uint8Array | Promise<Uint8Array>;
|
|
52
|
+
}
|
|
53
|
+
/** Signer handed to `sign()` (structurally compatible with web-bot-auth's). */
|
|
54
|
+
interface Signer {
|
|
55
|
+
alg: "ed25519";
|
|
56
|
+
keyid: string;
|
|
57
|
+
sign(data: string): Promise<Uint8Array>;
|
|
58
|
+
}
|
|
59
|
+
/** Public view of a key pair. Private material is never exposed. */
|
|
33
60
|
interface KeyPair {
|
|
34
|
-
|
|
35
|
-
privateJwk: JsonWebKey;
|
|
36
|
-
/** Public JWK ({kty, crv, x, kid}) — what exportJwks returns per key. */
|
|
37
|
-
publicJwk: JsonWebKey & {
|
|
38
|
-
kid: string;
|
|
39
|
-
};
|
|
40
|
-
/** RFC 7638 base64url-no-pad thumbprint of the public key. */
|
|
61
|
+
publicJwk: PublicJwk;
|
|
41
62
|
kid: string;
|
|
42
|
-
/** Pre-resolved web-bot-auth Signer (caches the imported CryptoKey). */
|
|
43
63
|
signer: Signer;
|
|
44
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Validate a Signature-Agent URL: absolute https:// with a host, printable
|
|
67
|
+
* ASCII only, no spaces/quotes/backslashes (it is emitted as an RFC 8941
|
|
68
|
+
* sf-string and must not break the header or the signature base).
|
|
69
|
+
*/
|
|
70
|
+
declare function validateSignatureAgentUrl(url: string): string;
|
|
45
71
|
declare class Identity {
|
|
46
|
-
private
|
|
47
|
-
private _retiring;
|
|
48
|
-
readonly signatureAgentUrl: string;
|
|
49
|
-
readonly userAgent: string | undefined;
|
|
72
|
+
#private;
|
|
50
73
|
private constructor();
|
|
74
|
+
get signatureAgentUrl(): string;
|
|
75
|
+
get userAgent(): string | undefined;
|
|
51
76
|
/** The active key's kid (used as `keyid=` param in Signature-Input). */
|
|
52
77
|
get kid(): string;
|
|
53
|
-
/**
|
|
78
|
+
/** The active key's public JWK (copy; never contains private material). */
|
|
79
|
+
get publicJwk(): PublicJwk;
|
|
80
|
+
/** Module-internal: returns the active Signer for sign() to use. */
|
|
54
81
|
_signer(): Signer;
|
|
55
|
-
/**
|
|
56
|
-
* Export the public JWKS document.
|
|
57
|
-
*
|
|
58
|
-
* Returns `{keys: [active_jwk]}` if no retiring key is held, or
|
|
59
|
-
* `{keys: [active_jwk, retiring_jwk]}` after a rotation. Active is always
|
|
60
|
-
* first. Verifiers select by `kid`. Mirror of Python `export_jwks`.
|
|
61
|
-
*/
|
|
82
|
+
/** Public JWKS: active first, then the retiring key (if any). */
|
|
62
83
|
exportJwks(): {
|
|
63
|
-
keys:
|
|
64
|
-
kid: string;
|
|
65
|
-
}>;
|
|
84
|
+
keys: PublicJwk[];
|
|
66
85
|
};
|
|
67
86
|
/**
|
|
68
87
|
* IDENT-07: generate a new active key, demote current to retiring.
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* Any previously-retiring key is dropped (only one overlap window).
|
|
88
|
+
* In-memory only — use `rotateKeyring()` to persist. The default path is
|
|
89
|
+
* a fresh timestamped file next to DEFAULT_KEY_PATH (never collides).
|
|
72
90
|
*/
|
|
73
|
-
rotate(newPath?: string): Promise<Identity>;
|
|
91
|
+
rotate(newPath?: string, keyOpts?: KeyOptions): Promise<Identity>;
|
|
74
92
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* byte-equal to what Python's `Identity.load_or_generate` writes (D-60).
|
|
93
|
+
* Persisted rotation: `<dir>/active.pem` → `retiring.pem` (replacing the
|
|
94
|
+
* previous retiring key), then a fresh `active.pem` is generated.
|
|
78
95
|
*/
|
|
79
|
-
|
|
96
|
+
rotateKeyring(dir: string, keyOpts?: KeyOptions): Promise<Identity>;
|
|
80
97
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
98
|
+
* Loads `path` if it exists, generates a fresh key there otherwise.
|
|
99
|
+
* Concurrent first starts are safe: the loser of the O_EXCL race loads the
|
|
100
|
+
* winner's key. `retiringPath` optionally loads the previous key.
|
|
84
101
|
*/
|
|
102
|
+
static loadOrGenerate(path: string | undefined, opts: IdentityOptions & KeyOptions & {
|
|
103
|
+
retiringPath?: string;
|
|
104
|
+
}): Promise<Identity>;
|
|
105
|
+
/** Load an existing key; never creates one. */
|
|
106
|
+
static load(path: string, opts: IdentityOptions & KeyOptions): Promise<Identity>;
|
|
107
|
+
/** Generate a new key at `path`; never overwrites. */
|
|
108
|
+
static generate(path: string, opts: IdentityOptions & KeyOptions): Promise<Identity>;
|
|
109
|
+
/** `<dir>/active.pem` (+ `retiring.pem`), generating active on first use. */
|
|
110
|
+
static loadKeyring(dir: string, opts: IdentityOptions & KeyOptions): Promise<Identity>;
|
|
111
|
+
/** Wrap an external signer (KMS, HSM, keychain). */
|
|
112
|
+
static fromSigner(backend: Ed25519SignerBackend, opts: IdentityOptions): Promise<Identity>;
|
|
113
|
+
/** RFC 9421 Appendix B.1.4 test key. NEVER use in production. */
|
|
85
114
|
static fromTestKey(signatureAgentUrl: string, opts?: Omit<IdentityOptions, "signatureAgentUrl">): Promise<Identity>;
|
|
86
115
|
toString(): string;
|
|
116
|
+
/** JSON.stringify(identity) → public data only. */
|
|
117
|
+
toJSON(): {
|
|
118
|
+
kid: string;
|
|
119
|
+
signatureAgentUrl: string;
|
|
120
|
+
userAgent?: string;
|
|
121
|
+
jwks: {
|
|
122
|
+
keys: PublicJwk[];
|
|
123
|
+
};
|
|
124
|
+
};
|
|
87
125
|
}
|
|
126
|
+
/** RFC 7638 thumbprint of an Ed25519 OKP JWK. */
|
|
127
|
+
declare function thumbprint(jwk: JsonWebKey): string;
|
|
88
128
|
|
|
89
129
|
/**
|
|
90
130
|
* createSignedFetch — drop-in `fetch` wrapper that signs every outgoing request.
|
|
@@ -97,14 +137,25 @@ declare class Identity {
|
|
|
97
137
|
* `cache`, `keepalive`, `integrity` and `referrerPolicy`.
|
|
98
138
|
* - Re-sending an already signed request (retry wrappers) produces exactly one
|
|
99
139
|
* fresh signature — stale Signature* headers are replaced, never merged.
|
|
140
|
+
* - **Redirects** (`redirect: "follow"`, the default) are followed here, not
|
|
141
|
+
* by the underlying fetch, so each hop is re-signed for its own
|
|
142
|
+
* `@authority`. Native fetch would otherwise replay hop 1's signature to the
|
|
143
|
+
* redirect target. Method/body rewriting and cross-origin credential
|
|
144
|
+
* stripping follow the WHATWG fetch spec.
|
|
145
|
+
*
|
|
146
|
+
* - `hosts` / `shouldSign` restrict which hosts receive your identity
|
|
147
|
+
* (redirects to other hosts go out unsigned); plain-http requests are
|
|
148
|
+
* unsigned unless `allowInsecure: true`.
|
|
100
149
|
*
|
|
101
150
|
* v1 limitation: streaming bodies are buffered (`req.clone().arrayBuffer()`)
|
|
102
151
|
* so Content-Digest can be computed.
|
|
103
152
|
*/
|
|
104
153
|
|
|
105
|
-
interface SignedFetchOptions {
|
|
154
|
+
interface SignedFetchOptions extends SignScopeOptions {
|
|
106
155
|
/** Underlying fetch implementation. Defaults to `globalThis.fetch` at call time. */
|
|
107
156
|
fetch?: typeof fetch;
|
|
157
|
+
/** Maximum redirects followed (and re-signed). Default 20, like fetch. */
|
|
158
|
+
maxRedirects?: number;
|
|
108
159
|
}
|
|
109
160
|
declare function createSignedFetch(identity: Identity, options?: SignedFetchOptions): typeof fetch;
|
|
110
161
|
|
|
@@ -123,15 +174,11 @@ declare function createSignedFetch(identity: Identity, options?: SignedFetchOpti
|
|
|
123
174
|
* Note: Playwright disables the HTTP cache while a route is active.
|
|
124
175
|
*/
|
|
125
176
|
|
|
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;
|
|
177
|
+
interface ApplyToOptions extends SignScopeOptions {
|
|
131
178
|
/** Route pattern passed to `page.route`. Default `"**\/*"`. */
|
|
132
179
|
pattern?: string;
|
|
133
180
|
}
|
|
134
|
-
declare function applyTo(page: Page, identity: Identity, options?: ApplyToOptions): Promise<void
|
|
181
|
+
declare function applyTo(page: Page, identity: Identity, options?: ApplyToOptions): Promise<() => Promise<void>>;
|
|
135
182
|
|
|
136
183
|
/**
|
|
137
184
|
* NormalizedRequest — the structural input to wbauth.sign().
|
|
@@ -165,25 +212,74 @@ interface SignOptions {
|
|
|
165
212
|
created?: Date;
|
|
166
213
|
/** Validity window in seconds. Defaults to 60. */
|
|
167
214
|
expiresAfterSeconds?: number;
|
|
168
|
-
/** Anti-replay nonce. Defaults to
|
|
215
|
+
/** Anti-replay nonce. Defaults to 64 random bytes, base64. */
|
|
169
216
|
nonce?: string;
|
|
170
217
|
/** Signature label (the `sig1=` prefix). Defaults to "sig1". */
|
|
171
218
|
label?: string;
|
|
172
219
|
}
|
|
220
|
+
/** RFC 9421 §2.2.3 `@authority`: lower-case host, no userinfo, no default port. */
|
|
221
|
+
declare function normalizeAuthority(url: string): string;
|
|
173
222
|
/**
|
|
174
223
|
* Sign a NormalizedRequest with the Web Bot Auth profile.
|
|
175
224
|
*
|
|
176
|
-
* Mutates `request.headers` in place
|
|
177
|
-
*
|
|
178
|
-
* `["@authority"]` only and silently breaks byte-equality with the Python
|
|
179
|
-
* signer). After signatureHeaders returns, also writes Signature and
|
|
180
|
-
* Signature-Input back into `request.headers`.
|
|
181
|
-
*
|
|
182
|
-
* @returns the same three values as a typed dataclass for callers that prefer
|
|
183
|
-
* a return value over header inspection.
|
|
225
|
+
* Mutates `request.headers` in place (Signature-Agent, Signature-Input,
|
|
226
|
+
* Signature) and returns the same three values.
|
|
184
227
|
*/
|
|
185
228
|
declare function sign(request: NormalizedRequest, identity: Identity, opts?: SignOptions): Promise<SignatureHeaders>;
|
|
186
229
|
|
|
230
|
+
declare class VerificationError extends Error {
|
|
231
|
+
name: string;
|
|
232
|
+
}
|
|
233
|
+
interface VerifiedAgent {
|
|
234
|
+
kid: string;
|
|
235
|
+
signatureAgentUrl: string | null;
|
|
236
|
+
label: string;
|
|
237
|
+
created: number;
|
|
238
|
+
expires: number;
|
|
239
|
+
nonce: string | null;
|
|
240
|
+
covered: string[];
|
|
241
|
+
jwk: JsonWebKey;
|
|
242
|
+
}
|
|
243
|
+
type KeyResolver = (kid: string, signatureAgent: string | null) => Promise<JsonWebKey | null | undefined>;
|
|
244
|
+
interface VerifierOptions {
|
|
245
|
+
keyResolver?: KeyResolver;
|
|
246
|
+
/** fetch used for directory lookups (default globalThis.fetch). */
|
|
247
|
+
fetch?: typeof fetch;
|
|
248
|
+
/** Directory cache TTL in seconds (default 300). */
|
|
249
|
+
jwksTtlSeconds?: number;
|
|
250
|
+
/** Clock skew tolerance in seconds (default 5). */
|
|
251
|
+
maxSkew?: number;
|
|
252
|
+
requireNonce?: boolean;
|
|
253
|
+
nonceStore?: NonceStore;
|
|
254
|
+
}
|
|
255
|
+
/** In-memory TTL set of seen nonces. Replace with a shared store in clusters. */
|
|
256
|
+
declare class NonceStore {
|
|
257
|
+
#private;
|
|
258
|
+
private readonly maxSize;
|
|
259
|
+
constructor(maxSize?: number);
|
|
260
|
+
seen(nonce: string, expiresAt: number): boolean;
|
|
261
|
+
}
|
|
262
|
+
declare class Verifier {
|
|
263
|
+
#private;
|
|
264
|
+
constructor(opts?: VerifierOptions);
|
|
265
|
+
/**
|
|
266
|
+
* Verify a WHATWG Request (body is read from a clone). `authority`
|
|
267
|
+
* overrides the value derived from the URL (e.g. behind a proxy).
|
|
268
|
+
*/
|
|
269
|
+
verify(request: Request, opts?: {
|
|
270
|
+
authority?: string;
|
|
271
|
+
now?: number;
|
|
272
|
+
}): Promise<VerifiedAgent>;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Fetch-style middleware helper (Hono, itty-router, Workers, Bun, Deno…):
|
|
276
|
+
* returns the verified agent, `null` for unsigned requests, or a 401
|
|
277
|
+
* `Response` when `required` and verification fails.
|
|
278
|
+
*/
|
|
279
|
+
declare function createVerifierMiddleware(opts?: VerifierOptions & {
|
|
280
|
+
required?: boolean;
|
|
281
|
+
}): (request: Request) => Promise<VerifiedAgent | null | Response>;
|
|
282
|
+
|
|
187
283
|
/**
|
|
188
284
|
* wbauth: Web Bot Auth (RFC 9421) TypeScript SDK — Phase 4 public surface.
|
|
189
285
|
*
|
|
@@ -199,6 +295,6 @@ declare function sign(request: NormalizedRequest, identity: Identity, opts?: Sig
|
|
|
199
295
|
* const res = await sf("https://api.example.com/data");
|
|
200
296
|
*/
|
|
201
297
|
|
|
202
|
-
declare const VERSION = "0.
|
|
298
|
+
declare const VERSION = "0.4.0";
|
|
203
299
|
|
|
204
|
-
export { type ApplyToOptions, DEFAULT_KEY_PATH, Identity, type IdentityOptions, type KeyPair, type NormalizedRequest, type SignOptions, type SignatureHeaders, type SignedFetchOptions, VERSION, applyTo, createSignedFetch, sign };
|
|
300
|
+
export { type ApplyToOptions, DEFAULT_KEY_PATH, type Ed25519SignerBackend, Identity, type IdentityOptions, KEY_PASSWORD_ENV, type KeyOptions, type KeyPair, type KeyResolver, NonceStore, type NormalizedRequest, type PublicJwk, type SignOptions, type SignScopeOptions, type SignatureHeaders, type SignedFetchOptions, type Signer, VERSION, VerificationError, type VerifiedAgent, Verifier, type VerifierOptions, applyTo, createSignedFetch, createVerifierMiddleware, normalizeAuthority, sign, thumbprint, validateSignatureAgentUrl };
|