@aooth/auth 0.1.47 → 0.1.48

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.
Files changed (33) hide show
  1. package/dist/atscript-db.cjs +3 -1
  2. package/dist/atscript-db.d.cts +4 -3
  3. package/dist/atscript-db.d.mts +4 -3
  4. package/dist/atscript-db.mjs +3 -1
  5. package/dist/auth-credential-C3qebKuy.d.mts +245 -0
  6. package/dist/auth-credential-CEijxoGU.d.cts +245 -0
  7. package/dist/authz.cjs +124 -35
  8. package/dist/authz.d.cts +78 -24
  9. package/dist/authz.d.mts +78 -24
  10. package/dist/authz.mjs +121 -37
  11. package/dist/{store-BG6m6oSJ.d.mts → clock-DJ_eroHW.d.cts} +50 -1
  12. package/dist/{store-BG6m6oSJ.d.cts → clock-DJ_eroHW.d.mts} +50 -1
  13. package/dist/{dynamic-client-store-DLRfntHr.mjs → dynamic-client-store-B1JwFHdP.mjs} +1 -0
  14. package/dist/{dynamic-client-store-DzqdUwnw.d.cts → dynamic-client-store-DJgtMjPw.d.mts} +58 -9
  15. package/dist/{dynamic-client-store-oHEsQaJg.d.mts → dynamic-client-store-DbiSLOj0.d.cts} +58 -9
  16. package/dist/{dynamic-client-store-DAStgv2j.cjs → dynamic-client-store-DykM9QOT.cjs} +1 -0
  17. package/dist/index.cjs +39 -26
  18. package/dist/index.d.cts +12 -218
  19. package/dist/index.d.mts +12 -218
  20. package/dist/index.mjs +39 -27
  21. package/dist/opaque-token-BaMOrC1A.mjs +13 -0
  22. package/dist/opaque-token-CaZgD2i6.cjs +18 -0
  23. package/dist/redis.d.cts +2 -2
  24. package/dist/redis.d.mts +2 -2
  25. package/dist/{store-N9daSmHS.d.cts → store-BYnpA7VK.d.cts} +1 -1
  26. package/dist/{store-jFgby4bK.d.mts → store-pKh0ZaGG.d.mts} +1 -1
  27. package/package.json +2 -2
  28. package/src/atscript-db/auth-credential.as +16 -0
  29. package/src/atscript-db/auth-credential.as.d.ts +4 -1
  30. package/src/atscript-db/dynamic-client.as +11 -1
  31. package/src/atscript-db/dynamic-client.as.d.ts +3 -0
  32. package/dist/clock-BjXa0LXb.d.cts +0 -14
  33. package/dist/clock-BjXa0LXb.d.mts +0 -14
package/dist/index.cjs CHANGED
@@ -1,6 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_clock = require("./clock-Bl-H3eqE.cjs");
3
3
  const require_payload = require("./payload-BJjvj8AH.cjs");
4
+ const require_opaque_token = require("./opaque-token-CaZgD2i6.cjs");
4
5
  let node_crypto = require("node:crypto");
5
6
  let jose = require("jose");
6
7
  //#region src/errors.ts
@@ -586,10 +587,21 @@ var AuthCredential = class {
586
587
  async issue(userId, options) {
587
588
  if (this.maxConcurrent !== void 0 && this.store.listForUser) await this.enforceConcurrencyLimit(userId);
588
589
  const now = this.clock.now();
589
- const { metadata, sessionId: providedSessionId, ttl, expiresAt, kind, ...payload } = options ?? {};
590
- const effectiveMetadata = kind !== void 0 ? {
590
+ const { metadata, sessionId: providedSessionId, ttl, expiresAt, kind, refresh, ...payload } = options ?? {};
591
+ let refreshTtl;
592
+ if (refresh === void 0) refreshTtl = this.refreshConfig?.ttl;
593
+ else if (refresh !== false) {
594
+ refreshTtl = refresh.ttl ?? this.refreshConfig?.ttl;
595
+ if (refreshTtl === void 0) throw new AuthError("INVALID_CONFIG", "issue: per-mint refresh needs a ttl when no instance refresh config exists");
596
+ if (refreshTtl <= 0) throw new AuthError("INVALID_CONFIG", `issue: refresh.ttl must be > 0 (got ${refreshTtl})`);
597
+ }
598
+ const perMintRefresh = refresh !== void 0 && refresh !== false;
599
+ const stampAccessTtl = refreshTtl !== void 0 && ttl !== void 0;
600
+ const effectiveMetadata = kind !== void 0 || stampAccessTtl || perMintRefresh ? {
591
601
  ...metadata,
592
- credentialKind: kind
602
+ ...kind !== void 0 && { credentialKind: kind },
603
+ ...stampAccessTtl && { accessTtl: ttl },
604
+ ...perMintRefresh && { refreshRotation: "always" }
593
605
  } : metadata;
594
606
  if (ttl !== void 0 && expiresAt !== void 0) throw new AuthError("INVALID_CONFIG", "issue: pass either `ttl` or `expiresAt`, not both");
595
607
  if (ttl !== void 0 && ttl <= 0) throw new AuthError("INVALID_CONFIG", `issue: ttl must be > 0 (got ${ttl})`);
@@ -607,17 +619,17 @@ var AuthCredential = class {
607
619
  const accessToken = await this.store.persist(accessState, accessStoreTtl);
608
620
  let refreshToken;
609
621
  let refreshExpiresAt;
610
- if (this.refreshConfig) {
622
+ if (refreshTtl !== void 0) {
611
623
  const refreshState = stateWithPayload(payload, {
612
624
  userId,
613
625
  issuedAt: now,
614
- expiresAt: now + this.refreshConfig.ttl,
626
+ expiresAt: now + refreshTtl,
615
627
  kind: "refresh",
616
628
  metadata: effectiveMetadata,
617
629
  sessionId
618
630
  });
619
- refreshToken = await this.store.persist(refreshState, this.refreshConfig.ttl);
620
- refreshExpiresAt = now + this.refreshConfig.ttl;
631
+ refreshToken = await this.store.persist(refreshState, refreshTtl);
632
+ refreshExpiresAt = now + refreshTtl;
621
633
  }
622
634
  return {
623
635
  accessToken,
@@ -642,9 +654,7 @@ var AuthCredential = class {
642
654
  expiresAt: state.expiresAt
643
655
  };
644
656
  }
645
- async refresh(refreshToken) {
646
- if (!this.refreshConfig) throw new AuthError("INVALID_CONFIG", "Refresh not enabled");
647
- const rotation = this.refreshConfig.rotation ?? "sliding";
657
+ async refresh(refreshToken, opts) {
648
658
  const now = this.clock.now();
649
659
  const oldState = await this.store.retrieve(refreshToken);
650
660
  if (!oldState) {
@@ -653,6 +663,8 @@ var AuthCredential = class {
653
663
  throw new AuthError("INVALID_TOKEN");
654
664
  }
655
665
  if (oldState.kind !== "refresh") throw new AuthError("INVALID_TOKEN", "Token is not a refresh credential");
666
+ await opts?.guard?.(oldState);
667
+ const rotation = oldState.metadata?.refreshRotation ?? (this.refreshConfig ? this.refreshConfig.rotation ?? "sliding" : "always");
656
668
  switch (rotation) {
657
669
  case "none": return await this.refreshNone(oldState, refreshToken, now);
658
670
  case "always": return await this.refreshAlways(oldState, refreshToken, now);
@@ -666,7 +678,8 @@ var AuthCredential = class {
666
678
  accessToken: newAccess.token,
667
679
  accessExpiresAt: newAccess.expiresAt,
668
680
  refreshToken,
669
- refreshExpiresAt: oldState.expiresAt
681
+ refreshExpiresAt: oldState.expiresAt,
682
+ userId: oldState.userId
670
683
  };
671
684
  }
672
685
  /**
@@ -709,8 +722,7 @@ var AuthCredential = class {
709
722
  * (`sliding`) expiry for the new refresh token.
710
723
  */
711
724
  async rotateWithGrace(oldState, refreshToken, now, preserveExpiry) {
712
- if (!this.refreshConfig) throw new AuthError("INVALID_CONFIG", "Refresh not enabled");
713
- const graceMs = this.refreshConfig.rotationGraceMs ?? DEFAULT_ROTATION_GRACE_MS;
725
+ const graceMs = this.refreshConfig?.rotationGraceMs ?? DEFAULT_ROTATION_GRACE_MS;
714
726
  if (typeof oldState.rotatedAt !== "number") return await this.issueRotatedPair(oldState, refreshToken, true, now, preserveExpiry);
715
727
  if (now - oldState.rotatedAt > graceMs) await this.respondToRefreshReuse({
716
728
  userId: oldState.userId,
@@ -855,18 +867,19 @@ var AuthCredential = class {
855
867
  for (let i = 0; i < toEvict && i < sorted.length; i++) await this.store.revoke(sorted[i].token);
856
868
  }
857
869
  async issueAccessFromRefresh(refreshState, now) {
870
+ const accessTtl = refreshState.metadata?.accessTtl ?? this.accessTtl;
858
871
  const accessState = stateWithPayload(require_payload.credentialPayloadOf(refreshState), {
859
872
  userId: refreshState.userId,
860
873
  issuedAt: now,
861
- expiresAt: now + this.accessTtl,
874
+ expiresAt: now + accessTtl,
862
875
  kind: "access",
863
876
  metadata: refreshState.metadata,
864
877
  sessionId: refreshState.sessionId,
865
878
  ...this.trackLastSeen === "refresh" && { lastSeenAt: now }
866
879
  });
867
880
  return {
868
- token: await this.store.persist(accessState, this.accessTtl),
869
- expiresAt: now + this.accessTtl
881
+ token: await this.store.persist(accessState, accessTtl),
882
+ expiresAt: now + accessTtl
870
883
  };
871
884
  }
872
885
  /**
@@ -877,10 +890,10 @@ var AuthCredential = class {
877
890
  * rotated (keeping it valid through the grace window) instead of consuming it.
878
891
  */
879
892
  async issueRotatedPair(oldRefreshState, oldRefreshToken, rotateOld, now, preserveExpiry = false) {
880
- if (!this.refreshConfig) throw new AuthError("INVALID_CONFIG", "Refresh not enabled");
881
- const access = await this.issueAccessFromRefresh(oldRefreshState, now);
882
- const refreshExpiresAt = preserveExpiry ? oldRefreshState.expiresAt : now + this.refreshConfig.ttl;
883
- const refreshTtl = preserveExpiry ? Math.max(0, oldRefreshState.expiresAt - now) : this.refreshConfig.ttl;
893
+ const slidingTtl = this.refreshConfig?.ttl;
894
+ if (!preserveExpiry && slidingTtl === void 0) throw new AuthError("INVALID_CONFIG", "Refresh not enabled");
895
+ const refreshExpiresAt = preserveExpiry ? oldRefreshState.expiresAt : now + slidingTtl;
896
+ const refreshTtl = preserveExpiry ? Math.max(0, oldRefreshState.expiresAt - now) : slidingTtl;
884
897
  const newRefreshState = stateWithPayload(require_payload.credentialPayloadOf(oldRefreshState), {
885
898
  userId: oldRefreshState.userId,
886
899
  issuedAt: now,
@@ -891,6 +904,7 @@ var AuthCredential = class {
891
904
  sessionId: oldRefreshState.sessionId,
892
905
  ...this.trackLastSeen === "refresh" && { lastSeenAt: now }
893
906
  });
907
+ const access = await this.issueAccessFromRefresh(oldRefreshState, now);
894
908
  const newRefreshToken = await this.store.persist(newRefreshState, refreshTtl);
895
909
  if (rotateOld) {
896
910
  const rotatedState = {
@@ -903,7 +917,8 @@ var AuthCredential = class {
903
917
  accessToken: access.token,
904
918
  accessExpiresAt: access.expiresAt,
905
919
  refreshToken: newRefreshToken,
906
- refreshExpiresAt
920
+ refreshExpiresAt,
921
+ userId: oldRefreshState.userId
907
922
  };
908
923
  }
909
924
  lookupConsumedRefresh(token) {
@@ -1136,12 +1151,9 @@ var RateLimiter = class {
1136
1151
  };
1137
1152
  //#endregion
1138
1153
  //#region src/magic-link.ts
1139
- /**
1140
- * 32 bytes of CSPRNG entropy (256 bits) encoded as base64url — 43 chars,
1141
- * URL-safe. Strong enough to survive short TTLs against online guessing.
1142
- */
1154
+ /** A magic-link token is a plain {@link generateOpaqueToken} mint. */
1143
1155
  function generateMagicLinkToken() {
1144
- return (0, node_crypto.randomBytes)(32).toString("base64url");
1156
+ return require_opaque_token.generateOpaqueToken();
1145
1157
  }
1146
1158
  //#endregion
1147
1159
  exports.AuthCredential = AuthCredential;
@@ -1156,6 +1168,7 @@ exports.RateLimiter = RateLimiter;
1156
1168
  exports.defaultClock = require_clock.defaultClock;
1157
1169
  exports.formatDurationMs = formatDurationMs;
1158
1170
  exports.generateMagicLinkToken = generateMagicLinkToken;
1171
+ exports.generateOpaqueToken = require_opaque_token.generateOpaqueToken;
1159
1172
  exports.parseDurationMs = parseDurationMs;
1160
1173
  exports.parseRateLimitRule = parseRateLimitRule;
1161
1174
  exports.renderRateLimitMessage = renderRateLimitMessage;
package/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
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";
3
- import { n as RateLimitStoreMemory, t as RateLimitStore } from "./store-N9daSmHS.cjs";
1
+ import { a as AuthContext, c as EnrichedSession, d as RefreshResult, f as SessionEnricher, i as DenylistStore, l as IssueResult, n as defaultClock, o as CredentialMetadata, p as SessionInfo, r as CredentialStore, s as CredentialState, t as Clock, u as RefreshConfig } from "./clock-DJ_eroHW.cjs";
2
+ import { i as RefreshCallOptions, n as AuthCredentialOptions, r as IssueOptions, t as AuthCredential } from "./auth-credential-CEijxoGU.cjs";
3
+ import { n as RateLimitStoreMemory, t as RateLimitStore } from "./store-BYnpA7VK.cjs";
4
4
  import { CryptoKey } from "jose";
5
5
 
6
6
  //#region src/errors.d.ts
@@ -203,218 +203,6 @@ declare class CredentialStoreEncapsulated<TPayload extends object = object> impl
203
203
  private decrypt;
204
204
  }
205
205
  //#endregion
206
- //#region src/credential/auth-credential.d.ts
207
- interface AuthCredentialOptions<TPayload extends object = object> {
208
- /** Pluggable credential store (Memory, JWT, Encapsulated, ...). */
209
- store: CredentialStore<TPayload>;
210
- /** Default 'token' — distinguishes session-style from token-style use. */
211
- method?: "session" | "token";
212
- /** Access token TTL in milliseconds. Defaults to 1 hour. Must be > 0. */
213
- accessTtl?: number;
214
- /** If provided, refresh tokens are enabled. */
215
- refresh?: RefreshConfig;
216
- /**
217
- * Optional denylist consulted by `validate` keyed on the raw token.
218
- *
219
- * Note: stateless stores (JWT, Encapsulated) maintain their own denylist
220
- * keyed on `jti` for `revoke`/`update`/`consume`. Sharing a single
221
- * `DenylistStore` instance across both is safe (the keyspaces are disjoint:
222
- * raw tokens vs UUID jti) but conceptually they serve different purposes.
223
- */
224
- denylist?: DenylistStore;
225
- /** Maximum concurrent active access credentials per user. */
226
- maxConcurrent?: number;
227
- /** Behavior when limit reached: 'reject' (default) or 'evict-oldest'. */
228
- onLimit?: "reject" | "evict-oldest";
229
- /**
230
- * Track per-session activity time (`lastSeenAt`). Default `false` — no extra
231
- * writes; `listSessions` falls back to `createdAt`.
232
- * - `'refresh'` (cheap): stamp `lastSeenAt` on the newly-minted credentials
233
- * during refresh — piggybacks the rotation write, no extra round-trip.
234
- * - `'validate'` (accurate, costly): `store.touch(token, now)` on every
235
- * successful `validate()` — one write per authenticated request. Requires a
236
- * store that implements `touch`; a no-op on stores that don't.
237
- */
238
- trackLastSeen?: "refresh" | "validate" | false;
239
- /** Optional clock for testability. */
240
- clock?: Clock;
241
- }
242
- /**
243
- * Options for {@link AuthCredential.issue}. The credential's typed payload
244
- * `TPayload` (the root fields a consumer added to their credential model — e.g.
245
- * `@arbac.attenuate.*`-annotated fields) is spread flat alongside the
246
- * framework-level hints below. Reserved keys `metadata`, `sessionId`, `ttl`,
247
- * `expiresAt`, `kind` (and the {@link CredentialState} envelope keys) must not be
248
- * reused as payload field names.
249
- */
250
- type IssueOptions<TPayload extends object = object> = TPayload & {
251
- metadata?: CredentialMetadata;
252
- /**
253
- * Pre-supply the session id. Omit to let `issue()` mint a random opaque one.
254
- * Both the access and refresh tokens of this login share it.
255
- */
256
- sessionId?: string;
257
- /**
258
- * Per-mint access-token lifetime in **milliseconds**, overriding the
259
- * instance-level `accessTtl` for THIS credential only — so one `AuthCredential`
260
- * can mint, say, a 30-minute browser session AND a long-lived PAT/CLI token
261
- * without a second instance/posture. Must be `> 0`. Mutually exclusive with
262
- * {@link expiresAt}. The refresh token (if any) is unaffected — it keeps
263
- * `refresh.ttl`.
264
- */
265
- ttl?: number;
266
- /**
267
- * Absolute access-token expiry instant (ms since epoch), overriding both
268
- * `ttl` and the instance `accessTtl`. Mutually exclusive with {@link ttl}.
269
- * Use when the caller already holds the exact instant.
270
- */
271
- expiresAt?: number;
272
- /**
273
- * Semantic credential kind for THIS mint — e.g. `"cli-session"` / `"pat"`.
274
- * Distinct from the internal access/refresh discriminator
275
- * ({@link CredentialState.kind}): it is stored in `metadata.credentialKind`
276
- * and carried forward across rotation, so the whole session family is
277
- * labelled. Surfaced as {@link SessionInfo.kind} and consumed by the
278
- * `listSessions({ kind })` filter to keep non-browser credentials out of the
279
- * default "active sessions" view. Omit for an ordinary interactive session.
280
- */
281
- kind?: string;
282
- };
283
- /**
284
- * Orchestrates credential issuance, validation, refresh, and revocation
285
- * on top of a pluggable {@link CredentialStore}.
286
- *
287
- * Design notes:
288
- * - `credentialId` returned in {@link AuthContext} is a SHA-256 fingerprint of
289
- * the access token, never the token itself. The fingerprint is stable
290
- * per-token, safe to log/persist, and cannot be replayed against the API.
291
- * M3 will switch to a `jti` claim for JWT/encapsulated stores.
292
- * - `kind: 'access' | 'refresh'` on {@link CredentialState} discriminates
293
- * tokens stored side-by-side in the same store.
294
- * - `listForUser` and `maxConcurrent` consider only access-kind credentials,
295
- * matching how callers typically display "active sessions".
296
- * - On detected refresh-reuse, the orchestrator best-effort revokes the
297
- * compromised token family (the OAuth-best-practice theft response). Set
298
- * `refresh.reuseResponse: 'user'` to escalate to revoking ALL of the user's
299
- * sessions. See {@link RefreshConfig.reuseResponse}.
300
- */
301
- declare class AuthCredential<TPayload extends object = object> {
302
- private readonly store;
303
- private readonly method;
304
- private readonly accessTtl;
305
- private readonly refreshConfig?;
306
- private readonly denylist?;
307
- private readonly maxConcurrent?;
308
- private readonly onLimit;
309
- private readonly trackLastSeen;
310
- private readonly clock;
311
- /**
312
- * Recently-consumed refresh tokens, keyed by the raw refresh token string.
313
- * Lets `'always'` rotation detect reuse: stateful stores forget the token
314
- * after `consume`, and stateless (JWT) stores hide it behind a denylist hit
315
- * on `retrieve`. Without this map the orchestrator can no longer distinguish
316
- * "fake token" from "previously valid token replayed". Pruned lazily on
317
- * access; bounded by refresh TTL.
318
- */
319
- private readonly consumedRefreshes;
320
- constructor(opts: AuthCredentialOptions<TPayload>);
321
- issue(userId: string, options?: IssueOptions<TPayload>): Promise<IssueResult>;
322
- validate(accessToken: string): Promise<AuthContext<TPayload> | null>;
323
- refresh(refreshToken: string): Promise<IssueResult>;
324
- private refreshNone;
325
- /**
326
- * `sliding` rotation: rotate on every use and slide the refresh expiry
327
- * forward (rolling session). Grace-tolerant via the shared store-backed
328
- * window.
329
- */
330
- private refreshSliding;
331
- /**
332
- * `always` rotation: rotate on every use but keep a FIXED session ceiling —
333
- * each rotated token inherits the family's original `expiresAt` (no sliding).
334
- *
335
- * On a stateful store this reuses the same store-backed grace window as
336
- * `sliding` (so a benign concurrent refresh within grace is NOT mistaken for
337
- * theft, even across instances). On a stateless store the old token cannot be
338
- * kept valid (`update` re-issues), so it falls back to single-use semantics
339
- * with a process-local reuse signal — the only mechanism possible there.
340
- */
341
- private refreshAlways;
342
- /**
343
- * Shared rotation-with-grace mechanism for `sliding` and `always` on stateful
344
- * stores. Keeps the old refresh valid + stamps `rotatedAt` on first rotation;
345
- * within `rotationGraceMs` of that stamp it re-issues a fresh pair WITHOUT
346
- * re-rotating (replay-tolerant); beyond grace it treats the re-presentation
347
- * as theft. `preserveExpiry` selects fixed-ceiling (`always`) vs sliding
348
- * (`sliding`) expiry for the new refresh token.
349
- */
350
- private rotateWithGrace;
351
- revoke(token: string): Promise<void>;
352
- revokeAllForUser(userId: string): Promise<number>;
353
- listForUser(userId: string): Promise<Array<AuthContext<TPayload>>>;
354
- /**
355
- * The session id for a credential: its stored `sessionId`, or the token
356
- * fingerprint for legacy rows predating sessionId (so they surface as
357
- * singleton sessions). The ONE place this fallback rule lives — shared by
358
- * `validate()`, `listForUser()`, and the session-family methods so "this
359
- * device" matching stays consistent across all of them.
360
- */
361
- private sessionIdOf;
362
- /** Session-grouping key for a stored credential entry (token attached). */
363
- private sessionKeyOf;
364
- /**
365
- * List the user's active sessions, one row per token family (access +
366
- * refresh + every rotation collapsed by `sessionId`). Newest first by
367
- * `lastSeenAt` (or `createdAt` when activity isn't tracked). Returns `[]` for
368
- * stores that can't enumerate (stateless JWT/encapsulated). Pass `enrich` to
369
- * map each row through a {@link SessionEnricher} (device/location labels).
370
- */
371
- listSessions(userId: string, opts?: {
372
- enrich?: SessionEnricher;
373
- kind?: string | string[];
374
- }): Promise<SessionInfo[] | EnrichedSession[]>;
375
- /**
376
- * Revoke a single session — every token in its family (access + refresh +
377
- * rotations). No-op for stores that can't enumerate. Other sessions keep
378
- * validating.
379
- */
380
- revokeSession(userId: string, sessionId: string): Promise<void>;
381
- /**
382
- * Revoke every session for the user EXCEPT `keepSessionId` ("log out
383
- * everywhere else"). Returns the number of distinct sessions revoked. No-op
384
- * (returns 0) for stores that can't enumerate.
385
- */
386
- revokeOtherSessions(userId: string, keepSessionId: string): Promise<number>;
387
- /**
388
- * Derive a stable 32-byte key from this credential's underlying store secret,
389
- * domain-separated by `label`. Lets adjacent subsystems (e.g. workflow-state
390
- * encryption) reuse the auth secret without managing a second one. Throws if
391
- * the store has no reusable symmetric secret (stateful/asymmetric) — callers
392
- * should then require an explicit secret.
393
- */
394
- deriveStateKey(label?: string): Buffer;
395
- private enforceConcurrencyLimit;
396
- private issueAccessFromRefresh;
397
- /**
398
- * Issue a fresh access + refresh pair off an existing refresh credential.
399
- * `preserveExpiry` keeps the family's original refresh `expiresAt` (a fixed
400
- * session ceiling, used by `always`); otherwise the new refresh slides to
401
- * `now + ttl` (used by `sliding`). `rotateOld` stamps the old refresh as
402
- * rotated (keeping it valid through the grace window) instead of consuming it.
403
- */
404
- private issueRotatedPair;
405
- private lookupConsumedRefresh;
406
- private fireRefreshReuseTheftResponse;
407
- /**
408
- * Best-effort theft response for a detected refresh-token reuse. Fires the
409
- * `onRotationReuse` hook, then revokes per {@link RefreshConfig.reuseResponse}:
410
- * the compromised token family (`'session'`, default) or every session for
411
- * the user (`'user'`). Falls back to user-wide revocation when the session
412
- * can't be targeted (no `sessionId`, or a store that can't enumerate
413
- * sessions). Always throws `REFRESH_REUSE_DETECTED`.
414
- */
415
- private respondToRefreshReuse;
416
- }
417
- //#endregion
418
206
  //#region src/rate-limit/rules.d.ts
419
207
  /**
420
208
  * Rate-limit rule model + the compact string grammar (RL.spec.md §4.1).
@@ -615,10 +403,16 @@ interface SmsSender {
615
403
  type BuildMagicLinkUrl = (kind: AuthEmailKind, token: string, ctx?: {
616
404
  userId?: string;
617
405
  }) => string;
406
+ /** A magic-link token is a plain {@link generateOpaqueToken} mint. */
407
+ declare function generateMagicLinkToken(): string;
408
+ //#endregion
409
+ //#region src/utils/opaque-token.d.ts
618
410
  /**
619
411
  * 32 bytes of CSPRNG entropy (256 bits) encoded as base64url — 43 chars,
620
- * URL-safe. Strong enough to survive short TTLs against online guessing.
412
+ * URL-safe. The single mint for every opaque bearer-style secret in the stack
413
+ * (magic-link tokens, dynamic-client secrets, authz browser bindings):
414
+ * unguessable online, cheap to generate, safe to place in a URL.
621
415
  */
622
- declare function generateMagicLinkToken(): string;
416
+ declare function generateOpaqueToken(): string;
623
417
  //#endregion
624
- 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, DEFAULT_RATE_LIMIT_MESSAGE, type DenylistStore, DenylistStoreMemory, type EmailSender, type EnrichedSession, type IssueOptions, type IssueResult, type JwtAlgorithm, type RateLimitDecision, type RateLimitRule, type RateLimitRuleInput, type RateLimitStore, RateLimitStoreMemory, RateLimiter, type RateLimiterOptions, type RefreshConfig, type SessionEnricher, type SessionInfo, type SmsSender, defaultClock, formatDurationMs, generateMagicLinkToken, parseDurationMs, parseRateLimitRule, renderRateLimitMessage };
418
+ 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, DEFAULT_RATE_LIMIT_MESSAGE, type DenylistStore, DenylistStoreMemory, type EmailSender, type EnrichedSession, type IssueOptions, type IssueResult, type JwtAlgorithm, type RateLimitDecision, type RateLimitRule, type RateLimitRuleInput, type RateLimitStore, RateLimitStoreMemory, RateLimiter, type RateLimiterOptions, type RefreshCallOptions, type RefreshConfig, type RefreshResult, type SessionEnricher, type SessionInfo, type SmsSender, defaultClock, formatDurationMs, generateMagicLinkToken, generateOpaqueToken, parseDurationMs, parseRateLimitRule, renderRateLimitMessage };