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.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
- * NoEncryption PEM at 0o600), same kid (RFC 7638 JWK thumbprint), same
9
- * multi-key rotation semantics (active + retiring), same REDACTED repr
10
- * (IDENT-08).
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 implementation choice (Pitfall 1): PEM→JWK is done via Node 20+
19
- * stdlib `node:crypto.createPrivateKey(pem).export({format:'jwk'})`. We do
20
- * NOT use `jose` or any third-party JOSE library — the stdlib output is
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
- /** Private JWK (includes "d") — used to mint the signer. Never logged. */
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 _active;
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
- /** Module-internal: returns the resolved Signer for sign() to use. */
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: Array<JsonWebKey & {
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
- * Returns a NEW Identity (immutable update — the old Identity is unchanged).
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
- * Primary entry point. Loads `path` if it exists, generates a fresh key
76
- * there otherwise. The on-disk file is PKCS8 NoEncryption PEM at 0o600,
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
- static loadOrGenerate(path: string | undefined, opts: IdentityOptions): Promise<Identity>;
96
+ rotateKeyring(dir: string, keyOpts?: KeyOptions): Promise<Identity>;
80
97
  /**
81
- * Construct from the RFC 9421 Appendix B.1.4 publicly-known test key.
82
- * For tests + Cloudflare debug smoke checks. NEVER use in production.
83
- * Does NOT write any file to disk.
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 web-bot-auth's `generateNonce()` (64-byte b64). */
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: writes Signature-Agent BEFORE building
184
- * the signing base (Pitfall 6 — without pre-set, web-bot-auth defaults to
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.3.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 };