@aooth/auth 0.1.6 → 0.1.8
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/atscript-db.cjs +36 -25
- package/dist/atscript-db.d.cts +28 -22
- package/dist/atscript-db.d.mts +28 -22
- package/dist/atscript-db.mjs +36 -25
- package/dist/authz.cjs +168 -0
- package/dist/authz.d.cts +252 -0
- package/dist/authz.d.mts +252 -0
- package/dist/authz.mjs +161 -0
- package/dist/client.cjs +59 -0
- package/dist/client.d.cts +64 -0
- package/dist/client.d.mts +64 -0
- package/dist/client.mjs +58 -0
- package/dist/clock-Bdsep_1j.mjs +4 -0
- package/dist/clock-BjXa0LXb.d.cts +14 -0
- package/dist/clock-BjXa0LXb.d.mts +14 -0
- package/dist/clock-Bl-H3eqE.cjs +9 -0
- package/dist/index.cjs +294 -65
- package/dist/index.d.cts +166 -44
- package/dist/index.d.mts +166 -44
- package/dist/index.mjs +289 -60
- package/dist/payload-BJjvj8AH.cjs +32 -0
- package/dist/payload-D-DzH5-J.mjs +27 -0
- package/dist/redis.cjs +9 -0
- package/dist/redis.d.cts +8 -7
- package/dist/redis.d.mts +8 -7
- package/dist/redis.mjs +9 -0
- package/dist/store-BG6m6oSJ.d.cts +263 -0
- package/dist/store-BG6m6oSJ.d.mts +263 -0
- package/package.json +28 -10
- package/src/atscript-db/auth-credential.as +16 -10
- package/src/atscript-db/auth-credential.as.d.ts +68 -0
- package/dist/store-B1t8KkfA.d.mts +0 -124
- package/dist/store-untAtWQz.d.cts +0 -124
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { a as CredentialState, i as CredentialMetadata, n as DenylistStore, o as
|
|
1
|
+
import { a as CredentialState, c as RefreshConfig, i as CredentialMetadata, l as SessionEnricher, n as DenylistStore, o as EnrichedSession, r as AuthContext, s as IssueResult, t as CredentialStore, u as SessionInfo } from "./store-BG6m6oSJ.cjs";
|
|
2
|
+
import { n as defaultClock, t as Clock } from "./clock-BjXa0LXb.cjs";
|
|
2
3
|
import { CryptoKey } from "jose";
|
|
3
4
|
|
|
4
5
|
//#region src/errors.d.ts
|
|
@@ -10,19 +11,6 @@ declare class AuthError extends Error {
|
|
|
10
11
|
constructor(type: AuthErrorType, message?: string, details?: Record<string, unknown> | undefined);
|
|
11
12
|
}
|
|
12
13
|
//#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
14
|
//#region src/stores/memory.d.ts
|
|
27
15
|
/**
|
|
28
16
|
* In-memory implementation of CredentialStore using random opaque token IDs.
|
|
@@ -32,20 +20,21 @@ declare const defaultClock: Clock;
|
|
|
32
20
|
* `listForUser` without scanning the full primary map.
|
|
33
21
|
* - Both maps are kept in sync on every mutation.
|
|
34
22
|
*/
|
|
35
|
-
declare class CredentialStoreMemory<
|
|
23
|
+
declare class CredentialStoreMemory<TPayload extends object = object> implements CredentialStore<TPayload> {
|
|
36
24
|
private readonly states;
|
|
37
25
|
private readonly byUser;
|
|
38
26
|
private readonly clock;
|
|
39
27
|
constructor(opts?: {
|
|
40
28
|
clock?: Clock;
|
|
41
29
|
});
|
|
42
|
-
persist(state: CredentialState
|
|
43
|
-
retrieve(token: string): Promise<CredentialState
|
|
44
|
-
consume(token: string): Promise<CredentialState
|
|
45
|
-
update(token: string, state: CredentialState
|
|
30
|
+
persist(state: CredentialState & TPayload, ttl?: number): Promise<string>;
|
|
31
|
+
retrieve(token: string): Promise<(CredentialState & TPayload) | null>;
|
|
32
|
+
consume(token: string): Promise<(CredentialState & TPayload) | null>;
|
|
33
|
+
update(token: string, state: CredentialState & TPayload): Promise<string>;
|
|
46
34
|
revoke(token: string): Promise<void>;
|
|
35
|
+
touch(token: string, at: number): Promise<void>;
|
|
47
36
|
revokeAllForUser(userId: string): Promise<number>;
|
|
48
|
-
listForUser(userId: string): Promise<Array<CredentialState
|
|
37
|
+
listForUser(userId: string): Promise<Array<CredentialState & TPayload & {
|
|
49
38
|
token: string;
|
|
50
39
|
}>>;
|
|
51
40
|
private indexAdd;
|
|
@@ -105,7 +94,7 @@ interface CredentialStoreJwtOptions {
|
|
|
105
94
|
* `denylist`; without one, `revoke`/`update`/`consume` throw
|
|
106
95
|
* STATELESS_OPERATION_UNSUPPORTED.
|
|
107
96
|
*/
|
|
108
|
-
declare class CredentialStoreJwt<
|
|
97
|
+
declare class CredentialStoreJwt<TPayload extends object = object> implements CredentialStore<TPayload> {
|
|
109
98
|
private readonly algorithm;
|
|
110
99
|
private readonly signingKey;
|
|
111
100
|
private readonly verifyingKey;
|
|
@@ -124,12 +113,13 @@ declare class CredentialStoreJwt<TClaims extends object = object> implements Cre
|
|
|
124
113
|
*/
|
|
125
114
|
private readonly epochs;
|
|
126
115
|
constructor(opts: CredentialStoreJwtOptions);
|
|
127
|
-
persist(state: CredentialState
|
|
128
|
-
retrieve(token: string): Promise<CredentialState
|
|
129
|
-
consume(token: string): Promise<CredentialState
|
|
130
|
-
update(token: string, state: CredentialState
|
|
116
|
+
persist(state: CredentialState & TPayload, ttl?: number): Promise<string>;
|
|
117
|
+
retrieve(token: string): Promise<(CredentialState & TPayload) | null>;
|
|
118
|
+
consume(token: string): Promise<(CredentialState & TPayload) | null>;
|
|
119
|
+
update(token: string, state: CredentialState & TPayload): Promise<string>;
|
|
131
120
|
revoke(token: string): Promise<void>;
|
|
132
121
|
revokeAllForUser(userId: string): Promise<number>;
|
|
122
|
+
deriveSubkey(label: string): Buffer;
|
|
133
123
|
private requireDenylist;
|
|
134
124
|
/** Convert payload `exp` (seconds) to ms; fall back to a 60s window. */
|
|
135
125
|
private payloadExpMs;
|
|
@@ -181,7 +171,7 @@ interface CredentialStoreEncapsulatedOptions {
|
|
|
181
171
|
* `CredentialStoreAtscriptDb` when you need native enumeration with durable
|
|
182
172
|
* cross-instance cascade semantics.
|
|
183
173
|
*/
|
|
184
|
-
declare class CredentialStoreEncapsulated<
|
|
174
|
+
declare class CredentialStoreEncapsulated<TPayload extends object = object> implements CredentialStore<TPayload> {
|
|
185
175
|
private readonly key;
|
|
186
176
|
private readonly denylist?;
|
|
187
177
|
private readonly clock;
|
|
@@ -195,12 +185,13 @@ declare class CredentialStoreEncapsulated<TClaims extends object = object> imple
|
|
|
195
185
|
*/
|
|
196
186
|
private readonly epochs;
|
|
197
187
|
constructor(opts: CredentialStoreEncapsulatedOptions);
|
|
198
|
-
persist(state: CredentialState
|
|
199
|
-
retrieve(token: string): Promise<CredentialState
|
|
200
|
-
consume(token: string): Promise<CredentialState
|
|
201
|
-
update(token: string, state: CredentialState
|
|
188
|
+
persist(state: CredentialState & TPayload, ttl?: number): Promise<string>;
|
|
189
|
+
retrieve(token: string): Promise<(CredentialState & TPayload) | null>;
|
|
190
|
+
consume(token: string): Promise<(CredentialState & TPayload) | null>;
|
|
191
|
+
update(token: string, state: CredentialState & TPayload): Promise<string>;
|
|
202
192
|
revoke(token: string): Promise<void>;
|
|
203
193
|
revokeAllForUser(userId: string): Promise<number>;
|
|
194
|
+
deriveSubkey(label: string): Buffer;
|
|
204
195
|
/**
|
|
205
196
|
* Reject decrypted payloads whose `issuedAt` predates the user's revocation
|
|
206
197
|
* epoch. Same-ms mints (`issuedAt === epoch`) are accepted so a workflow can
|
|
@@ -212,9 +203,9 @@ declare class CredentialStoreEncapsulated<TClaims extends object = object> imple
|
|
|
212
203
|
}
|
|
213
204
|
//#endregion
|
|
214
205
|
//#region src/credential/auth-credential.d.ts
|
|
215
|
-
interface AuthCredentialOptions<
|
|
206
|
+
interface AuthCredentialOptions<TPayload extends object = object> {
|
|
216
207
|
/** Pluggable credential store (Memory, JWT, Encapsulated, ...). */
|
|
217
|
-
store: CredentialStore<
|
|
208
|
+
store: CredentialStore<TPayload>;
|
|
218
209
|
/** Default 'token' — distinguishes session-style from token-style use. */
|
|
219
210
|
method?: "session" | "token";
|
|
220
211
|
/** Access token TTL in milliseconds. Defaults to 1 hour. Must be > 0. */
|
|
@@ -234,13 +225,60 @@ interface AuthCredentialOptions<TClaims extends object = object> {
|
|
|
234
225
|
maxConcurrent?: number;
|
|
235
226
|
/** Behavior when limit reached: 'reject' (default) or 'evict-oldest'. */
|
|
236
227
|
onLimit?: "reject" | "evict-oldest";
|
|
228
|
+
/**
|
|
229
|
+
* Track per-session activity time (`lastSeenAt`). Default `false` — no extra
|
|
230
|
+
* writes; `listSessions` falls back to `createdAt`.
|
|
231
|
+
* - `'refresh'` (cheap): stamp `lastSeenAt` on the newly-minted credentials
|
|
232
|
+
* during refresh — piggybacks the rotation write, no extra round-trip.
|
|
233
|
+
* - `'validate'` (accurate, costly): `store.touch(token, now)` on every
|
|
234
|
+
* successful `validate()` — one write per authenticated request. Requires a
|
|
235
|
+
* store that implements `touch`; a no-op on stores that don't.
|
|
236
|
+
*/
|
|
237
|
+
trackLastSeen?: "refresh" | "validate" | false;
|
|
237
238
|
/** Optional clock for testability. */
|
|
238
239
|
clock?: Clock;
|
|
239
240
|
}
|
|
240
|
-
|
|
241
|
-
|
|
241
|
+
/**
|
|
242
|
+
* Options for {@link AuthCredential.issue}. The credential's typed payload
|
|
243
|
+
* `TPayload` (the root fields a consumer added to their credential model — e.g.
|
|
244
|
+
* `@arbac.attenuate.*`-annotated fields) is spread flat alongside the
|
|
245
|
+
* framework-level hints below. Reserved keys `metadata`, `sessionId`, `ttl`,
|
|
246
|
+
* `expiresAt`, `kind` (and the {@link CredentialState} envelope keys) must not be
|
|
247
|
+
* reused as payload field names.
|
|
248
|
+
*/
|
|
249
|
+
type IssueOptions<TPayload extends object = object> = TPayload & {
|
|
242
250
|
metadata?: CredentialMetadata;
|
|
243
|
-
|
|
251
|
+
/**
|
|
252
|
+
* Pre-supply the session id. Omit to let `issue()` mint a random opaque one.
|
|
253
|
+
* Both the access and refresh tokens of this login share it.
|
|
254
|
+
*/
|
|
255
|
+
sessionId?: string;
|
|
256
|
+
/**
|
|
257
|
+
* Per-mint access-token lifetime in **milliseconds**, overriding the
|
|
258
|
+
* instance-level `accessTtl` for THIS credential only — so one `AuthCredential`
|
|
259
|
+
* can mint, say, a 30-minute browser session AND a long-lived PAT/CLI token
|
|
260
|
+
* without a second instance/posture. Must be `> 0`. Mutually exclusive with
|
|
261
|
+
* {@link expiresAt}. The refresh token (if any) is unaffected — it keeps
|
|
262
|
+
* `refresh.ttl`.
|
|
263
|
+
*/
|
|
264
|
+
ttl?: number;
|
|
265
|
+
/**
|
|
266
|
+
* Absolute access-token expiry instant (ms since epoch), overriding both
|
|
267
|
+
* `ttl` and the instance `accessTtl`. Mutually exclusive with {@link ttl}.
|
|
268
|
+
* Use when the caller already holds the exact instant.
|
|
269
|
+
*/
|
|
270
|
+
expiresAt?: number;
|
|
271
|
+
/**
|
|
272
|
+
* Semantic credential kind for THIS mint — e.g. `"cli-session"` / `"pat"`.
|
|
273
|
+
* Distinct from the internal access/refresh discriminator
|
|
274
|
+
* ({@link CredentialState.kind}): it is stored in `metadata.credentialKind`
|
|
275
|
+
* and carried forward across rotation, so the whole session family is
|
|
276
|
+
* labelled. Surfaced as {@link SessionInfo.kind} and consumed by the
|
|
277
|
+
* `listSessions({ kind })` filter to keep non-browser credentials out of the
|
|
278
|
+
* default "active sessions" view. Omit for an ordinary interactive session.
|
|
279
|
+
*/
|
|
280
|
+
kind?: string;
|
|
281
|
+
};
|
|
244
282
|
/**
|
|
245
283
|
* Orchestrates credential issuance, validation, refresh, and revocation
|
|
246
284
|
* on top of a pluggable {@link CredentialStore}.
|
|
@@ -254,11 +292,12 @@ interface IssueOptions<TClaims extends object = object> {
|
|
|
254
292
|
* tokens stored side-by-side in the same store.
|
|
255
293
|
* - `listForUser` and `maxConcurrent` consider only access-kind credentials,
|
|
256
294
|
* matching how callers typically display "active sessions".
|
|
257
|
-
* - On detected refresh-reuse
|
|
258
|
-
*
|
|
259
|
-
*
|
|
295
|
+
* - On detected refresh-reuse, the orchestrator best-effort revokes the
|
|
296
|
+
* compromised token family (the OAuth-best-practice theft response). Set
|
|
297
|
+
* `refresh.reuseResponse: 'user'` to escalate to revoking ALL of the user's
|
|
298
|
+
* sessions. See {@link RefreshConfig.reuseResponse}.
|
|
260
299
|
*/
|
|
261
|
-
declare class AuthCredential<
|
|
300
|
+
declare class AuthCredential<TPayload extends object = object> {
|
|
262
301
|
private readonly store;
|
|
263
302
|
private readonly method;
|
|
264
303
|
private readonly accessTtl;
|
|
@@ -266,6 +305,7 @@ declare class AuthCredential<TClaims extends object = object> {
|
|
|
266
305
|
private readonly denylist?;
|
|
267
306
|
private readonly maxConcurrent?;
|
|
268
307
|
private readonly onLimit;
|
|
308
|
+
private readonly trackLastSeen;
|
|
269
309
|
private readonly clock;
|
|
270
310
|
/**
|
|
271
311
|
* Recently-consumed refresh tokens, keyed by the raw refresh token string.
|
|
@@ -276,20 +316,102 @@ declare class AuthCredential<TClaims extends object = object> {
|
|
|
276
316
|
* access; bounded by refresh TTL.
|
|
277
317
|
*/
|
|
278
318
|
private readonly consumedRefreshes;
|
|
279
|
-
constructor(opts: AuthCredentialOptions<
|
|
280
|
-
issue(userId: string, options?: IssueOptions<
|
|
281
|
-
validate(accessToken: string): Promise<AuthContext<
|
|
319
|
+
constructor(opts: AuthCredentialOptions<TPayload>);
|
|
320
|
+
issue(userId: string, options?: IssueOptions<TPayload>): Promise<IssueResult>;
|
|
321
|
+
validate(accessToken: string): Promise<AuthContext<TPayload> | null>;
|
|
282
322
|
refresh(refreshToken: string): Promise<IssueResult>;
|
|
283
323
|
private refreshNone;
|
|
324
|
+
/**
|
|
325
|
+
* `sliding` rotation: rotate on every use and slide the refresh expiry
|
|
326
|
+
* forward (rolling session). Grace-tolerant via the shared store-backed
|
|
327
|
+
* window.
|
|
328
|
+
*/
|
|
284
329
|
private refreshSliding;
|
|
330
|
+
/**
|
|
331
|
+
* `always` rotation: rotate on every use but keep a FIXED session ceiling —
|
|
332
|
+
* each rotated token inherits the family's original `expiresAt` (no sliding).
|
|
333
|
+
*
|
|
334
|
+
* On a stateful store this reuses the same store-backed grace window as
|
|
335
|
+
* `sliding` (so a benign concurrent refresh within grace is NOT mistaken for
|
|
336
|
+
* theft, even across instances). On a stateless store the old token cannot be
|
|
337
|
+
* kept valid (`update` re-issues), so it falls back to single-use semantics
|
|
338
|
+
* with a process-local reuse signal — the only mechanism possible there.
|
|
339
|
+
*/
|
|
340
|
+
private refreshAlways;
|
|
341
|
+
/**
|
|
342
|
+
* Shared rotation-with-grace mechanism for `sliding` and `always` on stateful
|
|
343
|
+
* stores. Keeps the old refresh valid + stamps `rotatedAt` on first rotation;
|
|
344
|
+
* within `rotationGraceMs` of that stamp it re-issues a fresh pair WITHOUT
|
|
345
|
+
* re-rotating (replay-tolerant); beyond grace it treats the re-presentation
|
|
346
|
+
* as theft. `preserveExpiry` selects fixed-ceiling (`always`) vs sliding
|
|
347
|
+
* (`sliding`) expiry for the new refresh token.
|
|
348
|
+
*/
|
|
349
|
+
private rotateWithGrace;
|
|
285
350
|
revoke(token: string): Promise<void>;
|
|
286
351
|
revokeAllForUser(userId: string): Promise<number>;
|
|
287
|
-
listForUser(userId: string): Promise<Array<AuthContext<
|
|
352
|
+
listForUser(userId: string): Promise<Array<AuthContext<TPayload>>>;
|
|
353
|
+
/**
|
|
354
|
+
* The session id for a credential: its stored `sessionId`, or the token
|
|
355
|
+
* fingerprint for legacy rows predating sessionId (so they surface as
|
|
356
|
+
* singleton sessions). The ONE place this fallback rule lives — shared by
|
|
357
|
+
* `validate()`, `listForUser()`, and the session-family methods so "this
|
|
358
|
+
* device" matching stays consistent across all of them.
|
|
359
|
+
*/
|
|
360
|
+
private sessionIdOf;
|
|
361
|
+
/** Session-grouping key for a stored credential entry (token attached). */
|
|
362
|
+
private sessionKeyOf;
|
|
363
|
+
/**
|
|
364
|
+
* List the user's active sessions, one row per token family (access +
|
|
365
|
+
* refresh + every rotation collapsed by `sessionId`). Newest first by
|
|
366
|
+
* `lastSeenAt` (or `createdAt` when activity isn't tracked). Returns `[]` for
|
|
367
|
+
* stores that can't enumerate (stateless JWT/encapsulated). Pass `enrich` to
|
|
368
|
+
* map each row through a {@link SessionEnricher} (device/location labels).
|
|
369
|
+
*/
|
|
370
|
+
listSessions(userId: string, opts?: {
|
|
371
|
+
enrich?: SessionEnricher;
|
|
372
|
+
kind?: string | string[];
|
|
373
|
+
}): Promise<SessionInfo[] | EnrichedSession[]>;
|
|
374
|
+
/**
|
|
375
|
+
* Revoke a single session — every token in its family (access + refresh +
|
|
376
|
+
* rotations). No-op for stores that can't enumerate. Other sessions keep
|
|
377
|
+
* validating.
|
|
378
|
+
*/
|
|
379
|
+
revokeSession(userId: string, sessionId: string): Promise<void>;
|
|
380
|
+
/**
|
|
381
|
+
* Revoke every session for the user EXCEPT `keepSessionId` ("log out
|
|
382
|
+
* everywhere else"). Returns the number of distinct sessions revoked. No-op
|
|
383
|
+
* (returns 0) for stores that can't enumerate.
|
|
384
|
+
*/
|
|
385
|
+
revokeOtherSessions(userId: string, keepSessionId: string): Promise<number>;
|
|
386
|
+
/**
|
|
387
|
+
* Derive a stable 32-byte key from this credential's underlying store secret,
|
|
388
|
+
* domain-separated by `label`. Lets adjacent subsystems (e.g. workflow-state
|
|
389
|
+
* encryption) reuse the auth secret without managing a second one. Throws if
|
|
390
|
+
* the store has no reusable symmetric secret (stateful/asymmetric) — callers
|
|
391
|
+
* should then require an explicit secret.
|
|
392
|
+
*/
|
|
393
|
+
deriveStateKey(label?: string): Buffer;
|
|
288
394
|
private enforceConcurrencyLimit;
|
|
289
395
|
private issueAccessFromRefresh;
|
|
396
|
+
/**
|
|
397
|
+
* Issue a fresh access + refresh pair off an existing refresh credential.
|
|
398
|
+
* `preserveExpiry` keeps the family's original refresh `expiresAt` (a fixed
|
|
399
|
+
* session ceiling, used by `always`); otherwise the new refresh slides to
|
|
400
|
+
* `now + ttl` (used by `sliding`). `rotateOld` stamps the old refresh as
|
|
401
|
+
* rotated (keeping it valid through the grace window) instead of consuming it.
|
|
402
|
+
*/
|
|
290
403
|
private issueRotatedPair;
|
|
291
404
|
private lookupConsumedRefresh;
|
|
292
405
|
private fireRefreshReuseTheftResponse;
|
|
406
|
+
/**
|
|
407
|
+
* Best-effort theft response for a detected refresh-token reuse. Fires the
|
|
408
|
+
* `onRotationReuse` hook, then revokes per {@link RefreshConfig.reuseResponse}:
|
|
409
|
+
* the compromised token family (`'session'`, default) or every session for
|
|
410
|
+
* the user (`'user'`). Falls back to user-wide revocation when the session
|
|
411
|
+
* can't be targeted (no `sessionId`, or a store that can't enumerate
|
|
412
|
+
* sessions). Always throws `REFRESH_REUSE_DETECTED`.
|
|
413
|
+
*/
|
|
414
|
+
private respondToRefreshReuse;
|
|
293
415
|
}
|
|
294
416
|
//#endregion
|
|
295
417
|
//#region src/email.d.ts
|
|
@@ -391,4 +513,4 @@ type BuildMagicLinkUrl = (kind: AuthEmailKind, token: string, ctx?: {
|
|
|
391
513
|
*/
|
|
392
514
|
declare function generateMagicLinkToken(): string;
|
|
393
515
|
//#endregion
|
|
394
|
-
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 };
|
|
516
|
+
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 EnrichedSession, type IssueOptions, type IssueResult, type JwtAlgorithm, type RefreshConfig, type SessionEnricher, type SessionInfo, type SmsSender, defaultClock, generateMagicLinkToken };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { a as CredentialState, i as CredentialMetadata, n as DenylistStore, o as
|
|
1
|
+
import { a as CredentialState, c as RefreshConfig, i as CredentialMetadata, l as SessionEnricher, n as DenylistStore, o as EnrichedSession, r as AuthContext, s as IssueResult, t as CredentialStore, u as SessionInfo } from "./store-BG6m6oSJ.mjs";
|
|
2
|
+
import { n as defaultClock, t as Clock } from "./clock-BjXa0LXb.mjs";
|
|
2
3
|
import { CryptoKey } from "jose";
|
|
3
4
|
|
|
4
5
|
//#region src/errors.d.ts
|
|
@@ -10,19 +11,6 @@ declare class AuthError extends Error {
|
|
|
10
11
|
constructor(type: AuthErrorType, message?: string, details?: Record<string, unknown> | undefined);
|
|
11
12
|
}
|
|
12
13
|
//#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
14
|
//#region src/stores/memory.d.ts
|
|
27
15
|
/**
|
|
28
16
|
* In-memory implementation of CredentialStore using random opaque token IDs.
|
|
@@ -32,20 +20,21 @@ declare const defaultClock: Clock;
|
|
|
32
20
|
* `listForUser` without scanning the full primary map.
|
|
33
21
|
* - Both maps are kept in sync on every mutation.
|
|
34
22
|
*/
|
|
35
|
-
declare class CredentialStoreMemory<
|
|
23
|
+
declare class CredentialStoreMemory<TPayload extends object = object> implements CredentialStore<TPayload> {
|
|
36
24
|
private readonly states;
|
|
37
25
|
private readonly byUser;
|
|
38
26
|
private readonly clock;
|
|
39
27
|
constructor(opts?: {
|
|
40
28
|
clock?: Clock;
|
|
41
29
|
});
|
|
42
|
-
persist(state: CredentialState
|
|
43
|
-
retrieve(token: string): Promise<CredentialState
|
|
44
|
-
consume(token: string): Promise<CredentialState
|
|
45
|
-
update(token: string, state: CredentialState
|
|
30
|
+
persist(state: CredentialState & TPayload, ttl?: number): Promise<string>;
|
|
31
|
+
retrieve(token: string): Promise<(CredentialState & TPayload) | null>;
|
|
32
|
+
consume(token: string): Promise<(CredentialState & TPayload) | null>;
|
|
33
|
+
update(token: string, state: CredentialState & TPayload): Promise<string>;
|
|
46
34
|
revoke(token: string): Promise<void>;
|
|
35
|
+
touch(token: string, at: number): Promise<void>;
|
|
47
36
|
revokeAllForUser(userId: string): Promise<number>;
|
|
48
|
-
listForUser(userId: string): Promise<Array<CredentialState
|
|
37
|
+
listForUser(userId: string): Promise<Array<CredentialState & TPayload & {
|
|
49
38
|
token: string;
|
|
50
39
|
}>>;
|
|
51
40
|
private indexAdd;
|
|
@@ -105,7 +94,7 @@ interface CredentialStoreJwtOptions {
|
|
|
105
94
|
* `denylist`; without one, `revoke`/`update`/`consume` throw
|
|
106
95
|
* STATELESS_OPERATION_UNSUPPORTED.
|
|
107
96
|
*/
|
|
108
|
-
declare class CredentialStoreJwt<
|
|
97
|
+
declare class CredentialStoreJwt<TPayload extends object = object> implements CredentialStore<TPayload> {
|
|
109
98
|
private readonly algorithm;
|
|
110
99
|
private readonly signingKey;
|
|
111
100
|
private readonly verifyingKey;
|
|
@@ -124,12 +113,13 @@ declare class CredentialStoreJwt<TClaims extends object = object> implements Cre
|
|
|
124
113
|
*/
|
|
125
114
|
private readonly epochs;
|
|
126
115
|
constructor(opts: CredentialStoreJwtOptions);
|
|
127
|
-
persist(state: CredentialState
|
|
128
|
-
retrieve(token: string): Promise<CredentialState
|
|
129
|
-
consume(token: string): Promise<CredentialState
|
|
130
|
-
update(token: string, state: CredentialState
|
|
116
|
+
persist(state: CredentialState & TPayload, ttl?: number): Promise<string>;
|
|
117
|
+
retrieve(token: string): Promise<(CredentialState & TPayload) | null>;
|
|
118
|
+
consume(token: string): Promise<(CredentialState & TPayload) | null>;
|
|
119
|
+
update(token: string, state: CredentialState & TPayload): Promise<string>;
|
|
131
120
|
revoke(token: string): Promise<void>;
|
|
132
121
|
revokeAllForUser(userId: string): Promise<number>;
|
|
122
|
+
deriveSubkey(label: string): Buffer;
|
|
133
123
|
private requireDenylist;
|
|
134
124
|
/** Convert payload `exp` (seconds) to ms; fall back to a 60s window. */
|
|
135
125
|
private payloadExpMs;
|
|
@@ -181,7 +171,7 @@ interface CredentialStoreEncapsulatedOptions {
|
|
|
181
171
|
* `CredentialStoreAtscriptDb` when you need native enumeration with durable
|
|
182
172
|
* cross-instance cascade semantics.
|
|
183
173
|
*/
|
|
184
|
-
declare class CredentialStoreEncapsulated<
|
|
174
|
+
declare class CredentialStoreEncapsulated<TPayload extends object = object> implements CredentialStore<TPayload> {
|
|
185
175
|
private readonly key;
|
|
186
176
|
private readonly denylist?;
|
|
187
177
|
private readonly clock;
|
|
@@ -195,12 +185,13 @@ declare class CredentialStoreEncapsulated<TClaims extends object = object> imple
|
|
|
195
185
|
*/
|
|
196
186
|
private readonly epochs;
|
|
197
187
|
constructor(opts: CredentialStoreEncapsulatedOptions);
|
|
198
|
-
persist(state: CredentialState
|
|
199
|
-
retrieve(token: string): Promise<CredentialState
|
|
200
|
-
consume(token: string): Promise<CredentialState
|
|
201
|
-
update(token: string, state: CredentialState
|
|
188
|
+
persist(state: CredentialState & TPayload, ttl?: number): Promise<string>;
|
|
189
|
+
retrieve(token: string): Promise<(CredentialState & TPayload) | null>;
|
|
190
|
+
consume(token: string): Promise<(CredentialState & TPayload) | null>;
|
|
191
|
+
update(token: string, state: CredentialState & TPayload): Promise<string>;
|
|
202
192
|
revoke(token: string): Promise<void>;
|
|
203
193
|
revokeAllForUser(userId: string): Promise<number>;
|
|
194
|
+
deriveSubkey(label: string): Buffer;
|
|
204
195
|
/**
|
|
205
196
|
* Reject decrypted payloads whose `issuedAt` predates the user's revocation
|
|
206
197
|
* epoch. Same-ms mints (`issuedAt === epoch`) are accepted so a workflow can
|
|
@@ -212,9 +203,9 @@ declare class CredentialStoreEncapsulated<TClaims extends object = object> imple
|
|
|
212
203
|
}
|
|
213
204
|
//#endregion
|
|
214
205
|
//#region src/credential/auth-credential.d.ts
|
|
215
|
-
interface AuthCredentialOptions<
|
|
206
|
+
interface AuthCredentialOptions<TPayload extends object = object> {
|
|
216
207
|
/** Pluggable credential store (Memory, JWT, Encapsulated, ...). */
|
|
217
|
-
store: CredentialStore<
|
|
208
|
+
store: CredentialStore<TPayload>;
|
|
218
209
|
/** Default 'token' — distinguishes session-style from token-style use. */
|
|
219
210
|
method?: "session" | "token";
|
|
220
211
|
/** Access token TTL in milliseconds. Defaults to 1 hour. Must be > 0. */
|
|
@@ -234,13 +225,60 @@ interface AuthCredentialOptions<TClaims extends object = object> {
|
|
|
234
225
|
maxConcurrent?: number;
|
|
235
226
|
/** Behavior when limit reached: 'reject' (default) or 'evict-oldest'. */
|
|
236
227
|
onLimit?: "reject" | "evict-oldest";
|
|
228
|
+
/**
|
|
229
|
+
* Track per-session activity time (`lastSeenAt`). Default `false` — no extra
|
|
230
|
+
* writes; `listSessions` falls back to `createdAt`.
|
|
231
|
+
* - `'refresh'` (cheap): stamp `lastSeenAt` on the newly-minted credentials
|
|
232
|
+
* during refresh — piggybacks the rotation write, no extra round-trip.
|
|
233
|
+
* - `'validate'` (accurate, costly): `store.touch(token, now)` on every
|
|
234
|
+
* successful `validate()` — one write per authenticated request. Requires a
|
|
235
|
+
* store that implements `touch`; a no-op on stores that don't.
|
|
236
|
+
*/
|
|
237
|
+
trackLastSeen?: "refresh" | "validate" | false;
|
|
237
238
|
/** Optional clock for testability. */
|
|
238
239
|
clock?: Clock;
|
|
239
240
|
}
|
|
240
|
-
|
|
241
|
-
|
|
241
|
+
/**
|
|
242
|
+
* Options for {@link AuthCredential.issue}. The credential's typed payload
|
|
243
|
+
* `TPayload` (the root fields a consumer added to their credential model — e.g.
|
|
244
|
+
* `@arbac.attenuate.*`-annotated fields) is spread flat alongside the
|
|
245
|
+
* framework-level hints below. Reserved keys `metadata`, `sessionId`, `ttl`,
|
|
246
|
+
* `expiresAt`, `kind` (and the {@link CredentialState} envelope keys) must not be
|
|
247
|
+
* reused as payload field names.
|
|
248
|
+
*/
|
|
249
|
+
type IssueOptions<TPayload extends object = object> = TPayload & {
|
|
242
250
|
metadata?: CredentialMetadata;
|
|
243
|
-
|
|
251
|
+
/**
|
|
252
|
+
* Pre-supply the session id. Omit to let `issue()` mint a random opaque one.
|
|
253
|
+
* Both the access and refresh tokens of this login share it.
|
|
254
|
+
*/
|
|
255
|
+
sessionId?: string;
|
|
256
|
+
/**
|
|
257
|
+
* Per-mint access-token lifetime in **milliseconds**, overriding the
|
|
258
|
+
* instance-level `accessTtl` for THIS credential only — so one `AuthCredential`
|
|
259
|
+
* can mint, say, a 30-minute browser session AND a long-lived PAT/CLI token
|
|
260
|
+
* without a second instance/posture. Must be `> 0`. Mutually exclusive with
|
|
261
|
+
* {@link expiresAt}. The refresh token (if any) is unaffected — it keeps
|
|
262
|
+
* `refresh.ttl`.
|
|
263
|
+
*/
|
|
264
|
+
ttl?: number;
|
|
265
|
+
/**
|
|
266
|
+
* Absolute access-token expiry instant (ms since epoch), overriding both
|
|
267
|
+
* `ttl` and the instance `accessTtl`. Mutually exclusive with {@link ttl}.
|
|
268
|
+
* Use when the caller already holds the exact instant.
|
|
269
|
+
*/
|
|
270
|
+
expiresAt?: number;
|
|
271
|
+
/**
|
|
272
|
+
* Semantic credential kind for THIS mint — e.g. `"cli-session"` / `"pat"`.
|
|
273
|
+
* Distinct from the internal access/refresh discriminator
|
|
274
|
+
* ({@link CredentialState.kind}): it is stored in `metadata.credentialKind`
|
|
275
|
+
* and carried forward across rotation, so the whole session family is
|
|
276
|
+
* labelled. Surfaced as {@link SessionInfo.kind} and consumed by the
|
|
277
|
+
* `listSessions({ kind })` filter to keep non-browser credentials out of the
|
|
278
|
+
* default "active sessions" view. Omit for an ordinary interactive session.
|
|
279
|
+
*/
|
|
280
|
+
kind?: string;
|
|
281
|
+
};
|
|
244
282
|
/**
|
|
245
283
|
* Orchestrates credential issuance, validation, refresh, and revocation
|
|
246
284
|
* on top of a pluggable {@link CredentialStore}.
|
|
@@ -254,11 +292,12 @@ interface IssueOptions<TClaims extends object = object> {
|
|
|
254
292
|
* tokens stored side-by-side in the same store.
|
|
255
293
|
* - `listForUser` and `maxConcurrent` consider only access-kind credentials,
|
|
256
294
|
* matching how callers typically display "active sessions".
|
|
257
|
-
* - On detected refresh-reuse
|
|
258
|
-
*
|
|
259
|
-
*
|
|
295
|
+
* - On detected refresh-reuse, the orchestrator best-effort revokes the
|
|
296
|
+
* compromised token family (the OAuth-best-practice theft response). Set
|
|
297
|
+
* `refresh.reuseResponse: 'user'` to escalate to revoking ALL of the user's
|
|
298
|
+
* sessions. See {@link RefreshConfig.reuseResponse}.
|
|
260
299
|
*/
|
|
261
|
-
declare class AuthCredential<
|
|
300
|
+
declare class AuthCredential<TPayload extends object = object> {
|
|
262
301
|
private readonly store;
|
|
263
302
|
private readonly method;
|
|
264
303
|
private readonly accessTtl;
|
|
@@ -266,6 +305,7 @@ declare class AuthCredential<TClaims extends object = object> {
|
|
|
266
305
|
private readonly denylist?;
|
|
267
306
|
private readonly maxConcurrent?;
|
|
268
307
|
private readonly onLimit;
|
|
308
|
+
private readonly trackLastSeen;
|
|
269
309
|
private readonly clock;
|
|
270
310
|
/**
|
|
271
311
|
* Recently-consumed refresh tokens, keyed by the raw refresh token string.
|
|
@@ -276,20 +316,102 @@ declare class AuthCredential<TClaims extends object = object> {
|
|
|
276
316
|
* access; bounded by refresh TTL.
|
|
277
317
|
*/
|
|
278
318
|
private readonly consumedRefreshes;
|
|
279
|
-
constructor(opts: AuthCredentialOptions<
|
|
280
|
-
issue(userId: string, options?: IssueOptions<
|
|
281
|
-
validate(accessToken: string): Promise<AuthContext<
|
|
319
|
+
constructor(opts: AuthCredentialOptions<TPayload>);
|
|
320
|
+
issue(userId: string, options?: IssueOptions<TPayload>): Promise<IssueResult>;
|
|
321
|
+
validate(accessToken: string): Promise<AuthContext<TPayload> | null>;
|
|
282
322
|
refresh(refreshToken: string): Promise<IssueResult>;
|
|
283
323
|
private refreshNone;
|
|
324
|
+
/**
|
|
325
|
+
* `sliding` rotation: rotate on every use and slide the refresh expiry
|
|
326
|
+
* forward (rolling session). Grace-tolerant via the shared store-backed
|
|
327
|
+
* window.
|
|
328
|
+
*/
|
|
284
329
|
private refreshSliding;
|
|
330
|
+
/**
|
|
331
|
+
* `always` rotation: rotate on every use but keep a FIXED session ceiling —
|
|
332
|
+
* each rotated token inherits the family's original `expiresAt` (no sliding).
|
|
333
|
+
*
|
|
334
|
+
* On a stateful store this reuses the same store-backed grace window as
|
|
335
|
+
* `sliding` (so a benign concurrent refresh within grace is NOT mistaken for
|
|
336
|
+
* theft, even across instances). On a stateless store the old token cannot be
|
|
337
|
+
* kept valid (`update` re-issues), so it falls back to single-use semantics
|
|
338
|
+
* with a process-local reuse signal — the only mechanism possible there.
|
|
339
|
+
*/
|
|
340
|
+
private refreshAlways;
|
|
341
|
+
/**
|
|
342
|
+
* Shared rotation-with-grace mechanism for `sliding` and `always` on stateful
|
|
343
|
+
* stores. Keeps the old refresh valid + stamps `rotatedAt` on first rotation;
|
|
344
|
+
* within `rotationGraceMs` of that stamp it re-issues a fresh pair WITHOUT
|
|
345
|
+
* re-rotating (replay-tolerant); beyond grace it treats the re-presentation
|
|
346
|
+
* as theft. `preserveExpiry` selects fixed-ceiling (`always`) vs sliding
|
|
347
|
+
* (`sliding`) expiry for the new refresh token.
|
|
348
|
+
*/
|
|
349
|
+
private rotateWithGrace;
|
|
285
350
|
revoke(token: string): Promise<void>;
|
|
286
351
|
revokeAllForUser(userId: string): Promise<number>;
|
|
287
|
-
listForUser(userId: string): Promise<Array<AuthContext<
|
|
352
|
+
listForUser(userId: string): Promise<Array<AuthContext<TPayload>>>;
|
|
353
|
+
/**
|
|
354
|
+
* The session id for a credential: its stored `sessionId`, or the token
|
|
355
|
+
* fingerprint for legacy rows predating sessionId (so they surface as
|
|
356
|
+
* singleton sessions). The ONE place this fallback rule lives — shared by
|
|
357
|
+
* `validate()`, `listForUser()`, and the session-family methods so "this
|
|
358
|
+
* device" matching stays consistent across all of them.
|
|
359
|
+
*/
|
|
360
|
+
private sessionIdOf;
|
|
361
|
+
/** Session-grouping key for a stored credential entry (token attached). */
|
|
362
|
+
private sessionKeyOf;
|
|
363
|
+
/**
|
|
364
|
+
* List the user's active sessions, one row per token family (access +
|
|
365
|
+
* refresh + every rotation collapsed by `sessionId`). Newest first by
|
|
366
|
+
* `lastSeenAt` (or `createdAt` when activity isn't tracked). Returns `[]` for
|
|
367
|
+
* stores that can't enumerate (stateless JWT/encapsulated). Pass `enrich` to
|
|
368
|
+
* map each row through a {@link SessionEnricher} (device/location labels).
|
|
369
|
+
*/
|
|
370
|
+
listSessions(userId: string, opts?: {
|
|
371
|
+
enrich?: SessionEnricher;
|
|
372
|
+
kind?: string | string[];
|
|
373
|
+
}): Promise<SessionInfo[] | EnrichedSession[]>;
|
|
374
|
+
/**
|
|
375
|
+
* Revoke a single session — every token in its family (access + refresh +
|
|
376
|
+
* rotations). No-op for stores that can't enumerate. Other sessions keep
|
|
377
|
+
* validating.
|
|
378
|
+
*/
|
|
379
|
+
revokeSession(userId: string, sessionId: string): Promise<void>;
|
|
380
|
+
/**
|
|
381
|
+
* Revoke every session for the user EXCEPT `keepSessionId` ("log out
|
|
382
|
+
* everywhere else"). Returns the number of distinct sessions revoked. No-op
|
|
383
|
+
* (returns 0) for stores that can't enumerate.
|
|
384
|
+
*/
|
|
385
|
+
revokeOtherSessions(userId: string, keepSessionId: string): Promise<number>;
|
|
386
|
+
/**
|
|
387
|
+
* Derive a stable 32-byte key from this credential's underlying store secret,
|
|
388
|
+
* domain-separated by `label`. Lets adjacent subsystems (e.g. workflow-state
|
|
389
|
+
* encryption) reuse the auth secret without managing a second one. Throws if
|
|
390
|
+
* the store has no reusable symmetric secret (stateful/asymmetric) — callers
|
|
391
|
+
* should then require an explicit secret.
|
|
392
|
+
*/
|
|
393
|
+
deriveStateKey(label?: string): Buffer;
|
|
288
394
|
private enforceConcurrencyLimit;
|
|
289
395
|
private issueAccessFromRefresh;
|
|
396
|
+
/**
|
|
397
|
+
* Issue a fresh access + refresh pair off an existing refresh credential.
|
|
398
|
+
* `preserveExpiry` keeps the family's original refresh `expiresAt` (a fixed
|
|
399
|
+
* session ceiling, used by `always`); otherwise the new refresh slides to
|
|
400
|
+
* `now + ttl` (used by `sliding`). `rotateOld` stamps the old refresh as
|
|
401
|
+
* rotated (keeping it valid through the grace window) instead of consuming it.
|
|
402
|
+
*/
|
|
290
403
|
private issueRotatedPair;
|
|
291
404
|
private lookupConsumedRefresh;
|
|
292
405
|
private fireRefreshReuseTheftResponse;
|
|
406
|
+
/**
|
|
407
|
+
* Best-effort theft response for a detected refresh-token reuse. Fires the
|
|
408
|
+
* `onRotationReuse` hook, then revokes per {@link RefreshConfig.reuseResponse}:
|
|
409
|
+
* the compromised token family (`'session'`, default) or every session for
|
|
410
|
+
* the user (`'user'`). Falls back to user-wide revocation when the session
|
|
411
|
+
* can't be targeted (no `sessionId`, or a store that can't enumerate
|
|
412
|
+
* sessions). Always throws `REFRESH_REUSE_DETECTED`.
|
|
413
|
+
*/
|
|
414
|
+
private respondToRefreshReuse;
|
|
293
415
|
}
|
|
294
416
|
//#endregion
|
|
295
417
|
//#region src/email.d.ts
|
|
@@ -391,4 +513,4 @@ type BuildMagicLinkUrl = (kind: AuthEmailKind, token: string, ctx?: {
|
|
|
391
513
|
*/
|
|
392
514
|
declare function generateMagicLinkToken(): string;
|
|
393
515
|
//#endregion
|
|
394
|
-
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 };
|
|
516
|
+
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 EnrichedSession, type IssueOptions, type IssueResult, type JwtAlgorithm, type RefreshConfig, type SessionEnricher, type SessionInfo, type SmsSender, defaultClock, generateMagicLinkToken };
|