@web-ts-toolkit/express-oidc-vault-memory-store 0.43.0 → 0.48.0

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 CHANGED
@@ -16,7 +16,7 @@ pnpm add @web-ts-toolkit/express-oidc-vault @web-ts-toolkit/express-oidc-vault-m
16
16
 
17
17
  ## Production Note
18
18
 
19
- This package stores authorization transactions, exchange codes, sessions, rotated-session aliases, and backchannel logout replay JTIs in process memory.
19
+ This package stores authorization transactions, exchange codes, sessions, rotated-session aliases, backchannel logout replay JTIs, and DPoP proof reservations in process memory.
20
20
 
21
21
  Do not use it for multi-instance or production deployments. Use Redis or MongoDB provider packages for durable or horizontally scaled deployments.
22
22
 
@@ -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`. Both ESM (`import`, `index.d.mts`) and CJS (`require`, `index.d.ts`) declaration conditions are shipped; consumer imports always use the package name.
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 remains a revocation alias for the current logical session while that logical session is still live. Aliases expire with the rotated target session and are removed when logical, subject, provider-session, direct, or expiry deletion leaves no live session in that logical lineage.
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.
80
+
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.
68
84
 
69
- Rotation requires a distinct unused target session ID. Missing-source, same-ID, and existing-target rotation conflicts throw `OidcVaultStoreConflictError` without changing source or target records.
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
 
@@ -77,13 +93,65 @@ The store keeps these record kinds in separate in-process maps:
77
93
  - sessions, read and deleted by session, logical session, subject, or provider session identifiers
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
96
+ - DPoP proof reservations with their own bounded expiry heap/capacity
80
97
 
81
- `createAuthorizationTransaction`, `createExchangeCode`, and `createSession` are upserts; creating the same key again replaces the old value. Metadata should be structured-clone compatible and JSON-compatible for portability across the memory, Redis, and MongoDB stores. The memory store clones inputs and returned records with `structuredClone`, so callers retain ownership of their objects and later mutations do not update persisted state.
98
+ `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.
99
+
100
+ 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
101
 
83
102
  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
103
 
104
+ 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.
105
+
85
106
  `deleteSession(...)`, `deleteSessionsByLogicalSessionId(...)`, `deleteSessionsBySubject(...)`, and `deleteSessionsByProviderSessionId(...)` logically revoke live sessions and remove stale rotation aliases when no live session remains in a logical lineage.
86
107
 
108
+ 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.
109
+
110
+ 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.
111
+
87
112
  ## Replay JTI Expiry
88
113
 
89
114
  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.
115
+
116
+ ## Device-Binding Store Capabilities
117
+
118
+ `createMemoryOidcVaultStore` returns the core `OidcVaultDeviceBindingStoreProvider`. Its six methods are callable directly from the inferred factory result:
119
+
120
+ All shared types (`OidcVaultDeviceBindingStoreProvider`, `OidcVaultRecordBindingMatch`, guarded consume inputs, `ReserveDpopProofInput`, revocation context and capacity error) are **named root exports of `@web-ts-toolkit/express-oidc-vault`**. This package exports only its factory/options. Either vault `deviceBinding` or `fingerprintRecognition` requires all six capabilities; fingerprint-only flows do not reserve proofs. API middleware uses this store as its explicit replayStore. See the core's shipped README for the complete issuer/request-aware API and browser recipe.
121
+
122
+ - `getAuthorizationTransaction(state)` / `getExchangeCode(code)`: detached live snapshots, without spending records; reads are not locks.
123
+ - `consumeAuthorizationTransactionIfMatches({ state, match })` / `consumeExchangeCodeIfMatches({ code, expectedSessionId, match })`: synchronous expiry/complete-match/deletion before yielding; one winner for concurrent matching consumers.
124
+ - `getSessionRevocationContext(sessionId)`: live handle or unexpired alias resolves its currently live lineage, returning only `logicalSessionId`, allowlisted `provider.issuer/clientId`, and optional `deviceBinding`. No upstream credentials, metadata or profile are returned.
125
+ - `reserveDpopProof({ replayKey, expiresAt })`: process-object-shared atomic replay admission, described below.
126
+
127
+ `match` requires both `{ deviceBinding, browserBindingHash }`; each is an exact value or **null meaning stored absence/undefined**. Null never ignores a field. Records are legacy (neither field), cookie-only (browser hash), or bound (hash + `{ type: 'dpop', jkt }`). Both hashes are canonical 43-character SHA-256 base64url. Null/malformed/extra binding fields and a key without a browser hash are invalid; writes reject, and corrupted stored data cannot yield credentials. Mismatch returns `null` without spending a live record; the honest caller may retry. Original legacy consumes refuse all guarded records, including cookie-only records, without consumption.
128
+
129
+ Session rotation inherits omitted/undefined binding and accepts the same original key; it rejects null/malformed binding, a changed key, or adding a binding to an unbound source before mutation. Session IDs still follow the upsert/alias contracts above. A revocation alias never supplies historical or missing binding as proof of unboundness: all live lineage members must agree on binding/provider identity (malformed/inconsistent authority throws a fixed private diagnostic). Revocation context scans that lineage's candidates in the session map; it does not authenticate aliases through `getSession`.
130
+
131
+ ## Per-Request DPoP Replay And Capacity
132
+
133
+ | Memory option | Default | Contract |
134
+ | ---------------------- | ---------- | -------------------------------------------------------------------------- |
135
+ | `dpopReplayMaxEntries` | `100000` | Positive safe integer, shared by this store object; no live eviction |
136
+ | `now` | `Date.now` | Epoch-millisecond deterministic test clock; proof/store windows must agree |
137
+
138
+ Use one shared store object for every local protection-space instance. Independent memory objects/processes intentionally have independent replay state; use Redis/MongoDB for multi-instance protection. The store reserves opaque keys verbatim; shared orchestration supplies the namespace/key/JTI hash. It does not verify HTTP proofs or build namespaces itself.
139
+
140
+ ```ts
141
+ import { OidcVaultDpopReplayCapacityError } from '@web-ts-toolkit/express-oidc-vault';
142
+ import { createMemoryOidcVaultStore } from '@web-ts-toolkit/express-oidc-vault-memory-store';
143
+
144
+ const replayStore = createMemoryOidcVaultStore({ dpopReplayMaxEntries: 100_000 });
145
+ // Called only after a proof's signature, target, key and nonce are verified.
146
+ const reserved = await replayStore.reserveDpopProof({
147
+ replayKey: 'opaque-protection-space-key-jti-digest',
148
+ expiresAt: Date.now() + 60_000,
149
+ });
150
+ void [reserved, OidcVaultDpopReplayCapacityError];
151
+ ```
152
+
153
+ `dpopReplayMaxEntries` defaults to **100000**, and must be a positive safe integer. A duplicate returns `false` before capacity checks and never extends expiry. New reservations at capacity throw the core `OidcVaultDpopReplayCapacityError`. Live entries are never evicted. A future safe-integer expiry is required, with remaining TTL **<=360000 ms**; invalid/nonfinite/unsafe/fractional/expired (`<= now()`) or overlong windows return `false` without allocating. Expired keys may be admitted again.
154
+
155
+ Replay uses a Map plus an **indexed min-heap with exactly one node per retained key**, with no unbounded stale-node accumulation or full-map snapshot rebuild. Each admission removes at most **64** expired heap nodes plus a targeted expired duplicate; insertion/removal costs O(log N). It never scans session or alias maps. Expired entries awaiting bounded cleanup may conservatively consume capacity; later new admissions reclaim them. Cleanup runs on traffic, not a timer, and the total retained replay state stays within the configured limit.
156
+
157
+ DPoP reservations are **per request**, much higher-volume than logout JTIs. Size for unique proofs/second × proof-validity window plus headroom (approved defaults: at most 70 seconds retained). All instances in a protection space must use identical windows/namespaces and synchronized clocks. Never release a reservation after a later route failure; retries require a fresh proof. Capacity/provider errors must fail closed and map through orchestration to sanitized replay-unavailable, not proof acceptance. Heap bounds do not imply a fixed latency/throughput guarantee.
package/index.d.mts CHANGED
@@ -1,6 +1,9 @@
1
- import { OidcVaultStoreProvider } from '@web-ts-toolkit/express-oidc-vault';
1
+ import { OidcVaultDeviceBindingStoreProvider } from '@web-ts-toolkit/express-oidc-vault';
2
2
 
3
+ /** Construction options for the named package-root memory-store factory. */
3
4
  interface MemoryOidcVaultStoreOptions {
5
+ /** Positive safe integer shared by this store object; default 100000. No live replay reservations are evicted. */
6
+ dpopReplayMaxEntries?: number;
4
7
  /**
5
8
  * Store clock in epoch milliseconds.
6
9
  *
@@ -16,8 +19,11 @@ interface MemoryOidcVaultStoreOptions {
16
19
  * Records are kept in memory, cloned on read/write, cleaned up opportunistically
17
20
  * during store operations, and lost when the Node.js process exits. Do not use
18
21
  * this provider when sessions must survive restarts or be shared by multiple
19
- * application instances.
22
+ * application instances. The returned stronger provider includes guarded
23
+ * exact/null consumes, token-free revocation context, and bounded DPoP replay.
24
+ * Replay is shared only by callers reusing this same object; the store does not
25
+ * verify HTTP proofs. Capacity defaults to 100000 and never evicts live entries.
20
26
  */
21
- declare function createMemoryOidcVaultStore(options?: MemoryOidcVaultStoreOptions): OidcVaultStoreProvider;
27
+ declare function createMemoryOidcVaultStore(options?: MemoryOidcVaultStoreOptions): OidcVaultDeviceBindingStoreProvider;
22
28
 
23
29
  export { type MemoryOidcVaultStoreOptions, createMemoryOidcVaultStore };
package/index.d.ts CHANGED
@@ -1,6 +1,9 @@
1
- import { OidcVaultStoreProvider } from '@web-ts-toolkit/express-oidc-vault';
1
+ import { OidcVaultDeviceBindingStoreProvider } from '@web-ts-toolkit/express-oidc-vault';
2
2
 
3
+ /** Construction options for the named package-root memory-store factory. */
3
4
  interface MemoryOidcVaultStoreOptions {
5
+ /** Positive safe integer shared by this store object; default 100000. No live replay reservations are evicted. */
6
+ dpopReplayMaxEntries?: number;
4
7
  /**
5
8
  * Store clock in epoch milliseconds.
6
9
  *
@@ -16,8 +19,11 @@ interface MemoryOidcVaultStoreOptions {
16
19
  * Records are kept in memory, cloned on read/write, cleaned up opportunistically
17
20
  * during store operations, and lost when the Node.js process exits. Do not use
18
21
  * this provider when sessions must survive restarts or be shared by multiple
19
- * application instances.
22
+ * application instances. The returned stronger provider includes guarded
23
+ * exact/null consumes, token-free revocation context, and bounded DPoP replay.
24
+ * Replay is shared only by callers reusing this same object; the store does not
25
+ * verify HTTP proofs. Capacity defaults to 100000 and never evicts live entries.
20
26
  */
21
- declare function createMemoryOidcVaultStore(options?: MemoryOidcVaultStoreOptions): OidcVaultStoreProvider;
27
+ declare function createMemoryOidcVaultStore(options?: MemoryOidcVaultStoreOptions): OidcVaultDeviceBindingStoreProvider;
22
28
 
23
29
  export { type MemoryOidcVaultStoreOptions, createMemoryOidcVaultStore };