@web-ts-toolkit/express-oidc-vault-memory-store 0.42.2 → 0.47.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -3
- package/index.js +128 -87
- package/index.mjs +128 -87
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -59,14 +59,30 @@ const storeProvider = createMemoryOidcVaultStore({
|
|
|
59
59
|
|
|
60
60
|
## Main Exports
|
|
61
61
|
|
|
62
|
+
Use named imports from the package root; there is no default export or public subpath API. Use Node.js `>=22.12.0` for the core dependency's CJS/ESM runtime support. TypeScript apps also need `@types/node` and `@types/express`.
|
|
63
|
+
|
|
62
64
|
- `createMemoryOidcVaultStore(...)`
|
|
63
65
|
- `type MemoryOidcVaultStoreOptions`
|
|
64
66
|
|
|
67
|
+
## Lifecycle And Diagnostics
|
|
68
|
+
|
|
69
|
+
The factory is immediately usable: no connection, `ready()` call, background timer, or store teardown is required. Reuse one instance across requests in the same process. Operations perform their map mutations synchronously before yielding; this does not share state with other processes. Discarding the instance or restarting loses all records.
|
|
70
|
+
|
|
71
|
+
The store does not log records or errors. Session tokens, authorization state/PKCE data, exchange codes, public session IDs, and arbitrary backend/hook errors may be sensitive. Application diagnostics should use fixed operation names and allowlisted categories, not whole records, raw error messages, connection URLs, or credential-valued metric labels.
|
|
72
|
+
|
|
65
73
|
## Session Rotation Aliases
|
|
66
74
|
|
|
67
|
-
When a session is rotated, the previous public session ID
|
|
75
|
+
When a session is rotated, the previous public session ID becomes a revocation alias for the target's logical session. Each alias keeps the immediate target's `expiresAt`, if present; later rotations do not extend that deadline. An unexpired alias remains usable while its logical lineage has a live member. Aliases without an expiry can remain for the lifetime of that lineage.
|
|
76
|
+
|
|
77
|
+
For example, `A -> B (expiresAt=T1) -> C (expiresAt=T2)` leaves A usable only before T1, even if T2 is later or absent; B uses C's deadline. `getSession(A)` returns `null`: an alias is only a logout handle, never a readable session. With `A/L1 -> B/L1 -> C/L2`, B revokes L2 and any retained A alias revokes L1, not C. Omitting the next logical ID preserves the source lineage. Logical IDs are unscoped: use distinct IDs for unrelated login families.
|
|
78
|
+
|
|
79
|
+
Without successor `expiresAt`, memory and Redis impose no alias time limit; MongoDB instead uses a finite fallback (default five minutes). Long-lived non-expiring lineages can accumulate arbitrarily many memory/Redis aliases. Set explicit session lifetimes for portable finite windows; after an alias expires, revoke using the live ID or the appropriate logical/subject/provider-session deletion method. Core refresh preserves the session expiry.
|
|
68
80
|
|
|
69
|
-
|
|
81
|
+
Aliases are removed when logical, subject, provider-session, direct, or expiry deletion leaves no live session in that lineage. A lineage-changing create-upsert or rotation also retires the old lineage's aliases when no live member remains; expired records awaiting cleanup do not count as survivors. If another live member survives, including in another issuer/client scope, the earlier aliases retain their original lineage and expiry. They are never retargeted to the new lineage.
|
|
82
|
+
|
|
83
|
+
Rotation requires a different target session ID with no live session record. An alias-only ID or an expired target is accepted. Successful rotation takes ownership of the target ID and clears any former alias under that ID, just as `createSession` does. Missing-source, same-ID, and live-target rotation conflicts throw `OidcVaultStoreConflictError`; conflicts and input-clone failures preserve live source/target records and their aliases.
|
|
84
|
+
|
|
85
|
+
Compatibility note: ownership transitions now retire aliases for terminated lineages, so reusing an old logical session ID cannot resurrect those handles. Reusing an alias-only rotation target also prevents that ID from reverting to its former lineage after replacement deletion or expiry.
|
|
70
86
|
|
|
71
87
|
## Store Contract
|
|
72
88
|
|
|
@@ -78,12 +94,20 @@ The store keeps these record kinds in separate in-process maps:
|
|
|
78
94
|
- rotated-session aliases, used only so an old public session ID can revoke the current logical session
|
|
79
95
|
- backchannel logout token JTIs, consumed once until their expiry time
|
|
80
96
|
|
|
81
|
-
`createAuthorizationTransaction`, `createExchangeCode`, and `createSession` are upserts; creating the same key again replaces the old value.
|
|
97
|
+
`createAuthorizationTransaction`, `createExchangeCode`, and `createSession` are upserts; creating the same key again replaces the old value. A `createSession` replacement takes over the session ID with its own subject/logical/provider-session scope, and clears any stale rotation alias held under that ID so the reused ID cannot invoke an old logical-session meaning. Portable callers must still create sessions with a fresh unused ID and handle `OidcVaultStoreConflictError`: reusing a live ID replaces on memory/MongoDB but rejects on Redis. MongoDB also retains an alias row under a reused ID until expiry or lineage cleanup; only fresh-ID creation is portable.
|
|
98
|
+
|
|
99
|
+
Portable data consists of strings, finite numbers, booleans, null, arrays, and plain objects (no cycles, custom prototypes/serializers, functions, symbols, or undefined properties). Inputs are captured at invocation, including nested provider/user/metadata data, and returned values are detached from caller and stored data. Mutation immediately after calling a method cannot change its target, scope, or portable value. Memory uses `structuredClone`; unsupported inputs reject. Outside the portable subset, memory structured-clone, MongoDB BSON, and Redis JSON semantics differ; do not assume native types or object identity survive across providers. MongoDB/Redis snapshot plain containers but do not recursively isolate opaque native objects.
|
|
82
100
|
|
|
83
101
|
Expiry checks use `expiresAt <= now()` as expired. Cleanup is opportunistic: reads and writes prune expired records in bounded batches or when a specific record is accessed, not on a background timer.
|
|
84
102
|
|
|
103
|
+
Each same-map sweep inspects at most 64 snapshot slots per operation from a positional cursor, so a late cursor never rescans the map prefix. A key snapshot is rebuilt once per full pass (amortized over that pass's sweeps); keys added mid-pass are picked up on the next pass. Nested rotated-alias/lineage cleanup after session expiry, deletion, or a lineage-changing ownership transition is not batch-bounded: for each affected lineage it can scan the retained session map for liveness and the alias map for removal. These full-map scans grow with store populations; the 64-slot sweep bound does not bound total operation work.
|
|
104
|
+
|
|
85
105
|
`deleteSession(...)`, `deleteSessionsByLogicalSessionId(...)`, `deleteSessionsBySubject(...)`, and `deleteSessionsByProviderSessionId(...)` logically revoke live sessions and remove stale rotation aliases when no live session remains in a logical lineage.
|
|
86
106
|
|
|
107
|
+
Deleting a live public ID removes that record; deleting an unexpired alias revokes its logical lineage. Subject/provider-session object inputs filter by each supplied `issuer` and `clientId`; string inputs omit those filters and match across providers. A surviving member in another scope retains its lineage's unexpired aliases. Logical deletion has no issuer/client filter, and aliases do not carry one.
|
|
108
|
+
|
|
109
|
+
Bulk methods return the number of live sessions removed, excluding expired records and alias cleanup. They scan the session map synchronously, but sessions created after that work can survive. Across providers, a count is deletion accounting, not proof that no matching sessions remain: MongoDB may count expired documents awaiting TTL cleanup; Redis counts primary deletions during cursor traversal. Vault deletion does not revoke already-issued stateless access tokens or perform upstream provider logout.
|
|
110
|
+
|
|
87
111
|
## Replay JTI Expiry
|
|
88
112
|
|
|
89
113
|
Backchannel logout token JTI records are consumed only when `expiresAt` is a finite timestamp greater than the store clock at consume time. Expired, equal-to-current-time, `NaN`, and infinite expiries return `false` and are not stored.
|
package/index.js
CHANGED
|
@@ -27,6 +27,14 @@ var import_express_oidc_vault = require("@web-ts-toolkit/express-oidc-vault");
|
|
|
27
27
|
var cloneRecord = (value) => structuredClone(value);
|
|
28
28
|
var isExpiredRecord = (value, now) => typeof value.expiresAt === "number" && value.expiresAt <= now;
|
|
29
29
|
var EXPIRY_SWEEP_BATCH_SIZE = 64;
|
|
30
|
+
var createMapSweepState = () => ({ keys: [], position: 0 });
|
|
31
|
+
var createMemoryStoreSweepWork = () => ({
|
|
32
|
+
inspectedKeys: 0,
|
|
33
|
+
staleSnapshotSlots: 0,
|
|
34
|
+
snapshotRebuilds: 0,
|
|
35
|
+
aliasSessionVisits: 0,
|
|
36
|
+
aliasEntryVisits: 0
|
|
37
|
+
});
|
|
30
38
|
var matchesProviderScope = (session, input) => {
|
|
31
39
|
if (input.issuer !== void 0 && session.provider?.issuer !== input.issuer) {
|
|
32
40
|
return false;
|
|
@@ -46,24 +54,22 @@ var MemoryOidcVaultStore = class {
|
|
|
46
54
|
this.sessions = /* @__PURE__ */ new Map();
|
|
47
55
|
this.rotatedSessionAliases = /* @__PURE__ */ new Map();
|
|
48
56
|
this.backchannelLogoutTokenJtis = /* @__PURE__ */ new Map();
|
|
57
|
+
this.authorizationTransactionSweep = createMapSweepState();
|
|
58
|
+
this.exchangeCodeSweep = createMapSweepState();
|
|
59
|
+
this.sessionSweep = createMapSweepState();
|
|
60
|
+
this.rotatedSessionAliasSweep = createMapSweepState();
|
|
61
|
+
this.backchannelLogoutTokenJtiSweep = createMapSweepState();
|
|
62
|
+
this.sweepWork = createMemoryStoreSweepWork();
|
|
49
63
|
this.now = options.now ?? (() => Date.now());
|
|
50
64
|
}
|
|
51
65
|
async createAuthorizationTransaction(input) {
|
|
52
66
|
const now = this.now();
|
|
53
|
-
this.
|
|
54
|
-
this.authorizationTransactions,
|
|
55
|
-
now,
|
|
56
|
-
this.authorizationTransactionSweepCursor
|
|
57
|
-
);
|
|
67
|
+
this.pruneMapBatch(this.authorizationTransactions, this.authorizationTransactionSweep, now);
|
|
58
68
|
this.authorizationTransactions.set(input.state, cloneRecord(input));
|
|
59
69
|
}
|
|
60
70
|
async consumeAuthorizationTransaction(state) {
|
|
61
71
|
const now = this.now();
|
|
62
|
-
this.
|
|
63
|
-
this.authorizationTransactions,
|
|
64
|
-
now,
|
|
65
|
-
this.authorizationTransactionSweepCursor
|
|
66
|
-
);
|
|
72
|
+
this.pruneMapBatch(this.authorizationTransactions, this.authorizationTransactionSweep, now);
|
|
67
73
|
const record = this.authorizationTransactions.get(state);
|
|
68
74
|
if (!record) {
|
|
69
75
|
return null;
|
|
@@ -76,12 +82,12 @@ var MemoryOidcVaultStore = class {
|
|
|
76
82
|
}
|
|
77
83
|
async createExchangeCode(input) {
|
|
78
84
|
const now = this.now();
|
|
79
|
-
this.
|
|
85
|
+
this.pruneMapBatch(this.exchangeCodes, this.exchangeCodeSweep, now);
|
|
80
86
|
this.exchangeCodes.set(input.code, cloneRecord(input));
|
|
81
87
|
}
|
|
82
88
|
async consumeExchangeCode(code) {
|
|
83
89
|
const now = this.now();
|
|
84
|
-
this.
|
|
90
|
+
this.pruneMapBatch(this.exchangeCodes, this.exchangeCodeSweep, now);
|
|
85
91
|
const record = this.exchangeCodes.get(code);
|
|
86
92
|
if (!record) {
|
|
87
93
|
return null;
|
|
@@ -95,19 +101,20 @@ var MemoryOidcVaultStore = class {
|
|
|
95
101
|
async createSession(input) {
|
|
96
102
|
const timestamp = this.now();
|
|
97
103
|
this.pruneSessionsBatch(timestamp);
|
|
98
|
-
this.
|
|
99
|
-
this.rotatedSessionAliases,
|
|
100
|
-
timestamp,
|
|
101
|
-
this.rotatedSessionAliasSweepCursor
|
|
102
|
-
);
|
|
104
|
+
this.pruneMapBatch(this.rotatedSessionAliases, this.rotatedSessionAliasSweep, timestamp);
|
|
103
105
|
const session = {
|
|
104
106
|
...cloneRecord(input),
|
|
105
107
|
logicalSessionId: input.logicalSessionId ?? input.sessionId,
|
|
106
108
|
createdAt: input.createdAt ?? timestamp,
|
|
107
109
|
updatedAt: input.updatedAt ?? timestamp
|
|
108
110
|
};
|
|
111
|
+
const previousSession = this.sessions.get(session.sessionId);
|
|
112
|
+
const previousLogicalSessionId = previousSession?.logicalSessionId ?? previousSession?.sessionId;
|
|
109
113
|
this.sessions.set(session.sessionId, session);
|
|
110
114
|
this.rotatedSessionAliases.delete(session.sessionId);
|
|
115
|
+
if (previousLogicalSessionId !== void 0 && previousLogicalSessionId !== session.logicalSessionId) {
|
|
116
|
+
this.removeAliasesForInactiveLogicalSession(previousLogicalSessionId, timestamp);
|
|
117
|
+
}
|
|
111
118
|
return cloneRecord(session);
|
|
112
119
|
}
|
|
113
120
|
async getSession(sessionId) {
|
|
@@ -115,7 +122,7 @@ var MemoryOidcVaultStore = class {
|
|
|
115
122
|
const session = this.sessions.get(sessionId);
|
|
116
123
|
if (session && isExpiredRecord(session, now)) {
|
|
117
124
|
this.sessions.delete(sessionId);
|
|
118
|
-
this.removeAliasesForInactiveLogicalSession(session.logicalSessionId ?? session.sessionId);
|
|
125
|
+
this.removeAliasesForInactiveLogicalSession(session.logicalSessionId ?? session.sessionId, now);
|
|
119
126
|
return null;
|
|
120
127
|
}
|
|
121
128
|
return session ? cloneRecord(session) : null;
|
|
@@ -125,7 +132,7 @@ var MemoryOidcVaultStore = class {
|
|
|
125
132
|
const sourceSession = this.sessions.get(input.sessionId);
|
|
126
133
|
if (sourceSession && isExpiredRecord(sourceSession, timestamp)) {
|
|
127
134
|
this.sessions.delete(input.sessionId);
|
|
128
|
-
this.removeAliasesForInactiveLogicalSession(sourceSession.logicalSessionId ?? sourceSession.sessionId);
|
|
135
|
+
this.removeAliasesForInactiveLogicalSession(sourceSession.logicalSessionId ?? sourceSession.sessionId, timestamp);
|
|
129
136
|
}
|
|
130
137
|
const liveSourceSession = this.sessions.get(input.sessionId);
|
|
131
138
|
if (!liveSourceSession) {
|
|
@@ -135,12 +142,7 @@ var MemoryOidcVaultStore = class {
|
|
|
135
142
|
throw new import_express_oidc_vault.OidcVaultStoreConflictError("OIDC vault session rotation target must use a different session ID.");
|
|
136
143
|
}
|
|
137
144
|
const existingTargetSession = this.sessions.get(input.nextSession.sessionId);
|
|
138
|
-
if (existingTargetSession && isExpiredRecord(existingTargetSession, timestamp)) {
|
|
139
|
-
this.sessions.delete(input.nextSession.sessionId);
|
|
140
|
-
this.removeAliasesForInactiveLogicalSession(
|
|
141
|
-
existingTargetSession.logicalSessionId ?? existingTargetSession.sessionId
|
|
142
|
-
);
|
|
143
|
-
} else if (existingTargetSession) {
|
|
145
|
+
if (existingTargetSession && !isExpiredRecord(existingTargetSession, timestamp)) {
|
|
144
146
|
throw new import_express_oidc_vault.OidcVaultStoreConflictError("OIDC vault session rotation target already exists.");
|
|
145
147
|
}
|
|
146
148
|
const nextSession = cloneRecord(input.nextSession);
|
|
@@ -150,28 +152,36 @@ var MemoryOidcVaultStore = class {
|
|
|
150
152
|
createdAt: nextSession.createdAt ?? timestamp,
|
|
151
153
|
updatedAt: nextSession.updatedAt ?? timestamp
|
|
152
154
|
};
|
|
155
|
+
if (existingTargetSession) {
|
|
156
|
+
this.sessions.delete(session.sessionId);
|
|
157
|
+
this.removeAliasesForInactiveLogicalSession(
|
|
158
|
+
existingTargetSession.logicalSessionId ?? existingTargetSession.sessionId,
|
|
159
|
+
timestamp
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
const previousLogicalSessionId = liveSourceSession.logicalSessionId ?? liveSourceSession.sessionId;
|
|
153
163
|
this.sessions.delete(input.sessionId);
|
|
154
164
|
this.sessions.set(session.sessionId, session);
|
|
165
|
+
this.rotatedSessionAliases.delete(session.sessionId);
|
|
155
166
|
this.rotatedSessionAliases.set(input.sessionId, {
|
|
156
167
|
logicalSessionId: session.logicalSessionId ?? session.sessionId,
|
|
157
168
|
expiresAt: session.expiresAt
|
|
158
169
|
});
|
|
170
|
+
if (previousLogicalSessionId !== session.logicalSessionId) {
|
|
171
|
+
this.removeAliasesForInactiveLogicalSession(previousLogicalSessionId, timestamp);
|
|
172
|
+
}
|
|
159
173
|
return cloneRecord(session);
|
|
160
174
|
}
|
|
161
175
|
async deleteSession(sessionId) {
|
|
162
176
|
const now = this.now();
|
|
163
177
|
this.pruneSessionsBatch(now);
|
|
164
|
-
this.
|
|
165
|
-
this.rotatedSessionAliases,
|
|
166
|
-
now,
|
|
167
|
-
this.rotatedSessionAliasSweepCursor
|
|
168
|
-
);
|
|
178
|
+
this.pruneMapBatch(this.rotatedSessionAliases, this.rotatedSessionAliasSweep, now);
|
|
169
179
|
const session = this.sessions.get(sessionId);
|
|
170
180
|
if (session) {
|
|
171
181
|
const logicalSessionId = session.logicalSessionId ?? session.sessionId;
|
|
172
182
|
this.sessions.delete(sessionId);
|
|
173
183
|
this.rotatedSessionAliases.delete(sessionId);
|
|
174
|
-
this.removeAliasesForInactiveLogicalSession(logicalSessionId);
|
|
184
|
+
this.removeAliasesForInactiveLogicalSession(logicalSessionId, now);
|
|
175
185
|
return;
|
|
176
186
|
}
|
|
177
187
|
const alias = this.rotatedSessionAliases.get(sessionId);
|
|
@@ -200,17 +210,13 @@ var MemoryOidcVaultStore = class {
|
|
|
200
210
|
deleted += 1;
|
|
201
211
|
}
|
|
202
212
|
}
|
|
203
|
-
this.removeAliasesForInactiveLogicalSessions(expiredLogicalSessionIds);
|
|
213
|
+
this.removeAliasesForInactiveLogicalSessions(expiredLogicalSessionIds, now);
|
|
204
214
|
this.removeAliasesForLogicalSession(resolved.logicalSessionId);
|
|
205
215
|
return deleted;
|
|
206
216
|
}
|
|
207
217
|
async consumeBackchannelLogoutTokenJti(input) {
|
|
208
218
|
const now = this.now();
|
|
209
|
-
this.
|
|
210
|
-
this.backchannelLogoutTokenJtis,
|
|
211
|
-
now,
|
|
212
|
-
this.backchannelLogoutTokenJtiSweepCursor
|
|
213
|
-
);
|
|
219
|
+
this.pruneMapBatch(this.backchannelLogoutTokenJtis, this.backchannelLogoutTokenJtiSweep, now);
|
|
214
220
|
if (!Number.isFinite(input.expiresAt) || input.expiresAt <= now) {
|
|
215
221
|
return false;
|
|
216
222
|
}
|
|
@@ -239,8 +245,8 @@ var MemoryOidcVaultStore = class {
|
|
|
239
245
|
deleted += 1;
|
|
240
246
|
}
|
|
241
247
|
}
|
|
242
|
-
this.removeAliasesForInactiveLogicalSessions(expiredLogicalSessionIds);
|
|
243
|
-
this.removeAliasesForInactiveLogicalSessions(logicalSessionIds);
|
|
248
|
+
this.removeAliasesForInactiveLogicalSessions(expiredLogicalSessionIds, now);
|
|
249
|
+
this.removeAliasesForInactiveLogicalSessions(logicalSessionIds, now);
|
|
244
250
|
return deleted;
|
|
245
251
|
}
|
|
246
252
|
async deleteSessionsByProviderSessionId(input) {
|
|
@@ -261,83 +267,118 @@ var MemoryOidcVaultStore = class {
|
|
|
261
267
|
deleted += 1;
|
|
262
268
|
}
|
|
263
269
|
}
|
|
264
|
-
this.removeAliasesForInactiveLogicalSessions(expiredLogicalSessionIds);
|
|
265
|
-
this.removeAliasesForInactiveLogicalSessions(logicalSessionIds);
|
|
270
|
+
this.removeAliasesForInactiveLogicalSessions(expiredLogicalSessionIds, now);
|
|
271
|
+
this.removeAliasesForInactiveLogicalSessions(logicalSessionIds, now);
|
|
266
272
|
return deleted;
|
|
267
273
|
}
|
|
268
|
-
pruneMapBatch(map,
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
274
|
+
pruneMapBatch(map, sweep, now) {
|
|
275
|
+
if (map.size === 0) {
|
|
276
|
+
sweep.keys = [];
|
|
277
|
+
sweep.position = 0;
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (sweep.position >= sweep.keys.length) {
|
|
281
|
+
this.rebuildSweepSnapshot(map, sweep);
|
|
282
|
+
}
|
|
283
|
+
let visited = 0;
|
|
284
|
+
while (visited < EXPIRY_SWEEP_BATCH_SIZE) {
|
|
285
|
+
if (sweep.position >= sweep.keys.length) {
|
|
286
|
+
if (map.size <= sweep.keys.length) {
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
this.rebuildSweepSnapshot(map, sweep);
|
|
290
|
+
}
|
|
291
|
+
const key = sweep.keys[sweep.position];
|
|
292
|
+
sweep.position += 1;
|
|
293
|
+
visited += 1;
|
|
294
|
+
const value = map.get(key);
|
|
295
|
+
if (value === void 0) {
|
|
296
|
+
this.sweepWork.staleSnapshotSlots += 1;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
this.sweepWork.inspectedKeys += 1;
|
|
272
300
|
if (isExpiredRecord(value, now)) {
|
|
273
301
|
map.delete(key);
|
|
274
302
|
}
|
|
275
|
-
inspected += 1;
|
|
276
|
-
nextCursor = key;
|
|
277
|
-
if (inspected >= EXPIRY_SWEEP_BATCH_SIZE) {
|
|
278
|
-
return nextCursor;
|
|
279
|
-
}
|
|
280
303
|
}
|
|
281
|
-
return void 0;
|
|
282
304
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if (isExpiredRecord(session, now)) {
|
|
288
|
-
expiredLogicalSessionIds.add(session.logicalSessionId ?? session.sessionId);
|
|
289
|
-
this.sessions.delete(sessionId);
|
|
290
|
-
}
|
|
291
|
-
inspected += 1;
|
|
292
|
-
this.sessionSweepCursor = sessionId;
|
|
293
|
-
if (inspected >= EXPIRY_SWEEP_BATCH_SIZE) {
|
|
294
|
-
this.removeAliasesForInactiveLogicalSessions(expiredLogicalSessionIds);
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
this.sessionSweepCursor = void 0;
|
|
299
|
-
this.removeAliasesForInactiveLogicalSessions(expiredLogicalSessionIds);
|
|
305
|
+
rebuildSweepSnapshot(map, sweep) {
|
|
306
|
+
sweep.keys = Array.from(map.keys());
|
|
307
|
+
sweep.position = 0;
|
|
308
|
+
this.sweepWork.snapshotRebuilds += 1;
|
|
300
309
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
310
|
+
pruneSessionsBatch(now) {
|
|
311
|
+
const sweep = this.sessionSweep;
|
|
312
|
+
if (this.sessions.size === 0) {
|
|
313
|
+
sweep.keys = [];
|
|
314
|
+
sweep.position = 0;
|
|
304
315
|
return;
|
|
305
316
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
317
|
+
if (sweep.position >= sweep.keys.length) {
|
|
318
|
+
this.rebuildSweepSnapshot(this.sessions, sweep);
|
|
319
|
+
}
|
|
320
|
+
const expiredLogicalSessionIds = /* @__PURE__ */ new Set();
|
|
321
|
+
let visited = 0;
|
|
322
|
+
while (visited < EXPIRY_SWEEP_BATCH_SIZE) {
|
|
323
|
+
if (sweep.position >= sweep.keys.length) {
|
|
324
|
+
if (this.sessions.size <= sweep.keys.length) {
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
this.rebuildSweepSnapshot(this.sessions, sweep);
|
|
328
|
+
}
|
|
329
|
+
const sessionId = sweep.keys[sweep.position];
|
|
330
|
+
sweep.position += 1;
|
|
331
|
+
visited += 1;
|
|
332
|
+
const session = this.sessions.get(sessionId);
|
|
333
|
+
if (session === void 0) {
|
|
334
|
+
this.sweepWork.staleSnapshotSlots += 1;
|
|
310
335
|
continue;
|
|
311
336
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
return;
|
|
337
|
+
this.sweepWork.inspectedKeys += 1;
|
|
338
|
+
if (isExpiredRecord(session, now)) {
|
|
339
|
+
expiredLogicalSessionIds.add(session.logicalSessionId ?? session.sessionId);
|
|
340
|
+
this.sessions.delete(sessionId);
|
|
317
341
|
}
|
|
318
|
-
yield entry;
|
|
319
342
|
}
|
|
343
|
+
this.removeAliasesForInactiveLogicalSessions(expiredLogicalSessionIds, now);
|
|
320
344
|
}
|
|
321
|
-
removeAliasesForInactiveLogicalSessions(logicalSessionIds) {
|
|
345
|
+
removeAliasesForInactiveLogicalSessions(logicalSessionIds, now) {
|
|
322
346
|
for (const logicalSessionId of logicalSessionIds) {
|
|
323
|
-
this.removeAliasesForInactiveLogicalSession(logicalSessionId);
|
|
347
|
+
this.removeAliasesForInactiveLogicalSession(logicalSessionId, now);
|
|
324
348
|
}
|
|
325
349
|
}
|
|
326
|
-
removeAliasesForInactiveLogicalSession(logicalSessionId) {
|
|
327
|
-
if (!this.hasLiveSessionForLogicalSession(logicalSessionId)) {
|
|
350
|
+
removeAliasesForInactiveLogicalSession(logicalSessionId, now) {
|
|
351
|
+
if (!this.hasLiveSessionForLogicalSession(logicalSessionId, now)) {
|
|
328
352
|
this.removeAliasesForLogicalSession(logicalSessionId);
|
|
329
353
|
}
|
|
330
354
|
}
|
|
331
|
-
|
|
355
|
+
/**
|
|
356
|
+
* Residual cost note (SVH-04): liveness is a full scan of `sessions`
|
|
357
|
+
* (early-out on the first live member), so per retired logical lineage the
|
|
358
|
+
* cleanup work is proportional to retained sessions, not to the 64-entry sweep
|
|
359
|
+
* batch. Visits are counted in `sweepWork.aliasSessionVisits` so this
|
|
360
|
+
* nested cost stays measurable and is never implied to be constant-time.
|
|
361
|
+
*/
|
|
362
|
+
hasLiveSessionForLogicalSession(logicalSessionId, now) {
|
|
332
363
|
for (const session of this.sessions.values()) {
|
|
333
|
-
|
|
364
|
+
this.sweepWork.aliasSessionVisits += 1;
|
|
365
|
+
if ((session.logicalSessionId ?? session.sessionId) === logicalSessionId && !isExpiredRecord(session, now)) {
|
|
334
366
|
return true;
|
|
335
367
|
}
|
|
336
368
|
}
|
|
337
369
|
return false;
|
|
338
370
|
}
|
|
371
|
+
/**
|
|
372
|
+
* Residual cost note (SVH-04): alias removal scans every
|
|
373
|
+
* `rotatedSessionAliases` entry for the lineage, so per expired lineage the
|
|
374
|
+
* work is proportional to live aliases. Visits are counted in
|
|
375
|
+
* `sweepWork.aliasEntryVisits`. Bounding this further would need a
|
|
376
|
+
* lineage-to-alias index; that was deferred as unjustified complexity for
|
|
377
|
+
* the measured workloads (see README sweep-bounds note).
|
|
378
|
+
*/
|
|
339
379
|
removeAliasesForLogicalSession(logicalSessionId) {
|
|
340
380
|
for (const [sessionId, alias] of this.rotatedSessionAliases.entries()) {
|
|
381
|
+
this.sweepWork.aliasEntryVisits += 1;
|
|
341
382
|
if (alias.logicalSessionId === logicalSessionId) {
|
|
342
383
|
this.rotatedSessionAliases.delete(sessionId);
|
|
343
384
|
}
|
package/index.mjs
CHANGED
|
@@ -5,6 +5,14 @@ import {
|
|
|
5
5
|
var cloneRecord = (value) => structuredClone(value);
|
|
6
6
|
var isExpiredRecord = (value, now) => typeof value.expiresAt === "number" && value.expiresAt <= now;
|
|
7
7
|
var EXPIRY_SWEEP_BATCH_SIZE = 64;
|
|
8
|
+
var createMapSweepState = () => ({ keys: [], position: 0 });
|
|
9
|
+
var createMemoryStoreSweepWork = () => ({
|
|
10
|
+
inspectedKeys: 0,
|
|
11
|
+
staleSnapshotSlots: 0,
|
|
12
|
+
snapshotRebuilds: 0,
|
|
13
|
+
aliasSessionVisits: 0,
|
|
14
|
+
aliasEntryVisits: 0
|
|
15
|
+
});
|
|
8
16
|
var matchesProviderScope = (session, input) => {
|
|
9
17
|
if (input.issuer !== void 0 && session.provider?.issuer !== input.issuer) {
|
|
10
18
|
return false;
|
|
@@ -24,24 +32,22 @@ var MemoryOidcVaultStore = class {
|
|
|
24
32
|
this.sessions = /* @__PURE__ */ new Map();
|
|
25
33
|
this.rotatedSessionAliases = /* @__PURE__ */ new Map();
|
|
26
34
|
this.backchannelLogoutTokenJtis = /* @__PURE__ */ new Map();
|
|
35
|
+
this.authorizationTransactionSweep = createMapSweepState();
|
|
36
|
+
this.exchangeCodeSweep = createMapSweepState();
|
|
37
|
+
this.sessionSweep = createMapSweepState();
|
|
38
|
+
this.rotatedSessionAliasSweep = createMapSweepState();
|
|
39
|
+
this.backchannelLogoutTokenJtiSweep = createMapSweepState();
|
|
40
|
+
this.sweepWork = createMemoryStoreSweepWork();
|
|
27
41
|
this.now = options.now ?? (() => Date.now());
|
|
28
42
|
}
|
|
29
43
|
async createAuthorizationTransaction(input) {
|
|
30
44
|
const now = this.now();
|
|
31
|
-
this.
|
|
32
|
-
this.authorizationTransactions,
|
|
33
|
-
now,
|
|
34
|
-
this.authorizationTransactionSweepCursor
|
|
35
|
-
);
|
|
45
|
+
this.pruneMapBatch(this.authorizationTransactions, this.authorizationTransactionSweep, now);
|
|
36
46
|
this.authorizationTransactions.set(input.state, cloneRecord(input));
|
|
37
47
|
}
|
|
38
48
|
async consumeAuthorizationTransaction(state) {
|
|
39
49
|
const now = this.now();
|
|
40
|
-
this.
|
|
41
|
-
this.authorizationTransactions,
|
|
42
|
-
now,
|
|
43
|
-
this.authorizationTransactionSweepCursor
|
|
44
|
-
);
|
|
50
|
+
this.pruneMapBatch(this.authorizationTransactions, this.authorizationTransactionSweep, now);
|
|
45
51
|
const record = this.authorizationTransactions.get(state);
|
|
46
52
|
if (!record) {
|
|
47
53
|
return null;
|
|
@@ -54,12 +60,12 @@ var MemoryOidcVaultStore = class {
|
|
|
54
60
|
}
|
|
55
61
|
async createExchangeCode(input) {
|
|
56
62
|
const now = this.now();
|
|
57
|
-
this.
|
|
63
|
+
this.pruneMapBatch(this.exchangeCodes, this.exchangeCodeSweep, now);
|
|
58
64
|
this.exchangeCodes.set(input.code, cloneRecord(input));
|
|
59
65
|
}
|
|
60
66
|
async consumeExchangeCode(code) {
|
|
61
67
|
const now = this.now();
|
|
62
|
-
this.
|
|
68
|
+
this.pruneMapBatch(this.exchangeCodes, this.exchangeCodeSweep, now);
|
|
63
69
|
const record = this.exchangeCodes.get(code);
|
|
64
70
|
if (!record) {
|
|
65
71
|
return null;
|
|
@@ -73,19 +79,20 @@ var MemoryOidcVaultStore = class {
|
|
|
73
79
|
async createSession(input) {
|
|
74
80
|
const timestamp = this.now();
|
|
75
81
|
this.pruneSessionsBatch(timestamp);
|
|
76
|
-
this.
|
|
77
|
-
this.rotatedSessionAliases,
|
|
78
|
-
timestamp,
|
|
79
|
-
this.rotatedSessionAliasSweepCursor
|
|
80
|
-
);
|
|
82
|
+
this.pruneMapBatch(this.rotatedSessionAliases, this.rotatedSessionAliasSweep, timestamp);
|
|
81
83
|
const session = {
|
|
82
84
|
...cloneRecord(input),
|
|
83
85
|
logicalSessionId: input.logicalSessionId ?? input.sessionId,
|
|
84
86
|
createdAt: input.createdAt ?? timestamp,
|
|
85
87
|
updatedAt: input.updatedAt ?? timestamp
|
|
86
88
|
};
|
|
89
|
+
const previousSession = this.sessions.get(session.sessionId);
|
|
90
|
+
const previousLogicalSessionId = previousSession?.logicalSessionId ?? previousSession?.sessionId;
|
|
87
91
|
this.sessions.set(session.sessionId, session);
|
|
88
92
|
this.rotatedSessionAliases.delete(session.sessionId);
|
|
93
|
+
if (previousLogicalSessionId !== void 0 && previousLogicalSessionId !== session.logicalSessionId) {
|
|
94
|
+
this.removeAliasesForInactiveLogicalSession(previousLogicalSessionId, timestamp);
|
|
95
|
+
}
|
|
89
96
|
return cloneRecord(session);
|
|
90
97
|
}
|
|
91
98
|
async getSession(sessionId) {
|
|
@@ -93,7 +100,7 @@ var MemoryOidcVaultStore = class {
|
|
|
93
100
|
const session = this.sessions.get(sessionId);
|
|
94
101
|
if (session && isExpiredRecord(session, now)) {
|
|
95
102
|
this.sessions.delete(sessionId);
|
|
96
|
-
this.removeAliasesForInactiveLogicalSession(session.logicalSessionId ?? session.sessionId);
|
|
103
|
+
this.removeAliasesForInactiveLogicalSession(session.logicalSessionId ?? session.sessionId, now);
|
|
97
104
|
return null;
|
|
98
105
|
}
|
|
99
106
|
return session ? cloneRecord(session) : null;
|
|
@@ -103,7 +110,7 @@ var MemoryOidcVaultStore = class {
|
|
|
103
110
|
const sourceSession = this.sessions.get(input.sessionId);
|
|
104
111
|
if (sourceSession && isExpiredRecord(sourceSession, timestamp)) {
|
|
105
112
|
this.sessions.delete(input.sessionId);
|
|
106
|
-
this.removeAliasesForInactiveLogicalSession(sourceSession.logicalSessionId ?? sourceSession.sessionId);
|
|
113
|
+
this.removeAliasesForInactiveLogicalSession(sourceSession.logicalSessionId ?? sourceSession.sessionId, timestamp);
|
|
107
114
|
}
|
|
108
115
|
const liveSourceSession = this.sessions.get(input.sessionId);
|
|
109
116
|
if (!liveSourceSession) {
|
|
@@ -113,12 +120,7 @@ var MemoryOidcVaultStore = class {
|
|
|
113
120
|
throw new OidcVaultStoreConflictError("OIDC vault session rotation target must use a different session ID.");
|
|
114
121
|
}
|
|
115
122
|
const existingTargetSession = this.sessions.get(input.nextSession.sessionId);
|
|
116
|
-
if (existingTargetSession && isExpiredRecord(existingTargetSession, timestamp)) {
|
|
117
|
-
this.sessions.delete(input.nextSession.sessionId);
|
|
118
|
-
this.removeAliasesForInactiveLogicalSession(
|
|
119
|
-
existingTargetSession.logicalSessionId ?? existingTargetSession.sessionId
|
|
120
|
-
);
|
|
121
|
-
} else if (existingTargetSession) {
|
|
123
|
+
if (existingTargetSession && !isExpiredRecord(existingTargetSession, timestamp)) {
|
|
122
124
|
throw new OidcVaultStoreConflictError("OIDC vault session rotation target already exists.");
|
|
123
125
|
}
|
|
124
126
|
const nextSession = cloneRecord(input.nextSession);
|
|
@@ -128,28 +130,36 @@ var MemoryOidcVaultStore = class {
|
|
|
128
130
|
createdAt: nextSession.createdAt ?? timestamp,
|
|
129
131
|
updatedAt: nextSession.updatedAt ?? timestamp
|
|
130
132
|
};
|
|
133
|
+
if (existingTargetSession) {
|
|
134
|
+
this.sessions.delete(session.sessionId);
|
|
135
|
+
this.removeAliasesForInactiveLogicalSession(
|
|
136
|
+
existingTargetSession.logicalSessionId ?? existingTargetSession.sessionId,
|
|
137
|
+
timestamp
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
const previousLogicalSessionId = liveSourceSession.logicalSessionId ?? liveSourceSession.sessionId;
|
|
131
141
|
this.sessions.delete(input.sessionId);
|
|
132
142
|
this.sessions.set(session.sessionId, session);
|
|
143
|
+
this.rotatedSessionAliases.delete(session.sessionId);
|
|
133
144
|
this.rotatedSessionAliases.set(input.sessionId, {
|
|
134
145
|
logicalSessionId: session.logicalSessionId ?? session.sessionId,
|
|
135
146
|
expiresAt: session.expiresAt
|
|
136
147
|
});
|
|
148
|
+
if (previousLogicalSessionId !== session.logicalSessionId) {
|
|
149
|
+
this.removeAliasesForInactiveLogicalSession(previousLogicalSessionId, timestamp);
|
|
150
|
+
}
|
|
137
151
|
return cloneRecord(session);
|
|
138
152
|
}
|
|
139
153
|
async deleteSession(sessionId) {
|
|
140
154
|
const now = this.now();
|
|
141
155
|
this.pruneSessionsBatch(now);
|
|
142
|
-
this.
|
|
143
|
-
this.rotatedSessionAliases,
|
|
144
|
-
now,
|
|
145
|
-
this.rotatedSessionAliasSweepCursor
|
|
146
|
-
);
|
|
156
|
+
this.pruneMapBatch(this.rotatedSessionAliases, this.rotatedSessionAliasSweep, now);
|
|
147
157
|
const session = this.sessions.get(sessionId);
|
|
148
158
|
if (session) {
|
|
149
159
|
const logicalSessionId = session.logicalSessionId ?? session.sessionId;
|
|
150
160
|
this.sessions.delete(sessionId);
|
|
151
161
|
this.rotatedSessionAliases.delete(sessionId);
|
|
152
|
-
this.removeAliasesForInactiveLogicalSession(logicalSessionId);
|
|
162
|
+
this.removeAliasesForInactiveLogicalSession(logicalSessionId, now);
|
|
153
163
|
return;
|
|
154
164
|
}
|
|
155
165
|
const alias = this.rotatedSessionAliases.get(sessionId);
|
|
@@ -178,17 +188,13 @@ var MemoryOidcVaultStore = class {
|
|
|
178
188
|
deleted += 1;
|
|
179
189
|
}
|
|
180
190
|
}
|
|
181
|
-
this.removeAliasesForInactiveLogicalSessions(expiredLogicalSessionIds);
|
|
191
|
+
this.removeAliasesForInactiveLogicalSessions(expiredLogicalSessionIds, now);
|
|
182
192
|
this.removeAliasesForLogicalSession(resolved.logicalSessionId);
|
|
183
193
|
return deleted;
|
|
184
194
|
}
|
|
185
195
|
async consumeBackchannelLogoutTokenJti(input) {
|
|
186
196
|
const now = this.now();
|
|
187
|
-
this.
|
|
188
|
-
this.backchannelLogoutTokenJtis,
|
|
189
|
-
now,
|
|
190
|
-
this.backchannelLogoutTokenJtiSweepCursor
|
|
191
|
-
);
|
|
197
|
+
this.pruneMapBatch(this.backchannelLogoutTokenJtis, this.backchannelLogoutTokenJtiSweep, now);
|
|
192
198
|
if (!Number.isFinite(input.expiresAt) || input.expiresAt <= now) {
|
|
193
199
|
return false;
|
|
194
200
|
}
|
|
@@ -217,8 +223,8 @@ var MemoryOidcVaultStore = class {
|
|
|
217
223
|
deleted += 1;
|
|
218
224
|
}
|
|
219
225
|
}
|
|
220
|
-
this.removeAliasesForInactiveLogicalSessions(expiredLogicalSessionIds);
|
|
221
|
-
this.removeAliasesForInactiveLogicalSessions(logicalSessionIds);
|
|
226
|
+
this.removeAliasesForInactiveLogicalSessions(expiredLogicalSessionIds, now);
|
|
227
|
+
this.removeAliasesForInactiveLogicalSessions(logicalSessionIds, now);
|
|
222
228
|
return deleted;
|
|
223
229
|
}
|
|
224
230
|
async deleteSessionsByProviderSessionId(input) {
|
|
@@ -239,83 +245,118 @@ var MemoryOidcVaultStore = class {
|
|
|
239
245
|
deleted += 1;
|
|
240
246
|
}
|
|
241
247
|
}
|
|
242
|
-
this.removeAliasesForInactiveLogicalSessions(expiredLogicalSessionIds);
|
|
243
|
-
this.removeAliasesForInactiveLogicalSessions(logicalSessionIds);
|
|
248
|
+
this.removeAliasesForInactiveLogicalSessions(expiredLogicalSessionIds, now);
|
|
249
|
+
this.removeAliasesForInactiveLogicalSessions(logicalSessionIds, now);
|
|
244
250
|
return deleted;
|
|
245
251
|
}
|
|
246
|
-
pruneMapBatch(map,
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
252
|
+
pruneMapBatch(map, sweep, now) {
|
|
253
|
+
if (map.size === 0) {
|
|
254
|
+
sweep.keys = [];
|
|
255
|
+
sweep.position = 0;
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (sweep.position >= sweep.keys.length) {
|
|
259
|
+
this.rebuildSweepSnapshot(map, sweep);
|
|
260
|
+
}
|
|
261
|
+
let visited = 0;
|
|
262
|
+
while (visited < EXPIRY_SWEEP_BATCH_SIZE) {
|
|
263
|
+
if (sweep.position >= sweep.keys.length) {
|
|
264
|
+
if (map.size <= sweep.keys.length) {
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
this.rebuildSweepSnapshot(map, sweep);
|
|
268
|
+
}
|
|
269
|
+
const key = sweep.keys[sweep.position];
|
|
270
|
+
sweep.position += 1;
|
|
271
|
+
visited += 1;
|
|
272
|
+
const value = map.get(key);
|
|
273
|
+
if (value === void 0) {
|
|
274
|
+
this.sweepWork.staleSnapshotSlots += 1;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
this.sweepWork.inspectedKeys += 1;
|
|
250
278
|
if (isExpiredRecord(value, now)) {
|
|
251
279
|
map.delete(key);
|
|
252
280
|
}
|
|
253
|
-
inspected += 1;
|
|
254
|
-
nextCursor = key;
|
|
255
|
-
if (inspected >= EXPIRY_SWEEP_BATCH_SIZE) {
|
|
256
|
-
return nextCursor;
|
|
257
|
-
}
|
|
258
281
|
}
|
|
259
|
-
return void 0;
|
|
260
282
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
if (isExpiredRecord(session, now)) {
|
|
266
|
-
expiredLogicalSessionIds.add(session.logicalSessionId ?? session.sessionId);
|
|
267
|
-
this.sessions.delete(sessionId);
|
|
268
|
-
}
|
|
269
|
-
inspected += 1;
|
|
270
|
-
this.sessionSweepCursor = sessionId;
|
|
271
|
-
if (inspected >= EXPIRY_SWEEP_BATCH_SIZE) {
|
|
272
|
-
this.removeAliasesForInactiveLogicalSessions(expiredLogicalSessionIds);
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
this.sessionSweepCursor = void 0;
|
|
277
|
-
this.removeAliasesForInactiveLogicalSessions(expiredLogicalSessionIds);
|
|
283
|
+
rebuildSweepSnapshot(map, sweep) {
|
|
284
|
+
sweep.keys = Array.from(map.keys());
|
|
285
|
+
sweep.position = 0;
|
|
286
|
+
this.sweepWork.snapshotRebuilds += 1;
|
|
278
287
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
288
|
+
pruneSessionsBatch(now) {
|
|
289
|
+
const sweep = this.sessionSweep;
|
|
290
|
+
if (this.sessions.size === 0) {
|
|
291
|
+
sweep.keys = [];
|
|
292
|
+
sweep.position = 0;
|
|
282
293
|
return;
|
|
283
294
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
295
|
+
if (sweep.position >= sweep.keys.length) {
|
|
296
|
+
this.rebuildSweepSnapshot(this.sessions, sweep);
|
|
297
|
+
}
|
|
298
|
+
const expiredLogicalSessionIds = /* @__PURE__ */ new Set();
|
|
299
|
+
let visited = 0;
|
|
300
|
+
while (visited < EXPIRY_SWEEP_BATCH_SIZE) {
|
|
301
|
+
if (sweep.position >= sweep.keys.length) {
|
|
302
|
+
if (this.sessions.size <= sweep.keys.length) {
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
this.rebuildSweepSnapshot(this.sessions, sweep);
|
|
306
|
+
}
|
|
307
|
+
const sessionId = sweep.keys[sweep.position];
|
|
308
|
+
sweep.position += 1;
|
|
309
|
+
visited += 1;
|
|
310
|
+
const session = this.sessions.get(sessionId);
|
|
311
|
+
if (session === void 0) {
|
|
312
|
+
this.sweepWork.staleSnapshotSlots += 1;
|
|
288
313
|
continue;
|
|
289
314
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
return;
|
|
315
|
+
this.sweepWork.inspectedKeys += 1;
|
|
316
|
+
if (isExpiredRecord(session, now)) {
|
|
317
|
+
expiredLogicalSessionIds.add(session.logicalSessionId ?? session.sessionId);
|
|
318
|
+
this.sessions.delete(sessionId);
|
|
295
319
|
}
|
|
296
|
-
yield entry;
|
|
297
320
|
}
|
|
321
|
+
this.removeAliasesForInactiveLogicalSessions(expiredLogicalSessionIds, now);
|
|
298
322
|
}
|
|
299
|
-
removeAliasesForInactiveLogicalSessions(logicalSessionIds) {
|
|
323
|
+
removeAliasesForInactiveLogicalSessions(logicalSessionIds, now) {
|
|
300
324
|
for (const logicalSessionId of logicalSessionIds) {
|
|
301
|
-
this.removeAliasesForInactiveLogicalSession(logicalSessionId);
|
|
325
|
+
this.removeAliasesForInactiveLogicalSession(logicalSessionId, now);
|
|
302
326
|
}
|
|
303
327
|
}
|
|
304
|
-
removeAliasesForInactiveLogicalSession(logicalSessionId) {
|
|
305
|
-
if (!this.hasLiveSessionForLogicalSession(logicalSessionId)) {
|
|
328
|
+
removeAliasesForInactiveLogicalSession(logicalSessionId, now) {
|
|
329
|
+
if (!this.hasLiveSessionForLogicalSession(logicalSessionId, now)) {
|
|
306
330
|
this.removeAliasesForLogicalSession(logicalSessionId);
|
|
307
331
|
}
|
|
308
332
|
}
|
|
309
|
-
|
|
333
|
+
/**
|
|
334
|
+
* Residual cost note (SVH-04): liveness is a full scan of `sessions`
|
|
335
|
+
* (early-out on the first live member), so per retired logical lineage the
|
|
336
|
+
* cleanup work is proportional to retained sessions, not to the 64-entry sweep
|
|
337
|
+
* batch. Visits are counted in `sweepWork.aliasSessionVisits` so this
|
|
338
|
+
* nested cost stays measurable and is never implied to be constant-time.
|
|
339
|
+
*/
|
|
340
|
+
hasLiveSessionForLogicalSession(logicalSessionId, now) {
|
|
310
341
|
for (const session of this.sessions.values()) {
|
|
311
|
-
|
|
342
|
+
this.sweepWork.aliasSessionVisits += 1;
|
|
343
|
+
if ((session.logicalSessionId ?? session.sessionId) === logicalSessionId && !isExpiredRecord(session, now)) {
|
|
312
344
|
return true;
|
|
313
345
|
}
|
|
314
346
|
}
|
|
315
347
|
return false;
|
|
316
348
|
}
|
|
349
|
+
/**
|
|
350
|
+
* Residual cost note (SVH-04): alias removal scans every
|
|
351
|
+
* `rotatedSessionAliases` entry for the lineage, so per expired lineage the
|
|
352
|
+
* work is proportional to live aliases. Visits are counted in
|
|
353
|
+
* `sweepWork.aliasEntryVisits`. Bounding this further would need a
|
|
354
|
+
* lineage-to-alias index; that was deferred as unjustified complexity for
|
|
355
|
+
* the measured workloads (see README sweep-bounds note).
|
|
356
|
+
*/
|
|
317
357
|
removeAliasesForLogicalSession(logicalSessionId) {
|
|
318
358
|
for (const [sessionId, alias] of this.rotatedSessionAliases.entries()) {
|
|
359
|
+
this.sweepWork.aliasEntryVisits += 1;
|
|
319
360
|
if (alias.logicalSessionId === logicalSessionId) {
|
|
320
361
|
this.rotatedSessionAliases.delete(sessionId);
|
|
321
362
|
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@web-ts-toolkit/express-oidc-vault-memory-store",
|
|
3
3
|
"description": "In-memory store provider for @web-ts-toolkit/express-oidc-vault",
|
|
4
4
|
"homepage": "https://web-ts-toolkit.pages.dev/docs/packages/express-oidc-vault-memory-store",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.47.1",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"keywords": [
|
|
8
8
|
"express",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"node": ">=22"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@web-ts-toolkit/express-oidc-vault": "0.
|
|
32
|
+
"@web-ts-toolkit/express-oidc-vault": "0.47.1"
|
|
33
33
|
},
|
|
34
34
|
"files": [
|
|
35
35
|
"**/*",
|