wb-auth 0.3.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 +714 -200
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +158 -69
- package/dist/index.d.ts +158 -69
- package/dist/index.js +714 -206
- package/dist/index.js.map +1 -1
- package/package.json +73 -77
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.
|
|
@@ -103,11 +143,15 @@ declare class Identity {
|
|
|
103
143
|
* redirect target. Method/body rewriting and cross-origin credential
|
|
104
144
|
* stripping follow the WHATWG fetch spec.
|
|
105
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`.
|
|
149
|
+
*
|
|
106
150
|
* v1 limitation: streaming bodies are buffered (`req.clone().arrayBuffer()`)
|
|
107
151
|
* so Content-Digest can be computed.
|
|
108
152
|
*/
|
|
109
153
|
|
|
110
|
-
interface SignedFetchOptions {
|
|
154
|
+
interface SignedFetchOptions extends SignScopeOptions {
|
|
111
155
|
/** Underlying fetch implementation. Defaults to `globalThis.fetch` at call time. */
|
|
112
156
|
fetch?: typeof fetch;
|
|
113
157
|
/** Maximum redirects followed (and re-signed). Default 20, like fetch. */
|
|
@@ -130,15 +174,11 @@ declare function createSignedFetch(identity: Identity, options?: SignedFetchOpti
|
|
|
130
174
|
* Note: Playwright disables the HTTP cache while a route is active.
|
|
131
175
|
*/
|
|
132
176
|
|
|
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;
|
|
177
|
+
interface ApplyToOptions extends SignScopeOptions {
|
|
138
178
|
/** Route pattern passed to `page.route`. Default `"**\/*"`. */
|
|
139
179
|
pattern?: string;
|
|
140
180
|
}
|
|
141
|
-
declare function applyTo(page: Page, identity: Identity, options?: ApplyToOptions): Promise<void
|
|
181
|
+
declare function applyTo(page: Page, identity: Identity, options?: ApplyToOptions): Promise<() => Promise<void>>;
|
|
142
182
|
|
|
143
183
|
/**
|
|
144
184
|
* NormalizedRequest — the structural input to wbauth.sign().
|
|
@@ -172,25 +212,74 @@ interface SignOptions {
|
|
|
172
212
|
created?: Date;
|
|
173
213
|
/** Validity window in seconds. Defaults to 60. */
|
|
174
214
|
expiresAfterSeconds?: number;
|
|
175
|
-
/** Anti-replay nonce. Defaults to
|
|
215
|
+
/** Anti-replay nonce. Defaults to 64 random bytes, base64. */
|
|
176
216
|
nonce?: string;
|
|
177
217
|
/** Signature label (the `sig1=` prefix). Defaults to "sig1". */
|
|
178
218
|
label?: string;
|
|
179
219
|
}
|
|
220
|
+
/** RFC 9421 §2.2.3 `@authority`: lower-case host, no userinfo, no default port. */
|
|
221
|
+
declare function normalizeAuthority(url: string): string;
|
|
180
222
|
/**
|
|
181
223
|
* Sign a NormalizedRequest with the Web Bot Auth profile.
|
|
182
224
|
*
|
|
183
|
-
* Mutates `request.headers` in place
|
|
184
|
-
*
|
|
185
|
-
* `["@authority"]` only and silently breaks byte-equality with the Python
|
|
186
|
-
* signer). After signatureHeaders returns, also writes Signature and
|
|
187
|
-
* Signature-Input back into `request.headers`.
|
|
188
|
-
*
|
|
189
|
-
* @returns the same three values as a typed dataclass for callers that prefer
|
|
190
|
-
* a return value over header inspection.
|
|
225
|
+
* Mutates `request.headers` in place (Signature-Agent, Signature-Input,
|
|
226
|
+
* Signature) and returns the same three values.
|
|
191
227
|
*/
|
|
192
228
|
declare function sign(request: NormalizedRequest, identity: Identity, opts?: SignOptions): Promise<SignatureHeaders>;
|
|
193
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
|
+
|
|
194
283
|
/**
|
|
195
284
|
* wbauth: Web Bot Auth (RFC 9421) TypeScript SDK — Phase 4 public surface.
|
|
196
285
|
*
|
|
@@ -206,6 +295,6 @@ declare function sign(request: NormalizedRequest, identity: Identity, opts?: Sig
|
|
|
206
295
|
* const res = await sf("https://api.example.com/data");
|
|
207
296
|
*/
|
|
208
297
|
|
|
209
|
-
declare const VERSION = "0.
|
|
298
|
+
declare const VERSION = "0.4.0";
|
|
210
299
|
|
|
211
|
-
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 };
|