@aooth/auth 0.1.77 → 0.1.79
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 +9 -1
- package/dist/atscript-db.d.cts +11 -3
- package/dist/atscript-db.d.mts +11 -3
- package/dist/atscript-db.mjs +9 -1
- package/dist/{auth-credential-C5DiSbb2.d.mts → auth-credential-CLAQgjCS.d.mts} +28 -1
- package/dist/{auth-credential-CU4vsKhu.d.cts → auth-credential-GRAK230V.d.cts} +28 -1
- package/dist/authz.d.cts +2 -2
- package/dist/authz.d.mts +2 -2
- package/dist/{clock-C0O0FCCN.d.cts → clock-CyjqzLxc.d.cts} +3 -2
- package/dist/{clock-C0O0FCCN.d.mts → clock-CyjqzLxc.d.mts} +3 -2
- package/dist/{dynamic-client-store-BCAqzG0Y.d.mts → dynamic-client-store-BF4nXMK2.d.mts} +2 -2
- package/dist/{dynamic-client-store-CafYRhI4.d.cts → dynamic-client-store-HLfcl2FJ.d.cts} +2 -2
- package/dist/index.cjs +170 -25
- package/dist/index.d.cts +23 -7
- package/dist/index.d.mts +23 -7
- package/dist/index.mjs +170 -25
- package/dist/redis.cjs +8 -4
- package/dist/redis.d.cts +3 -3
- package/dist/redis.d.mts +3 -3
- package/dist/redis.mjs +8 -4
- package/dist/{store-3YN2hfss.d.cts → store-CVD1nd2j.d.mts} +7 -4
- package/dist/{store-BUIC0iCK.d.mts → store-ClQnA8yh.d.cts} +7 -4
- package/package.json +10 -10
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { a as AuthContext, d as RefreshConfig, f as RefreshResult, i as DenylistStore, l as EnrichedSession, m as SessionInfo, n as defaultClock, o as CredentialMetadata, p as SessionEnricher, r as CredentialStore, s as CredentialState, t as Clock, u as IssueResult } from "./clock-
|
|
2
|
-
import { i as RefreshCallOptions, n as AuthCredentialOptions, r as IssueOptions, t as AuthCredential } from "./auth-credential-
|
|
3
|
-
import { n as RateLimitStoreMemory, t as RateLimitStore } from "./store-
|
|
1
|
+
import { a as AuthContext, d as RefreshConfig, f as RefreshResult, i as DenylistStore, l as EnrichedSession, m as SessionInfo, n as defaultClock, o as CredentialMetadata, p as SessionEnricher, r as CredentialStore, s as CredentialState, t as Clock, u as IssueResult } from "./clock-CyjqzLxc.cjs";
|
|
2
|
+
import { i as RefreshCallOptions, n as AuthCredentialOptions, r as IssueOptions, t as AuthCredential } from "./auth-credential-GRAK230V.cjs";
|
|
3
|
+
import { n as RateLimitStoreMemory, t as RateLimitStore } from "./store-ClQnA8yh.cjs";
|
|
4
4
|
import { CryptoKey } from "jose";
|
|
5
5
|
//#region src/errors.d.ts
|
|
6
6
|
type AuthErrorType = "INVALID_TOKEN" | "TOKEN_EXPIRED" | "TOKEN_REVOKED" | "REFRESH_REUSE_DETECTED" | "STATELESS_OPERATION_UNSUPPORTED" | "MAX_CONCURRENT_REACHED" | "INVALID_CONFIG";
|
|
@@ -32,7 +32,7 @@ declare class CredentialStoreMemory<TPayload extends object = object> implements
|
|
|
32
32
|
consume(token: string): Promise<(CredentialState & TPayload) | null>;
|
|
33
33
|
update(token: string, state: CredentialState & TPayload): Promise<string>;
|
|
34
34
|
revoke(token: string): Promise<void>;
|
|
35
|
-
touch(token: string, at: number): Promise<void>;
|
|
35
|
+
touch(token: string, at: number, _state?: CredentialState & TPayload): Promise<void>;
|
|
36
36
|
revokeAllForUser(userId: string): Promise<number>;
|
|
37
37
|
listForUser(userId: string): Promise<Array<CredentialState & TPayload & {
|
|
38
38
|
token: string;
|
|
@@ -46,19 +46,23 @@ declare class CredentialStoreMemory<TPayload extends object = object> implements
|
|
|
46
46
|
* In-memory denylist for stateless revocation.
|
|
47
47
|
*
|
|
48
48
|
* Stores a map from JTI (or any opaque identifier) to absolute expiry timestamp.
|
|
49
|
-
* `has` returns false for entries whose TTL has elapsed and lazily removes them
|
|
50
|
-
* `
|
|
51
|
-
*
|
|
49
|
+
* `has` returns false for entries whose TTL has elapsed and lazily removes them;
|
|
50
|
+
* `add` sweeps expired entries as the map grows (amortized, each time it
|
|
51
|
+
* doubles). `cleanup` performs a sweep on demand and returns the number of
|
|
52
|
+
* entries removed.
|
|
52
53
|
*/
|
|
53
54
|
declare class DenylistStoreMemory implements DenylistStore {
|
|
54
55
|
private readonly entries;
|
|
55
56
|
private readonly clock;
|
|
57
|
+
/** Map size at which `add` next sweeps expired entries (amortized; doubles). */
|
|
58
|
+
private sweepAt;
|
|
56
59
|
constructor(opts?: {
|
|
57
60
|
clock?: Clock;
|
|
58
61
|
});
|
|
59
62
|
add(jti: string, expiresAt: number): Promise<void>;
|
|
60
63
|
has(jti: string): Promise<boolean>;
|
|
61
64
|
cleanup(): Promise<number>;
|
|
65
|
+
private sweep;
|
|
62
66
|
}
|
|
63
67
|
//#endregion
|
|
64
68
|
//#region src/stores/jwt.d.ts
|
|
@@ -98,6 +102,12 @@ declare class CredentialStoreJwt<TPayload extends object = object> implements Cr
|
|
|
98
102
|
private readonly algorithm;
|
|
99
103
|
private readonly signingKey;
|
|
100
104
|
private readonly verifyingKey;
|
|
105
|
+
/**
|
|
106
|
+
* HS* with raw secret bytes: the HMAC `CryptoKey` imported once (lazily) and
|
|
107
|
+
* reused for every sign / verify — jose would otherwise re-import the raw
|
|
108
|
+
* bytes per call. `signingKey` keeps the bytes for `deriveSubkey`.
|
|
109
|
+
*/
|
|
110
|
+
private hmacKey?;
|
|
101
111
|
private readonly issuer?;
|
|
102
112
|
private readonly audience?;
|
|
103
113
|
private readonly denylist?;
|
|
@@ -131,6 +141,12 @@ declare class CredentialStoreJwt<TPayload extends object = object> implements Cr
|
|
|
131
141
|
*/
|
|
132
142
|
private passesEpoch;
|
|
133
143
|
private verify;
|
|
144
|
+
/**
|
|
145
|
+
* The key jose gets: raw HS* secret bytes as their HMAC `CryptoKey`
|
|
146
|
+
* (imported once — usable for both sign and verify, its hash pinned to the
|
|
147
|
+
* configured algorithm), anything else as given.
|
|
148
|
+
*/
|
|
149
|
+
private keyFor;
|
|
134
150
|
private payloadToState;
|
|
135
151
|
}
|
|
136
152
|
//#endregion
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { a as AuthContext, d as RefreshConfig, f as RefreshResult, i as DenylistStore, l as EnrichedSession, m as SessionInfo, n as defaultClock, o as CredentialMetadata, p as SessionEnricher, r as CredentialStore, s as CredentialState, t as Clock, u as IssueResult } from "./clock-
|
|
2
|
-
import { i as RefreshCallOptions, n as AuthCredentialOptions, r as IssueOptions, t as AuthCredential } from "./auth-credential-
|
|
3
|
-
import { n as RateLimitStoreMemory, t as RateLimitStore } from "./store-
|
|
1
|
+
import { a as AuthContext, d as RefreshConfig, f as RefreshResult, i as DenylistStore, l as EnrichedSession, m as SessionInfo, n as defaultClock, o as CredentialMetadata, p as SessionEnricher, r as CredentialStore, s as CredentialState, t as Clock, u as IssueResult } from "./clock-CyjqzLxc.mjs";
|
|
2
|
+
import { i as RefreshCallOptions, n as AuthCredentialOptions, r as IssueOptions, t as AuthCredential } from "./auth-credential-CLAQgjCS.mjs";
|
|
3
|
+
import { n as RateLimitStoreMemory, t as RateLimitStore } from "./store-CVD1nd2j.mjs";
|
|
4
4
|
import { CryptoKey } from "jose";
|
|
5
5
|
//#region src/errors.d.ts
|
|
6
6
|
type AuthErrorType = "INVALID_TOKEN" | "TOKEN_EXPIRED" | "TOKEN_REVOKED" | "REFRESH_REUSE_DETECTED" | "STATELESS_OPERATION_UNSUPPORTED" | "MAX_CONCURRENT_REACHED" | "INVALID_CONFIG";
|
|
@@ -32,7 +32,7 @@ declare class CredentialStoreMemory<TPayload extends object = object> implements
|
|
|
32
32
|
consume(token: string): Promise<(CredentialState & TPayload) | null>;
|
|
33
33
|
update(token: string, state: CredentialState & TPayload): Promise<string>;
|
|
34
34
|
revoke(token: string): Promise<void>;
|
|
35
|
-
touch(token: string, at: number): Promise<void>;
|
|
35
|
+
touch(token: string, at: number, _state?: CredentialState & TPayload): Promise<void>;
|
|
36
36
|
revokeAllForUser(userId: string): Promise<number>;
|
|
37
37
|
listForUser(userId: string): Promise<Array<CredentialState & TPayload & {
|
|
38
38
|
token: string;
|
|
@@ -46,19 +46,23 @@ declare class CredentialStoreMemory<TPayload extends object = object> implements
|
|
|
46
46
|
* In-memory denylist for stateless revocation.
|
|
47
47
|
*
|
|
48
48
|
* Stores a map from JTI (or any opaque identifier) to absolute expiry timestamp.
|
|
49
|
-
* `has` returns false for entries whose TTL has elapsed and lazily removes them
|
|
50
|
-
* `
|
|
51
|
-
*
|
|
49
|
+
* `has` returns false for entries whose TTL has elapsed and lazily removes them;
|
|
50
|
+
* `add` sweeps expired entries as the map grows (amortized, each time it
|
|
51
|
+
* doubles). `cleanup` performs a sweep on demand and returns the number of
|
|
52
|
+
* entries removed.
|
|
52
53
|
*/
|
|
53
54
|
declare class DenylistStoreMemory implements DenylistStore {
|
|
54
55
|
private readonly entries;
|
|
55
56
|
private readonly clock;
|
|
57
|
+
/** Map size at which `add` next sweeps expired entries (amortized; doubles). */
|
|
58
|
+
private sweepAt;
|
|
56
59
|
constructor(opts?: {
|
|
57
60
|
clock?: Clock;
|
|
58
61
|
});
|
|
59
62
|
add(jti: string, expiresAt: number): Promise<void>;
|
|
60
63
|
has(jti: string): Promise<boolean>;
|
|
61
64
|
cleanup(): Promise<number>;
|
|
65
|
+
private sweep;
|
|
62
66
|
}
|
|
63
67
|
//#endregion
|
|
64
68
|
//#region src/stores/jwt.d.ts
|
|
@@ -98,6 +102,12 @@ declare class CredentialStoreJwt<TPayload extends object = object> implements Cr
|
|
|
98
102
|
private readonly algorithm;
|
|
99
103
|
private readonly signingKey;
|
|
100
104
|
private readonly verifyingKey;
|
|
105
|
+
/**
|
|
106
|
+
* HS* with raw secret bytes: the HMAC `CryptoKey` imported once (lazily) and
|
|
107
|
+
* reused for every sign / verify — jose would otherwise re-import the raw
|
|
108
|
+
* bytes per call. `signingKey` keeps the bytes for `deriveSubkey`.
|
|
109
|
+
*/
|
|
110
|
+
private hmacKey?;
|
|
101
111
|
private readonly issuer?;
|
|
102
112
|
private readonly audience?;
|
|
103
113
|
private readonly denylist?;
|
|
@@ -131,6 +141,12 @@ declare class CredentialStoreJwt<TPayload extends object = object> implements Cr
|
|
|
131
141
|
*/
|
|
132
142
|
private passesEpoch;
|
|
133
143
|
private verify;
|
|
144
|
+
/**
|
|
145
|
+
* The key jose gets: raw HS* secret bytes as their HMAC `CryptoKey`
|
|
146
|
+
* (imported once — usable for both sign and verify, its hash pinned to the
|
|
147
|
+
* configured algorithm), anything else as given.
|
|
148
|
+
*/
|
|
149
|
+
private keyFor;
|
|
134
150
|
private payloadToState;
|
|
135
151
|
}
|
|
136
152
|
//#endregion
|
package/dist/index.mjs
CHANGED
|
@@ -80,7 +80,7 @@ var CredentialStoreMemory = class {
|
|
|
80
80
|
this.states.delete(token);
|
|
81
81
|
this.indexRemove(state.userId, token);
|
|
82
82
|
}
|
|
83
|
-
async touch(token, at) {
|
|
83
|
+
async touch(token, at, _state) {
|
|
84
84
|
const state = this.states.get(token);
|
|
85
85
|
if (!state) return;
|
|
86
86
|
state.lastSeenAt = at;
|
|
@@ -142,17 +142,24 @@ var CredentialStoreMemory = class {
|
|
|
142
142
|
* In-memory denylist for stateless revocation.
|
|
143
143
|
*
|
|
144
144
|
* Stores a map from JTI (or any opaque identifier) to absolute expiry timestamp.
|
|
145
|
-
* `has` returns false for entries whose TTL has elapsed and lazily removes them
|
|
146
|
-
* `
|
|
147
|
-
*
|
|
145
|
+
* `has` returns false for entries whose TTL has elapsed and lazily removes them;
|
|
146
|
+
* `add` sweeps expired entries as the map grows (amortized, each time it
|
|
147
|
+
* doubles). `cleanup` performs a sweep on demand and returns the number of
|
|
148
|
+
* entries removed.
|
|
148
149
|
*/
|
|
149
150
|
var DenylistStoreMemory = class {
|
|
150
151
|
entries = /* @__PURE__ */ new Map();
|
|
151
152
|
clock;
|
|
153
|
+
/** Map size at which `add` next sweeps expired entries (amortized; doubles). */
|
|
154
|
+
sweepAt = MIN_SWEEP_SIZE$2;
|
|
152
155
|
constructor(opts) {
|
|
153
156
|
this.clock = opts?.clock ?? defaultClock;
|
|
154
157
|
}
|
|
155
158
|
async add(jti, expiresAt) {
|
|
159
|
+
if (this.entries.size >= this.sweepAt && !this.entries.has(jti)) {
|
|
160
|
+
this.sweep(this.clock.now());
|
|
161
|
+
this.sweepAt = Math.max(MIN_SWEEP_SIZE$2, this.entries.size * 2);
|
|
162
|
+
}
|
|
156
163
|
this.entries.set(jti, expiresAt);
|
|
157
164
|
}
|
|
158
165
|
async has(jti) {
|
|
@@ -165,7 +172,9 @@ var DenylistStoreMemory = class {
|
|
|
165
172
|
return true;
|
|
166
173
|
}
|
|
167
174
|
async cleanup() {
|
|
168
|
-
|
|
175
|
+
return this.sweep(this.clock.now());
|
|
176
|
+
}
|
|
177
|
+
sweep(now) {
|
|
169
178
|
let removed = 0;
|
|
170
179
|
for (const [jti, expiresAt] of this.entries) if (expiresAt <= now) {
|
|
171
180
|
this.entries.delete(jti);
|
|
@@ -174,6 +183,8 @@ var DenylistStoreMemory = class {
|
|
|
174
183
|
return removed;
|
|
175
184
|
}
|
|
176
185
|
};
|
|
186
|
+
/** Smallest map size an amortized expiry sweep runs at. */
|
|
187
|
+
const MIN_SWEEP_SIZE$2 = 1024;
|
|
177
188
|
//#endregion
|
|
178
189
|
//#region src/stores/derive-subkey.ts
|
|
179
190
|
const SUBKEY_SALT = Buffer.from("aooth/credential-store/subkey-v1");
|
|
@@ -200,6 +211,12 @@ var CredentialStoreJwt = class {
|
|
|
200
211
|
algorithm;
|
|
201
212
|
signingKey;
|
|
202
213
|
verifyingKey;
|
|
214
|
+
/**
|
|
215
|
+
* HS* with raw secret bytes: the HMAC `CryptoKey` imported once (lazily) and
|
|
216
|
+
* reused for every sign / verify — jose would otherwise re-import the raw
|
|
217
|
+
* bytes per call. `signingKey` keeps the bytes for `deriveSubkey`.
|
|
218
|
+
*/
|
|
219
|
+
hmacKey;
|
|
203
220
|
issuer;
|
|
204
221
|
audience;
|
|
205
222
|
denylist;
|
|
@@ -248,7 +265,7 @@ var CredentialStoreJwt = class {
|
|
|
248
265
|
const builder = new SignJWT({ state: stateClaim }).setProtectedHeader({ alg: this.algorithm }).setSubject(state.userId).setIssuedAt(Math.floor(state.issuedAt / 1e3)).setExpirationTime(Math.floor(expiresAtMs / 1e3)).setJti(jti);
|
|
249
266
|
if (this.issuer) builder.setIssuer(this.issuer);
|
|
250
267
|
if (this.audience) builder.setAudience(this.audience);
|
|
251
|
-
return builder.sign(this.signingKey);
|
|
268
|
+
return builder.sign(await this.keyFor(this.signingKey));
|
|
252
269
|
}
|
|
253
270
|
async retrieve(token) {
|
|
254
271
|
const verified = await this.verify(token);
|
|
@@ -314,7 +331,7 @@ var CredentialStoreJwt = class {
|
|
|
314
331
|
}
|
|
315
332
|
async verify(token) {
|
|
316
333
|
try {
|
|
317
|
-
return { payload: (await jwtVerify(token, this.verifyingKey, {
|
|
334
|
+
return { payload: (await jwtVerify(token, await this.keyFor(this.verifyingKey), {
|
|
318
335
|
algorithms: [this.algorithm],
|
|
319
336
|
issuer: this.issuer,
|
|
320
337
|
audience: this.audience,
|
|
@@ -324,6 +341,19 @@ var CredentialStoreJwt = class {
|
|
|
324
341
|
return null;
|
|
325
342
|
}
|
|
326
343
|
}
|
|
344
|
+
/**
|
|
345
|
+
* The key jose gets: raw HS* secret bytes as their HMAC `CryptoKey`
|
|
346
|
+
* (imported once — usable for both sign and verify, its hash pinned to the
|
|
347
|
+
* configured algorithm), anything else as given.
|
|
348
|
+
*/
|
|
349
|
+
keyFor(key) {
|
|
350
|
+
if (!(key instanceof Uint8Array) || !HS_ALGS.has(this.algorithm)) return key;
|
|
351
|
+
this.hmacKey ??= crypto.subtle.importKey("raw", key, {
|
|
352
|
+
name: "HMAC",
|
|
353
|
+
hash: `SHA-${this.algorithm.slice(2)}`
|
|
354
|
+
}, false, ["sign", "verify"]);
|
|
355
|
+
return this.hmacKey;
|
|
356
|
+
}
|
|
327
357
|
payloadToState(payload) {
|
|
328
358
|
if (typeof payload.sub !== "string") return null;
|
|
329
359
|
if (typeof payload.iat !== "number" || typeof payload.exp !== "number") return null;
|
|
@@ -510,6 +540,63 @@ function fingerprint(token) {
|
|
|
510
540
|
return createHash("sha256").update(token).digest("hex");
|
|
511
541
|
}
|
|
512
542
|
/**
|
|
543
|
+
* Turn `ctx.credentialId` (already in place, so key order is kept) into a
|
|
544
|
+
* lazily computed, memoized, still writable property.
|
|
545
|
+
*/
|
|
546
|
+
function lazyCredentialId(ctx, token) {
|
|
547
|
+
let value;
|
|
548
|
+
Object.defineProperty(ctx, "credentialId", {
|
|
549
|
+
enumerable: true,
|
|
550
|
+
configurable: true,
|
|
551
|
+
get: () => value ??= fingerprint(token),
|
|
552
|
+
set: (next) => {
|
|
553
|
+
value = next;
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
/** Smallest map size an amortized expiry sweep runs at. */
|
|
558
|
+
const MIN_SWEEP_SIZE$1 = 1024;
|
|
559
|
+
/**
|
|
560
|
+
* The opt-in `validateCache`: token → validated state for `ttlMs`, bounded
|
|
561
|
+
* (oldest dropped first). `epoch` moves on every eviction so a validation
|
|
562
|
+
* racing a revocation never re-caches the revoked token.
|
|
563
|
+
*/
|
|
564
|
+
var ValidateCache = class {
|
|
565
|
+
ttlMs;
|
|
566
|
+
max;
|
|
567
|
+
entries = /* @__PURE__ */ new Map();
|
|
568
|
+
epoch = 0;
|
|
569
|
+
constructor(ttlMs, max) {
|
|
570
|
+
this.ttlMs = ttlMs;
|
|
571
|
+
this.max = max;
|
|
572
|
+
}
|
|
573
|
+
get(token, now) {
|
|
574
|
+
const entry = this.entries.get(token);
|
|
575
|
+
if (!entry) return void 0;
|
|
576
|
+
if (entry.until > now) return entry.state;
|
|
577
|
+
this.entries.delete(token);
|
|
578
|
+
}
|
|
579
|
+
set(token, state, now) {
|
|
580
|
+
this.entries.delete(token);
|
|
581
|
+
if (this.entries.size >= this.max) {
|
|
582
|
+
const oldest = this.entries.keys().next();
|
|
583
|
+
if (!oldest.done) this.entries.delete(oldest.value);
|
|
584
|
+
}
|
|
585
|
+
this.entries.set(token, {
|
|
586
|
+
state,
|
|
587
|
+
until: now + this.ttlMs
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
delete(token) {
|
|
591
|
+
this.epoch++;
|
|
592
|
+
this.entries.delete(token);
|
|
593
|
+
}
|
|
594
|
+
deleteUser(userId) {
|
|
595
|
+
this.epoch++;
|
|
596
|
+
for (const [token, entry] of this.entries) if (entry.state.userId === userId) this.entries.delete(token);
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
/**
|
|
513
600
|
* Merge a credential's typed payload under the fixed {@link CredentialState}
|
|
514
601
|
* envelope, asserting the `CredentialState & TPayload` shape in ONE place. The
|
|
515
602
|
* payload is spread first so an envelope field always wins a name clash. Every
|
|
@@ -570,6 +657,10 @@ var AuthCredential = class {
|
|
|
570
657
|
* access; bounded by refresh TTL.
|
|
571
658
|
*/
|
|
572
659
|
consumedRefreshes = /* @__PURE__ */ new Map();
|
|
660
|
+
/** Map size at which `consumedRefreshes` next sweeps its expired entries. */
|
|
661
|
+
consumedSweepAt = MIN_SWEEP_SIZE$1;
|
|
662
|
+
/** Opt-in validated-state cache (`validateCache`). */
|
|
663
|
+
validated;
|
|
573
664
|
constructor(opts) {
|
|
574
665
|
this.store = opts.store;
|
|
575
666
|
this.method = opts.method ?? "token";
|
|
@@ -580,6 +671,10 @@ var AuthCredential = class {
|
|
|
580
671
|
this.onLimit = opts.onLimit ?? "reject";
|
|
581
672
|
this.trackLastSeen = opts.trackLastSeen ?? false;
|
|
582
673
|
this.clock = opts.clock ?? defaultClock;
|
|
674
|
+
if (opts.validateCache) {
|
|
675
|
+
if (!(opts.validateCache.ttlMs > 0)) throw new AuthError("INVALID_CONFIG", `validateCache.ttlMs must be > 0 (got ${opts.validateCache.ttlMs})`);
|
|
676
|
+
this.validated = new ValidateCache(opts.validateCache.ttlMs, opts.validateCache.max ?? 1e4);
|
|
677
|
+
}
|
|
583
678
|
if (this.accessTtl <= 0) throw new AuthError("INVALID_CONFIG", `accessTtl must be > 0 (got ${this.accessTtl})`);
|
|
584
679
|
if (this.refreshConfig && this.refreshConfig.ttl <= 0) throw new AuthError("INVALID_CONFIG", `refresh.ttl must be > 0 (got ${this.refreshConfig.ttl})`);
|
|
585
680
|
const graceMs = this.refreshConfig?.rotationGraceMs;
|
|
@@ -642,20 +737,42 @@ var AuthCredential = class {
|
|
|
642
737
|
};
|
|
643
738
|
}
|
|
644
739
|
async validate(accessToken) {
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
if (!state)
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
740
|
+
let state = this.validated?.get(accessToken, this.clock.now());
|
|
741
|
+
let fresh = false;
|
|
742
|
+
if (!state) {
|
|
743
|
+
fresh = true;
|
|
744
|
+
const epoch = this.validated?.epoch;
|
|
745
|
+
state = await this.retrieveUnlessDenied(accessToken);
|
|
746
|
+
if (!state) return null;
|
|
747
|
+
if (state.expiresAt <= this.clock.now()) return null;
|
|
748
|
+
if (state.kind === "refresh") return null;
|
|
749
|
+
if (this.validated && this.validated.epoch === epoch) this.validated.set(accessToken, state, this.clock.now());
|
|
750
|
+
} else if (state.expiresAt <= this.clock.now()) {
|
|
751
|
+
this.validated.delete(accessToken);
|
|
752
|
+
return null;
|
|
753
|
+
}
|
|
754
|
+
if (this.trackLastSeen === "validate" && this.store.touch) await this.store.touch(accessToken, this.clock.now(), fresh ? state : void 0);
|
|
755
|
+
const ctx = {
|
|
652
756
|
...credentialPayloadOf(state),
|
|
653
757
|
userId: state.userId,
|
|
654
758
|
method: this.method,
|
|
655
|
-
credentialId:
|
|
759
|
+
credentialId: "",
|
|
656
760
|
sessionId: this.sessionIdOf(state.sessionId, accessToken),
|
|
657
761
|
expiresAt: state.expiresAt
|
|
658
762
|
};
|
|
763
|
+
lazyCredentialId(ctx, accessToken);
|
|
764
|
+
return ctx;
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* The stored state of `accessToken` unless the orchestrator's denylist has
|
|
768
|
+
* it — both looked up concurrently (one round trip on remote stores).
|
|
769
|
+
*/
|
|
770
|
+
async retrieveUnlessDenied(accessToken) {
|
|
771
|
+
const retrieving = this.store.retrieve(accessToken);
|
|
772
|
+
if (!this.denylist) return await retrieving;
|
|
773
|
+
retrieving.catch(() => {});
|
|
774
|
+
if (await this.denylist.has(accessToken)) return null;
|
|
775
|
+
return await retrieving;
|
|
659
776
|
}
|
|
660
777
|
async refresh(refreshToken, opts) {
|
|
661
778
|
const now = this.clock.now();
|
|
@@ -706,6 +823,7 @@ var AuthCredential = class {
|
|
|
706
823
|
async refreshAlways(oldState, refreshToken, now) {
|
|
707
824
|
if (!this.store.listForUser) {
|
|
708
825
|
await this.store.consume(refreshToken);
|
|
826
|
+
this.sweepConsumedRefreshes();
|
|
709
827
|
this.consumedRefreshes.set(refreshToken, {
|
|
710
828
|
userId: oldState.userId,
|
|
711
829
|
iat: oldState.issuedAt,
|
|
@@ -762,10 +880,20 @@ var AuthCredential = class {
|
|
|
762
880
|
};
|
|
763
881
|
}
|
|
764
882
|
async revoke(token) {
|
|
765
|
-
|
|
883
|
+
this.validated?.delete(token);
|
|
884
|
+
try {
|
|
885
|
+
await this.store.revoke(token);
|
|
886
|
+
} finally {
|
|
887
|
+
this.validated?.delete(token);
|
|
888
|
+
}
|
|
766
889
|
}
|
|
767
890
|
async revokeAllForUser(userId) {
|
|
768
|
-
|
|
891
|
+
this.validated?.deleteUser(userId);
|
|
892
|
+
try {
|
|
893
|
+
return await this.store.revokeAllForUser(userId);
|
|
894
|
+
} finally {
|
|
895
|
+
this.validated?.deleteUser(userId);
|
|
896
|
+
}
|
|
769
897
|
}
|
|
770
898
|
async listForUser(userId) {
|
|
771
899
|
if (!this.store.listForUser) return [];
|
|
@@ -849,7 +977,7 @@ var AuthCredential = class {
|
|
|
849
977
|
async revokeSession(userId, sessionId) {
|
|
850
978
|
if (!this.store.listForUser) return;
|
|
851
979
|
const all = await this.store.listForUser(userId);
|
|
852
|
-
await Promise.all(all.filter((entry) => this.sessionKeyOf(entry) === sessionId).map((entry) => this.
|
|
980
|
+
await Promise.all(all.filter((entry) => this.sessionKeyOf(entry) === sessionId).map((entry) => this.revoke(entry.token)));
|
|
853
981
|
}
|
|
854
982
|
/**
|
|
855
983
|
* Revoke every session for the user EXCEPT `keepSessionId` ("log out
|
|
@@ -865,7 +993,7 @@ var AuthCredential = class {
|
|
|
865
993
|
const key = this.sessionKeyOf(entry);
|
|
866
994
|
if (key === keepSessionId) continue;
|
|
867
995
|
revokedSessions.add(key);
|
|
868
|
-
revokes.push(this.
|
|
996
|
+
revokes.push(this.revoke(entry.token));
|
|
869
997
|
}
|
|
870
998
|
await Promise.all(revokes);
|
|
871
999
|
return revokedSessions.size;
|
|
@@ -892,7 +1020,7 @@ var AuthCredential = class {
|
|
|
892
1020
|
});
|
|
893
1021
|
const sorted = accessOnly.toSorted((a, b) => a.issuedAt - b.issuedAt);
|
|
894
1022
|
const toEvict = accessOnly.length - this.maxConcurrent + 1;
|
|
895
|
-
for (let i = 0; i < toEvict && i < sorted.length; i++) await this.
|
|
1023
|
+
for (let i = 0; i < toEvict && i < sorted.length; i++) await this.revoke(sorted[i].token);
|
|
896
1024
|
}
|
|
897
1025
|
async issueAccessFromRefresh(refreshState, now) {
|
|
898
1026
|
const accessTtl = refreshState.metadata?.accessTtl ?? this.accessTtl;
|
|
@@ -955,6 +1083,13 @@ var AuthCredential = class {
|
|
|
955
1083
|
userId: oldRefreshState.userId
|
|
956
1084
|
};
|
|
957
1085
|
}
|
|
1086
|
+
/** Amortized sweep of expired `consumedRefreshes` entries (each size doubling). */
|
|
1087
|
+
sweepConsumedRefreshes() {
|
|
1088
|
+
if (this.consumedRefreshes.size < this.consumedSweepAt) return;
|
|
1089
|
+
const now = this.clock.now();
|
|
1090
|
+
for (const [token, entry] of this.consumedRefreshes) if (entry.exp <= now) this.consumedRefreshes.delete(token);
|
|
1091
|
+
this.consumedSweepAt = Math.max(MIN_SWEEP_SIZE$1, this.consumedRefreshes.size * 2);
|
|
1092
|
+
}
|
|
958
1093
|
lookupConsumedRefresh(token) {
|
|
959
1094
|
const entry = this.consumedRefreshes.get(token);
|
|
960
1095
|
if (!entry) return null;
|
|
@@ -991,7 +1126,7 @@ var AuthCredential = class {
|
|
|
991
1126
|
...reuse.rotatedAt !== void 0 && { rotatedAt: reuse.rotatedAt }
|
|
992
1127
|
});
|
|
993
1128
|
if ((this.refreshConfig?.reuseResponse ?? "session") === "session" && reuse.sessionId !== void 0 && this.store.listForUser) await this.revokeSession(reuse.userId, reuse.sessionId);
|
|
994
|
-
else await this.
|
|
1129
|
+
else await this.revokeAllForUser(reuse.userId);
|
|
995
1130
|
throw new AuthError("REFRESH_REUSE_DETECTED", void 0, {
|
|
996
1131
|
userId: reuse.userId,
|
|
997
1132
|
...reuse.sessionId !== void 0 && { sessionId: reuse.sessionId },
|
|
@@ -1081,13 +1216,15 @@ function renderRateLimitMessage(template, rule, retryAfterMs) {
|
|
|
1081
1216
|
//#region src/rate-limit/store.ts
|
|
1082
1217
|
/**
|
|
1083
1218
|
* In-memory `RateLimitStore` — the zero-config default (single process only).
|
|
1084
|
-
* Lazy-evicts expired counters on access
|
|
1085
|
-
*
|
|
1086
|
-
* `DenylistStoreMemory`).
|
|
1219
|
+
* Lazy-evicts expired counters on access, and sweeps expired counters itself
|
|
1220
|
+
* as the map grows (amortized, each time it doubles); `cleanup()` sweeps the
|
|
1221
|
+
* whole map on demand (same pattern as `DenylistStoreMemory`).
|
|
1087
1222
|
*/
|
|
1088
1223
|
var RateLimitStoreMemory = class {
|
|
1089
1224
|
entries = /* @__PURE__ */ new Map();
|
|
1090
1225
|
clock;
|
|
1226
|
+
/** Map size at which `hit` next sweeps expired counters (amortized; doubles). */
|
|
1227
|
+
sweepAt = MIN_SWEEP_SIZE;
|
|
1091
1228
|
constructor(opts) {
|
|
1092
1229
|
this.clock = opts?.clock ?? defaultClock;
|
|
1093
1230
|
}
|
|
@@ -1095,6 +1232,10 @@ var RateLimitStoreMemory = class {
|
|
|
1095
1232
|
const now = this.clock.now();
|
|
1096
1233
|
const entry = this.entries.get(key);
|
|
1097
1234
|
if (!entry || entry.expiresAt <= now) {
|
|
1235
|
+
if (!entry && this.entries.size >= this.sweepAt) {
|
|
1236
|
+
this.sweep(now);
|
|
1237
|
+
this.sweepAt = Math.max(MIN_SWEEP_SIZE, this.entries.size * 2);
|
|
1238
|
+
}
|
|
1098
1239
|
this.entries.set(key, {
|
|
1099
1240
|
count: 1,
|
|
1100
1241
|
expiresAt: now + ttlMs
|
|
@@ -1112,7 +1253,9 @@ var RateLimitStoreMemory = class {
|
|
|
1112
1253
|
this.entries.clear();
|
|
1113
1254
|
}
|
|
1114
1255
|
async cleanup() {
|
|
1115
|
-
|
|
1256
|
+
return this.sweep(this.clock.now());
|
|
1257
|
+
}
|
|
1258
|
+
sweep(now) {
|
|
1116
1259
|
let removed = 0;
|
|
1117
1260
|
for (const [key, entry] of this.entries) if (entry.expiresAt <= now) {
|
|
1118
1261
|
this.entries.delete(key);
|
|
@@ -1121,6 +1264,8 @@ var RateLimitStoreMemory = class {
|
|
|
1121
1264
|
return removed;
|
|
1122
1265
|
}
|
|
1123
1266
|
};
|
|
1267
|
+
/** Smallest map size an amortized expiry sweep runs at. */
|
|
1268
|
+
const MIN_SWEEP_SIZE = 1024;
|
|
1124
1269
|
//#endregion
|
|
1125
1270
|
//#region src/rate-limit/rate-limiter.ts
|
|
1126
1271
|
/**
|
package/dist/redis.cjs
CHANGED
|
@@ -74,10 +74,14 @@ var CredentialStoreRedis = class {
|
|
|
74
74
|
await this.redis.del(this.tokenKey(token));
|
|
75
75
|
await this.redis.srem(this.userKey(state.userId), token);
|
|
76
76
|
}
|
|
77
|
-
async touch(token, at) {
|
|
78
|
-
|
|
79
|
-
if (
|
|
80
|
-
|
|
77
|
+
async touch(token, at, current) {
|
|
78
|
+
let state;
|
|
79
|
+
if (current) state = { ...current };
|
|
80
|
+
else {
|
|
81
|
+
const raw = await this.redis.get(this.tokenKey(token));
|
|
82
|
+
if (raw === null) return;
|
|
83
|
+
state = JSON.parse(raw);
|
|
84
|
+
}
|
|
81
85
|
state.lastSeenAt = at;
|
|
82
86
|
const ttlMs = Math.max(0, state.expiresAt - Date.now());
|
|
83
87
|
if (ttlMs <= 0) return;
|
package/dist/redis.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { i as DenylistStore, r as CredentialStore, s as CredentialState } from "./clock-
|
|
2
|
-
import { t as RateLimitStore } from "./store-
|
|
1
|
+
import { i as DenylistStore, r as CredentialStore, s as CredentialState } from "./clock-CyjqzLxc.cjs";
|
|
2
|
+
import { t as RateLimitStore } from "./store-ClQnA8yh.cjs";
|
|
3
3
|
//#region src/redis/index.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* Structural Redis client. Covers the exact set of commands used by
|
|
@@ -101,7 +101,7 @@ declare class CredentialStoreRedis<TPayload extends object = object> implements
|
|
|
101
101
|
consume(token: string): Promise<(CredentialState & TPayload) | null>;
|
|
102
102
|
update(token: string, state: CredentialState & TPayload): Promise<string>;
|
|
103
103
|
revoke(token: string): Promise<void>;
|
|
104
|
-
touch(token: string, at: number): Promise<void>;
|
|
104
|
+
touch(token: string, at: number, current?: CredentialState & TPayload): Promise<void>;
|
|
105
105
|
revokeAllForUser(userId: string): Promise<number>;
|
|
106
106
|
listForUser(userId: string): Promise<Array<CredentialState & TPayload & {
|
|
107
107
|
token: string;
|
package/dist/redis.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { i as DenylistStore, r as CredentialStore, s as CredentialState } from "./clock-
|
|
2
|
-
import { t as RateLimitStore } from "./store-
|
|
1
|
+
import { i as DenylistStore, r as CredentialStore, s as CredentialState } from "./clock-CyjqzLxc.mjs";
|
|
2
|
+
import { t as RateLimitStore } from "./store-CVD1nd2j.mjs";
|
|
3
3
|
//#region src/redis/index.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* Structural Redis client. Covers the exact set of commands used by
|
|
@@ -101,7 +101,7 @@ declare class CredentialStoreRedis<TPayload extends object = object> implements
|
|
|
101
101
|
consume(token: string): Promise<(CredentialState & TPayload) | null>;
|
|
102
102
|
update(token: string, state: CredentialState & TPayload): Promise<string>;
|
|
103
103
|
revoke(token: string): Promise<void>;
|
|
104
|
-
touch(token: string, at: number): Promise<void>;
|
|
104
|
+
touch(token: string, at: number, current?: CredentialState & TPayload): Promise<void>;
|
|
105
105
|
revokeAllForUser(userId: string): Promise<number>;
|
|
106
106
|
listForUser(userId: string): Promise<Array<CredentialState & TPayload & {
|
|
107
107
|
token: string;
|
package/dist/redis.mjs
CHANGED
|
@@ -73,10 +73,14 @@ var CredentialStoreRedis = class {
|
|
|
73
73
|
await this.redis.del(this.tokenKey(token));
|
|
74
74
|
await this.redis.srem(this.userKey(state.userId), token);
|
|
75
75
|
}
|
|
76
|
-
async touch(token, at) {
|
|
77
|
-
|
|
78
|
-
if (
|
|
79
|
-
|
|
76
|
+
async touch(token, at, current) {
|
|
77
|
+
let state;
|
|
78
|
+
if (current) state = { ...current };
|
|
79
|
+
else {
|
|
80
|
+
const raw = await this.redis.get(this.tokenKey(token));
|
|
81
|
+
if (raw === null) return;
|
|
82
|
+
state = JSON.parse(raw);
|
|
83
|
+
}
|
|
80
84
|
state.lastSeenAt = at;
|
|
81
85
|
const ttlMs = Math.max(0, state.expiresAt - Date.now());
|
|
82
86
|
if (ttlMs <= 0) return;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as Clock } from "./clock-
|
|
1
|
+
import { t as Clock } from "./clock-CyjqzLxc.mjs";
|
|
2
2
|
//#region src/rate-limit/store.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* Storage contract for the fixed-window limiter (RL.spec.md §4.3).
|
|
@@ -18,13 +18,15 @@ interface RateLimitStore {
|
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
20
|
* In-memory `RateLimitStore` — the zero-config default (single process only).
|
|
21
|
-
* Lazy-evicts expired counters on access
|
|
22
|
-
*
|
|
23
|
-
* `DenylistStoreMemory`).
|
|
21
|
+
* Lazy-evicts expired counters on access, and sweeps expired counters itself
|
|
22
|
+
* as the map grows (amortized, each time it doubles); `cleanup()` sweeps the
|
|
23
|
+
* whole map on demand (same pattern as `DenylistStoreMemory`).
|
|
24
24
|
*/
|
|
25
25
|
declare class RateLimitStoreMemory implements RateLimitStore {
|
|
26
26
|
private readonly entries;
|
|
27
27
|
private readonly clock;
|
|
28
|
+
/** Map size at which `hit` next sweeps expired counters (amortized; doubles). */
|
|
29
|
+
private sweepAt;
|
|
28
30
|
constructor(opts?: {
|
|
29
31
|
clock?: Clock;
|
|
30
32
|
});
|
|
@@ -33,6 +35,7 @@ declare class RateLimitStoreMemory implements RateLimitStore {
|
|
|
33
35
|
/** Drop every counter — test-harness reset between cases. */
|
|
34
36
|
clear(): void;
|
|
35
37
|
cleanup(): Promise<number>;
|
|
38
|
+
private sweep;
|
|
36
39
|
}
|
|
37
40
|
//#endregion
|
|
38
41
|
export { RateLimitStoreMemory as n, RateLimitStore as t };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as Clock } from "./clock-
|
|
1
|
+
import { t as Clock } from "./clock-CyjqzLxc.cjs";
|
|
2
2
|
//#region src/rate-limit/store.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* Storage contract for the fixed-window limiter (RL.spec.md §4.3).
|
|
@@ -18,13 +18,15 @@ interface RateLimitStore {
|
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
20
|
* In-memory `RateLimitStore` — the zero-config default (single process only).
|
|
21
|
-
* Lazy-evicts expired counters on access
|
|
22
|
-
*
|
|
23
|
-
* `DenylistStoreMemory`).
|
|
21
|
+
* Lazy-evicts expired counters on access, and sweeps expired counters itself
|
|
22
|
+
* as the map grows (amortized, each time it doubles); `cleanup()` sweeps the
|
|
23
|
+
* whole map on demand (same pattern as `DenylistStoreMemory`).
|
|
24
24
|
*/
|
|
25
25
|
declare class RateLimitStoreMemory implements RateLimitStore {
|
|
26
26
|
private readonly entries;
|
|
27
27
|
private readonly clock;
|
|
28
|
+
/** Map size at which `hit` next sweeps expired counters (amortized; doubles). */
|
|
29
|
+
private sweepAt;
|
|
28
30
|
constructor(opts?: {
|
|
29
31
|
clock?: Clock;
|
|
30
32
|
});
|
|
@@ -33,6 +35,7 @@ declare class RateLimitStoreMemory implements RateLimitStore {
|
|
|
33
35
|
/** Drop every counter — test-harness reset between cases. */
|
|
34
36
|
clear(): void;
|
|
35
37
|
cleanup(): Promise<number>;
|
|
38
|
+
private sweep;
|
|
36
39
|
}
|
|
37
40
|
//#endregion
|
|
38
41
|
export { RateLimitStoreMemory as n, RateLimitStore as t };
|