@aooth/auth 0.1.1

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.
@@ -0,0 +1,386 @@
1
+ import { a as CredentialState, i as CredentialMetadata, n as DenylistStore, o as IssueResult, r as AuthContext, s as RefreshConfig, t as CredentialStore } from "./store-untAtWQz.cjs";
2
+ import { CryptoKey } from "jose";
3
+
4
+ //#region src/errors.d.ts
5
+ type AuthErrorType = "INVALID_TOKEN" | "TOKEN_EXPIRED" | "TOKEN_REVOKED" | "REFRESH_REUSE_DETECTED" | "STATELESS_OPERATION_UNSUPPORTED" | "MAX_CONCURRENT_REACHED" | "INVALID_CONFIG";
6
+ declare class AuthError extends Error {
7
+ readonly type: AuthErrorType;
8
+ readonly details?: Record<string, unknown> | undefined;
9
+ readonly name = "AuthError";
10
+ constructor(type: AuthErrorType, message?: string, details?: Record<string, unknown> | undefined);
11
+ }
12
+ //#endregion
13
+ //#region src/utils/clock.d.ts
14
+ /**
15
+ * Minimal time abstraction shared across stores and orchestrators.
16
+ *
17
+ * Defaults to wall-clock; tests inject a fake clock to deterministically
18
+ * advance time. Kept tiny (single `now()` method) so any timing primitive
19
+ * — Date, performance.now-ish, monotonic, etc. — can be plugged in.
20
+ */
21
+ interface Clock {
22
+ now(): number;
23
+ }
24
+ declare const defaultClock: Clock;
25
+ //#endregion
26
+ //#region src/stores/memory.d.ts
27
+ /**
28
+ * In-memory implementation of CredentialStore using random opaque token IDs.
29
+ *
30
+ * - `Map<token, state>` is the primary index.
31
+ * - `Map<userId, Set<token>>` is a secondary index for `revokeAllForUser` /
32
+ * `listForUser` without scanning the full primary map.
33
+ * - Both maps are kept in sync on every mutation.
34
+ */
35
+ declare class CredentialStoreMemory<TClaims extends object = object> implements CredentialStore<TClaims> {
36
+ private readonly states;
37
+ private readonly byUser;
38
+ private readonly clock;
39
+ constructor(opts?: {
40
+ clock?: Clock;
41
+ });
42
+ persist(state: CredentialState<TClaims>, ttl?: number): Promise<string>;
43
+ retrieve(token: string): Promise<CredentialState<TClaims> | null>;
44
+ consume(token: string): Promise<CredentialState<TClaims> | null>;
45
+ update(token: string, state: CredentialState<TClaims>): Promise<string>;
46
+ revoke(token: string): Promise<void>;
47
+ revokeAllForUser(userId: string): Promise<number>;
48
+ listForUser(userId: string): Promise<Array<CredentialState<TClaims> & {
49
+ token: string;
50
+ }>>;
51
+ private indexAdd;
52
+ private indexRemove;
53
+ }
54
+ //#endregion
55
+ //#region src/stores/denylist-memory.d.ts
56
+ /**
57
+ * In-memory denylist for stateless revocation.
58
+ *
59
+ * Stores a map from JTI (or any opaque identifier) to absolute expiry timestamp.
60
+ * `has` returns false for entries whose TTL has elapsed and lazily removes them.
61
+ * `cleanup` performs a sweep across the map and returns the number of entries
62
+ * removed, useful for periodic compaction in long-lived processes.
63
+ */
64
+ declare class DenylistStoreMemory implements DenylistStore {
65
+ private readonly entries;
66
+ private readonly clock;
67
+ constructor(opts?: {
68
+ clock?: Clock;
69
+ });
70
+ add(jti: string, expiresAt: number): Promise<void>;
71
+ has(jti: string): Promise<boolean>;
72
+ cleanup(): Promise<number>;
73
+ }
74
+ //#endregion
75
+ //#region src/stores/jwt.d.ts
76
+ /**
77
+ * Supported JWT signing algorithms.
78
+ * - HS256/HS384/HS512: symmetric (shared secret).
79
+ * - RS*, ES*, EdDSA: asymmetric (private/public keypair).
80
+ */
81
+ type JwtAlgorithm = "HS256" | "HS384" | "HS512" | "RS256" | "RS384" | "RS512" | "ES256" | "ES384" | "ES512" | "EdDSA";
82
+ interface CredentialStoreJwtOptions {
83
+ /** Signing algorithm. Defaults to 'HS256'. */
84
+ algorithm?: JwtAlgorithm;
85
+ /** For HS*: secret string or Uint8Array (>=32 bytes recommended for HS256). */
86
+ secret?: string | Uint8Array | CryptoKey;
87
+ /** For asymmetric algorithms: signing (private) key. */
88
+ privateKey?: CryptoKey | Uint8Array;
89
+ /** For asymmetric algorithms: verifying (public) key. */
90
+ publicKey?: CryptoKey | Uint8Array;
91
+ /** Issuer claim (`iss`). Optional. */
92
+ issuer?: string;
93
+ /** Audience claim (`aud`). Optional. */
94
+ audience?: string;
95
+ /** Optional denylist for revocation support. */
96
+ denylist?: DenylistStore;
97
+ /** Optional clock for testability. */
98
+ clock?: Clock;
99
+ }
100
+ /**
101
+ * Stateless credential store that signs the credential state into a JWT.
102
+ *
103
+ * The token IS the state — there is no internal map. State is reconstructed
104
+ * by verifying signature/claims on every retrieve. Revocation requires a
105
+ * `denylist`; without one, `revoke`/`update`/`consume` throw
106
+ * STATELESS_OPERATION_UNSUPPORTED.
107
+ */
108
+ declare class CredentialStoreJwt<TClaims extends object = object> implements CredentialStore<TClaims> {
109
+ private readonly algorithm;
110
+ private readonly signingKey;
111
+ private readonly verifyingKey;
112
+ private readonly issuer?;
113
+ private readonly audience?;
114
+ private readonly denylist?;
115
+ private readonly clock;
116
+ /**
117
+ * Per-user revocation epoch (ms). `revokeAllForUser` sets this to
118
+ * `clock.now()`; `retrieve` rejects any token whose `iatMs` predates the
119
+ * user's epoch (same-ms mints are accepted so recovery/invite flows can
120
+ * revoke and re-issue in one tick). Compensates for JWT statelessness so
121
+ * password-change cascades invalidate tokens minted before the change.
122
+ * In-memory: resets on process restart (a known JWT limitation — production
123
+ * deployments needing durability should back this with an external store).
124
+ */
125
+ private readonly epochs;
126
+ constructor(opts: CredentialStoreJwtOptions);
127
+ persist(state: CredentialState<TClaims>, ttl?: number): Promise<string>;
128
+ retrieve(token: string): Promise<CredentialState<TClaims> | null>;
129
+ consume(token: string): Promise<CredentialState<TClaims> | null>;
130
+ update(token: string, state: CredentialState<TClaims>): Promise<string>;
131
+ revoke(token: string): Promise<void>;
132
+ revokeAllForUser(userId: string): Promise<number>;
133
+ private requireDenylist;
134
+ /** Convert payload `exp` (seconds) to ms; fall back to a 60s window. */
135
+ private payloadExpMs;
136
+ /**
137
+ * Reject tokens minted before the user's revocation epoch. Same-ms mints
138
+ * (`iatMs === epoch`) are accepted so a workflow can revoke and re-issue in
139
+ * one tick. `iatMs` mirrors `iat` at ms precision; we fall back to
140
+ * `iat * 1000` for tokens that predate the mirror field.
141
+ */
142
+ private passesEpoch;
143
+ private verify;
144
+ private payloadToState;
145
+ }
146
+ //#endregion
147
+ //#region src/stores/encapsulated.d.ts
148
+ interface CredentialStoreEncapsulatedOptions {
149
+ /**
150
+ * Encryption key material. If a 32-byte Buffer/Uint8Array is provided it is
151
+ * used directly; otherwise it is run through scrypt to derive a 32-byte key.
152
+ */
153
+ secret: string | Buffer | Uint8Array;
154
+ /** Optional denylist for revocation support. */
155
+ denylist?: DenylistStore;
156
+ /** Optional clock for testability. */
157
+ clock?: Clock;
158
+ }
159
+ /**
160
+ * Stateless credential store that encrypts the credential state with
161
+ * AES-256-GCM. The token IS the encrypted blob, base64url-encoded as
162
+ * `iv (12B) || ciphertext || authTag (16B)`.
163
+ *
164
+ * Same denylist semantics as {@link CredentialStoreJwt}: revocation /
165
+ * single-use consume / update require a denylist; otherwise these operations
166
+ * throw STATELESS_OPERATION_UNSUPPORTED.
167
+ *
168
+ * Key derivation:
169
+ * - A 32-byte `Buffer`/`Uint8Array` `secret` is used as the AES key directly
170
+ * (no KDF).
171
+ * - Any other secret (string, shorter/longer buffer) is run through scrypt
172
+ * with a fixed library-scoped salt to produce a 32-byte key. The salt is
173
+ * intentionally fixed so keys remain stable across process restarts; if it
174
+ * were random, every previously issued token would become undecryptable.
175
+ * For maximum protection against rainbow tables on weak passphrases,
176
+ * provide a 32-byte random buffer as `secret` (the KDF path is skipped).
177
+ *
178
+ * Revocation caveat: `revokeAllForUser` uses an in-memory per-user epoch map.
179
+ * It does not survive process restart and does not sync across instances —
180
+ * the same limitation as `CredentialStoreJwt`. Use `CredentialStoreRedis` or
181
+ * `CredentialStoreAtscriptDb` when you need native enumeration with durable
182
+ * cross-instance cascade semantics.
183
+ */
184
+ declare class CredentialStoreEncapsulated<TClaims extends object = object> implements CredentialStore<TClaims> {
185
+ private readonly key;
186
+ private readonly denylist?;
187
+ private readonly clock;
188
+ /**
189
+ * Per-user revocation epoch (ms). `revokeAllForUser` sets this to
190
+ * `clock.now()`; `retrieve`/`consume` reject any decrypted state whose
191
+ * `issuedAt` is strictly less than the user's epoch (same-ms mints are
192
+ * accepted so recovery/invite flows can revoke and re-issue in the same
193
+ * tick). Mirrors the JWT store pattern; see class JSDoc for durability
194
+ * caveats.
195
+ */
196
+ private readonly epochs;
197
+ constructor(opts: CredentialStoreEncapsulatedOptions);
198
+ persist(state: CredentialState<TClaims>, ttl?: number): Promise<string>;
199
+ retrieve(token: string): Promise<CredentialState<TClaims> | null>;
200
+ consume(token: string): Promise<CredentialState<TClaims> | null>;
201
+ update(token: string, state: CredentialState<TClaims>): Promise<string>;
202
+ revoke(token: string): Promise<void>;
203
+ revokeAllForUser(userId: string): Promise<number>;
204
+ /**
205
+ * Reject decrypted payloads whose `issuedAt` predates the user's revocation
206
+ * epoch. Same-ms mints (`issuedAt === epoch`) are accepted so a workflow can
207
+ * revoke and re-issue in one tick. Mirrors `CredentialStoreJwt.passesEpoch`.
208
+ */
209
+ private passesEpoch;
210
+ private requireDenylist;
211
+ private decrypt;
212
+ }
213
+ //#endregion
214
+ //#region src/credential/auth-credential.d.ts
215
+ interface AuthCredentialOptions<TClaims extends object = object> {
216
+ /** Pluggable credential store (Memory, JWT, Encapsulated, ...). */
217
+ store: CredentialStore<TClaims>;
218
+ /** Default 'token' — distinguishes session-style from token-style use. */
219
+ method?: "session" | "token";
220
+ /** Access token TTL in milliseconds. Defaults to 1 hour. Must be > 0. */
221
+ accessTtl?: number;
222
+ /** If provided, refresh tokens are enabled. */
223
+ refresh?: RefreshConfig;
224
+ /**
225
+ * Optional denylist consulted by `validate` keyed on the raw token.
226
+ *
227
+ * Note: stateless stores (JWT, Encapsulated) maintain their own denylist
228
+ * keyed on `jti` for `revoke`/`update`/`consume`. Sharing a single
229
+ * `DenylistStore` instance across both is safe (the keyspaces are disjoint:
230
+ * raw tokens vs UUID jti) but conceptually they serve different purposes.
231
+ */
232
+ denylist?: DenylistStore;
233
+ /** Maximum concurrent active access credentials per user. */
234
+ maxConcurrent?: number;
235
+ /** Behavior when limit reached: 'reject' (default) or 'evict-oldest'. */
236
+ onLimit?: "reject" | "evict-oldest";
237
+ /** Optional clock for testability. */
238
+ clock?: Clock;
239
+ }
240
+ interface IssueOptions<TClaims extends object = object> {
241
+ claims?: TClaims;
242
+ metadata?: CredentialMetadata;
243
+ }
244
+ /**
245
+ * Orchestrates credential issuance, validation, refresh, and revocation
246
+ * on top of a pluggable {@link CredentialStore}.
247
+ *
248
+ * Design notes:
249
+ * - `credentialId` returned in {@link AuthContext} is a SHA-256 fingerprint of
250
+ * the access token, never the token itself. The fingerprint is stable
251
+ * per-token, safe to log/persist, and cannot be replayed against the API.
252
+ * M3 will switch to a `jti` claim for JWT/encapsulated stores.
253
+ * - `kind: 'access' | 'refresh'` on {@link CredentialState} discriminates
254
+ * tokens stored side-by-side in the same store.
255
+ * - `listForUser` and `maxConcurrent` consider only access-kind credentials,
256
+ * matching how callers typically display "active sessions".
257
+ * - On detected refresh-reuse-after-grace, the orchestrator best-effort
258
+ * revokes ALL credentials for the affected user (access + refresh).
259
+ * This is OAuth-best-practice theft response; documented and intentional.
260
+ */
261
+ declare class AuthCredential<TClaims extends object = object> {
262
+ private readonly store;
263
+ private readonly method;
264
+ private readonly accessTtl;
265
+ private readonly refreshConfig?;
266
+ private readonly denylist?;
267
+ private readonly maxConcurrent?;
268
+ private readonly onLimit;
269
+ private readonly clock;
270
+ /**
271
+ * Recently-consumed refresh tokens, keyed by the raw refresh token string.
272
+ * Lets `'always'` rotation detect reuse: stateful stores forget the token
273
+ * after `consume`, and stateless (JWT) stores hide it behind a denylist hit
274
+ * on `retrieve`. Without this map the orchestrator can no longer distinguish
275
+ * "fake token" from "previously valid token replayed". Pruned lazily on
276
+ * access; bounded by refresh TTL.
277
+ */
278
+ private readonly consumedRefreshes;
279
+ constructor(opts: AuthCredentialOptions<TClaims>);
280
+ issue(userId: string, options?: IssueOptions<TClaims>): Promise<IssueResult>;
281
+ validate(accessToken: string): Promise<AuthContext<TClaims> | null>;
282
+ refresh(refreshToken: string): Promise<IssueResult>;
283
+ private refreshNone;
284
+ private refreshSliding;
285
+ revoke(token: string): Promise<void>;
286
+ revokeAllForUser(userId: string): Promise<number>;
287
+ listForUser(userId: string): Promise<Array<AuthContext<TClaims>>>;
288
+ private enforceConcurrencyLimit;
289
+ private issueAccessFromRefresh;
290
+ private issueRotatedPair;
291
+ private lookupConsumedRefresh;
292
+ private fireRefreshReuseTheftResponse;
293
+ }
294
+ //#endregion
295
+ //#region src/email.d.ts
296
+ /**
297
+ * Discriminator for auth-related email events emitted by the workflow stack.
298
+ *
299
+ * `mfa.code` is reserved for v2 — v1 is TOTP only. `login.pincode`,
300
+ * `recovery.pincode`, `invite.pincode` and `notifyNewDevice` are added with
301
+ * BIG 3.1 (login workflow re-implementation).
302
+ */
303
+ type AuthEmailKind = "recovery.magicLink" | "invite.magicLink" | "mfa.code" | "login.pincode" | "recovery.pincode" | "invite.pincode" | "notifyNewDevice";
304
+ /**
305
+ * Structured event passed to `EmailSender.send()` from inside the auth
306
+ * workflows. Flat and serialisable so consumers can route it to any
307
+ * transport (templated mailer, queue, webhook).
308
+ */
309
+ interface AuthEmailEvent {
310
+ kind: AuthEmailKind;
311
+ recipient: string;
312
+ /** Resume URL for magic-link events. Absent for code / notify events. */
313
+ url?: string;
314
+ /** Numeric OTP for code-bearing events. Absent for magic links / notify. */
315
+ code?: string;
316
+ /** Unix-ms timestamp at which the token / code expires. */
317
+ expiresAt: number;
318
+ /** Recipient's username when known (omitted for invites to new accounts). */
319
+ username?: string;
320
+ /** Free-form payload (e.g. invite-side `roles: string[]`, notify-side `ip`). */
321
+ metadata?: Record<string, unknown>;
322
+ }
323
+ /**
324
+ * Transport interface implemented by consumers. Aoothjs ships no
325
+ * implementation — wire SendGrid / SES / Twilio / queue here.
326
+ *
327
+ * The workflow `await`s this call, so it should not block on slow downstream
328
+ * transports — push to a queue and return.
329
+ */
330
+ interface EmailSender {
331
+ send(event: AuthEmailEvent): Promise<void>;
332
+ }
333
+ //#endregion
334
+ //#region src/sms.d.ts
335
+ /**
336
+ * Discriminator for SMS events emitted by the auth workflow stack.
337
+ *
338
+ * Mirrors {@link AuthEmailKind} — kept on a separate union because email and
339
+ * SMS transports do not overlap (email carries `url` for magic links; SMS
340
+ * never does).
341
+ */
342
+ type AuthSmsKind = "login.pincode" | "recovery.pincode" | "invite.pincode";
343
+ /**
344
+ * Structured event passed to {@link SmsSender}`.send()` from inside the auth
345
+ * workflows. Flat and serialisable so consumers can route it to any
346
+ * SMS gateway (Twilio, SNS, queue).
347
+ */
348
+ interface AuthSmsEvent {
349
+ kind: AuthSmsKind;
350
+ /** E.164 phone number. */
351
+ recipient: string;
352
+ code: string;
353
+ /** Pincode TTL in ms — convenience for templating "expires in N min". */
354
+ ttlMs: number;
355
+ /** User id when known (omitted for pre-resolved flows). */
356
+ userId?: string;
357
+ }
358
+ /**
359
+ * Transport interface implemented by consumers. Aoothjs ships no concrete
360
+ * SMS adapter (Twilio / SNS / etc. are consumer-specific) — only this
361
+ * interface. Register via `setProvideRegistry([SmsSender, () => mySender])`.
362
+ *
363
+ * Boot-time invariant: when a workflow's options include SMS as an MFA
364
+ * transport, the workflow constructor throws if no `SmsSender` is
365
+ * registered (fail loud, not at first user attempt).
366
+ */
367
+ interface SmsSender {
368
+ send(event: AuthSmsEvent): Promise<void>;
369
+ }
370
+ //#endregion
371
+ //#region src/magic-link.d.ts
372
+ /**
373
+ * Consumer-supplied URL builder. The consumer chooses route, query
374
+ * convention, and base URL — aoothjs does not assume a domain or scheme.
375
+ *
376
+ * Recommended convention: include the token as `?wfs=<token>` so the
377
+ * frontend can mount `<AsWfForm initialToken="...">` to resume the flow.
378
+ */
379
+ type BuildMagicLinkUrl = (kind: AuthEmailKind, token: string) => string;
380
+ /**
381
+ * 32 bytes of CSPRNG entropy (256 bits) encoded as base64url — 43 chars,
382
+ * URL-safe. Strong enough to survive short TTLs against online guessing.
383
+ */
384
+ declare function generateMagicLinkToken(): string;
385
+ //#endregion
386
+ export { type AuthContext, AuthCredential, type AuthCredentialOptions, type AuthEmailEvent, type AuthEmailKind, AuthError, type AuthErrorType, type AuthSmsEvent, type AuthSmsKind, type BuildMagicLinkUrl, type Clock, type CredentialMetadata, type CredentialState, type CredentialStore, CredentialStoreEncapsulated, type CredentialStoreEncapsulatedOptions, CredentialStoreJwt, type CredentialStoreJwtOptions, CredentialStoreMemory, type DenylistStore, DenylistStoreMemory, type EmailSender, type IssueOptions, type IssueResult, type JwtAlgorithm, type RefreshConfig, type SmsSender, defaultClock, generateMagicLinkToken };