@aooth/auth 0.1.46 → 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.
- package/dist/atscript-db.cjs +3 -1
- package/dist/atscript-db.d.cts +4 -3
- package/dist/atscript-db.d.mts +4 -3
- package/dist/atscript-db.mjs +3 -1
- package/dist/auth-credential-C3qebKuy.d.mts +245 -0
- package/dist/auth-credential-CEijxoGU.d.cts +245 -0
- package/dist/authz.cjs +124 -35
- package/dist/authz.d.cts +78 -24
- package/dist/authz.d.mts +78 -24
- package/dist/authz.mjs +121 -37
- package/dist/{store-BG6m6oSJ.d.mts → clock-DJ_eroHW.d.cts} +50 -1
- package/dist/{store-BG6m6oSJ.d.cts → clock-DJ_eroHW.d.mts} +50 -1
- package/dist/{dynamic-client-store-DLRfntHr.mjs → dynamic-client-store-B1JwFHdP.mjs} +1 -0
- package/dist/{dynamic-client-store-DzqdUwnw.d.cts → dynamic-client-store-DJgtMjPw.d.mts} +58 -9
- package/dist/{dynamic-client-store-oHEsQaJg.d.mts → dynamic-client-store-DbiSLOj0.d.cts} +58 -9
- package/dist/{dynamic-client-store-DAStgv2j.cjs → dynamic-client-store-DykM9QOT.cjs} +1 -0
- package/dist/index.cjs +39 -26
- package/dist/index.d.cts +12 -218
- package/dist/index.d.mts +12 -218
- package/dist/index.mjs +39 -27
- package/dist/opaque-token-BaMOrC1A.mjs +13 -0
- package/dist/opaque-token-CaZgD2i6.cjs +18 -0
- package/dist/redis.d.cts +2 -2
- package/dist/redis.d.mts +2 -2
- package/dist/{store-N9daSmHS.d.cts → store-BYnpA7VK.d.cts} +1 -1
- package/dist/{store-jFgby4bK.d.mts → store-pKh0ZaGG.d.mts} +1 -1
- package/package.json +8 -8
- package/src/atscript-db/auth-credential.as +16 -0
- package/src/atscript-db/auth-credential.as.d.ts +4 -1
- package/src/atscript-db/dynamic-client.as +11 -1
- package/src/atscript-db/dynamic-client.as.d.ts +3 -0
- package/dist/clock-BjXa0LXb.d.cts +0 -14
- 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
|
-
|
|
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 (
|
|
622
|
+
if (refreshTtl !== void 0) {
|
|
611
623
|
const refreshState = stateWithPayload(payload, {
|
|
612
624
|
userId,
|
|
613
625
|
issuedAt: now,
|
|
614
|
-
expiresAt: now +
|
|
626
|
+
expiresAt: now + refreshTtl,
|
|
615
627
|
kind: "refresh",
|
|
616
628
|
metadata: effectiveMetadata,
|
|
617
629
|
sessionId
|
|
618
630
|
});
|
|
619
|
-
refreshToken = await this.store.persist(refreshState,
|
|
620
|
-
refreshExpiresAt = now +
|
|
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
|
-
|
|
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 +
|
|
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,
|
|
869
|
-
expiresAt: now +
|
|
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
|
-
|
|
881
|
-
|
|
882
|
-
const refreshExpiresAt = preserveExpiry ? oldRefreshState.expiresAt : now +
|
|
883
|
-
const refreshTtl = preserveExpiry ? Math.max(0, oldRefreshState.expiresAt - now) :
|
|
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
|
|
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
|
|
2
|
-
import { n as
|
|
3
|
-
import { n as RateLimitStoreMemory, t as RateLimitStore } from "./store-
|
|
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.
|
|
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
|
|
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 };
|