@web-ts-toolkit/express-oidc-vault-redis-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
@@ -20,6 +20,7 @@ import { createClient } from 'redis';
20
20
  import { createRedisOidcVaultStore } from '@web-ts-toolkit/express-oidc-vault-redis-store';
21
21
 
22
22
  const redis = createClient({ url: process.env.REDIS_URL });
23
+ redis.on('error', () => console.warn('OIDC vault Redis connection error.'));
23
24
  await redis.connect();
24
25
 
25
26
  const storeProvider = createRedisOidcVaultStore({
@@ -38,6 +39,7 @@ import { createRedisOidcVaultStore } from '@web-ts-toolkit/express-oidc-vault-re
38
39
 
39
40
  const app = express();
40
41
  const redis = createClient({ url: process.env.REDIS_URL });
42
+ redis.on('error', () => console.warn('OIDC vault Redis connection error.'));
41
43
 
42
44
  await redis.connect();
43
45
 
@@ -69,19 +71,37 @@ app.use(
69
71
  - one-time records are consumed atomically through Redis commands instead of `get` plus `del`
70
72
  - session rotation and session indexes are updated atomically so concurrent refreshes do not fork multiple active sessions
71
73
  - 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
72
- - obsolete rotated session IDs are stored as aliases owned by the logical session; revoking the active logical session, or deleting through any obsolete ID, removes all aliases and the reverse alias index
74
+ - obsolete rotated session IDs are stored as aliases owned by the logical session; scoped/direct deletion preserves unexpired aliases while another live member survives, including another issuer/client scope. Deleting through an obsolete ID still revokes its logical lineage
75
+ - alias cleanup runs inside the delete script alongside the primary mutation. It checks keyed primary membership with Redis TTL authority before removing terminated-lineage aliases; empty logical logout uses the same liveness rule in an atomic cleanup script. Rotation cannot interleave between that check and alias removal. The overall indexed traversal is not a global logout snapshot, so later arrivals can survive
76
+ - **Alias-cleanup compatibility:** scoped/direct logout no longer discards surviving lineage handles. Successful delete scripts no longer repeat application-side alias cleanup. A fixed warmed workload of 12 rotated lineages deleted by subject used 52 client commands before and 16 after on both Redis 6.2 and 7.2 (36 fewer sequential round trips); this measures command count, not latency. Lua still materializes whole logical-member sets, probes primaries until finding a survivor, and materializes/deletes all aliases when inactive; large lineages/alias sets remain scale-dependent server work
73
77
  - creating or rotating into a previously obsolete session ID removes that stale alias ownership first, so a reused ID cannot invoke an old logical-session meaning
74
- - `createAuthorizationTransaction` and `createExchangeCode` are upserts; `createSession` is create-only and duplicate session IDs throw `OidcVaultStoreConflictError` without changing the existing record or indexes
75
- - session metadata should be JSON-compatible for portability across store providers
78
+ - `createAuthorizationTransaction` and `createExchangeCode` are upserts; `createSession` is create-only and duplicate session IDs throw `OidcVaultStoreConflictError` without changing the existing record or indexes. Portable callers must create sessions with a fresh unused ID and handle `OidcVaultStoreConflictError`: reusing a live ID rejects here but replaces on memory/MongoDB, so only fresh-ID creation is portable
79
+ - portable data consists of strings, finite numbers, booleans, null, arrays, and plain objects; exclude cycles, custom prototypes/serializers, functions, symbols, and undefined properties. This applies to nested provider/user/metadata data, not only top-level metadata
80
+ - portable plain-object/array inputs are captured before asynchronous work, including rotation source reads and scoped deletion scans. Returned session containers are independent from caller input, including nested provider/user/metadata arrays; later caller mutation cannot change the committed portable value or eventual result
81
+ - compatibility: session ownership snapshots do not add a JSON round-trip. The existing JSON serializer still handles native values and `toJSON` hooks (and still rejects unsupported values such as BigInt/cycles). Opaque non-plain objects are not recursively copied and have no portable mutation-isolation guarantee; create/rotate results retain their native values while reads reflect persisted JSON
76
82
  - backchannel logout token JTI records are consumed only when `expiresAt` is finite and greater than the store clock at consume time
77
83
  - the optional `now` hook controls store-domain timestamps and testable JTI validation only; Redis server time is the authority for Redis key expiry and revocation-index cleanup
78
84
  - the client must implement `sendCommand(args)`; official standalone `redis` clients already do, and a Sentinel-wrapped master client or any adapter implementing `OidcVaultRedisClient` does too
79
85
 
86
+ ## Rotation Aliases And Lifetime
87
+
88
+ Rotation preserves the source logical ID when the next session omits it. Each old ID becomes a revocation alias for its immediate successor's logical ID and `expiresAt`. With `A -> B (expiresAt=T1) -> C (expiresAt=T2)`, A stops working at T1 even if T2 is later or absent; B uses C's deadline. Later rotations never extend earlier aliases. `getSession(A)` returns `null`: an alias is never a readable/authentication session.
89
+
90
+ With `A/L1 -> B/L1 -> C/L2`, the new B alias revokes L2; earlier A keeps L1 and its original deadline, never C. Redis does not eagerly retire L1 aliases merely because rotation moves its last member elsewhere (memory does); they can remain until expiry or explicit lineage cleanup. Use distinct logical IDs for unrelated login families rather than relying on retained handles disappearing on reuse.
91
+
92
+ Without successor `expiresAt`, Redis and memory impose no alias time limit; MongoDB uses a finite fallback (default five minutes). Non-expiring Redis sessions and their aliases can grow without a fixed retention or size bound. Use explicit session lifetimes for portable finite windows. After an alias expires, use the live ID or the appropriate logical/subject/provider-session deletion method; core refresh preserves session expiry.
93
+
94
+ ## Deletion, Counts And Concurrent Arrivals
95
+
96
+ Deleting a live public ID removes that record; deleting an unexpired alias revokes its logical lineage. Logical deletion and aliases have no issuer/client filter. Subject/provider-session object inputs match each supplied `issuer` and `clientId`; string inputs omit both filters and match across providers. A retained member in another scope keeps its unexpired aliases.
97
+
98
+ Bulk deletion makes one cursor traversal, rechecks scope in each atomic delete script, and can follow a rotated-away source to matching same-lineage successors. Counts sum primary sessions actually deleted by those scripts, including such successors; expired/missing records, stale membership repair and alias cleanup do not contribute. Duplicate cursor visits or overlapping revocations do not double-count an already deleted primary. A concurrent arrival may survive, and `0` is not proof that the scope is empty. An error can follow earlier committed deletions; the complete traversal is not one transaction. MongoDB instead repeats scoped deletion until an empty query and can count expired documents awaiting TTL cleanup; memory counts live deletions during a synchronous map scan. Vault deletion does not revoke outstanding stateless access tokens or call upstream logout.
99
+
80
100
  ## Redis Version And Topology
81
101
 
82
- - **Minimum Redis version: 6.2.** One-time authorization transactions and exchange codes are consumed with `GETDEL`, which is unavailable before Redis 6.2. Versions 6.2 and 7.2 are exercised in integration tests.
102
+ - **Supported minimum Redis version: 6.2.** Versions 6.2 and 7.2 are exercised in integration tests. One-time consumption now uses guarded cached Lua, including legacy consumes, so old calls cannot spend guarded records.
83
103
  - **Standalone Redis**: supported through the official `redis` `createClient(...)` client (`RedisClientType`). This is the tested topological default.
84
- - **Redis Sentinel**: supported by passing the underlying master client retrieved from a `redis.createSentinel(...)` sentinel — for example, the client returned by `await sentinel.acquire()`, or via `await sentinel.use(c => c)` patterns documented in the `redis` package. The bare `createSentinel(...)` root client does NOT satisfy this package's structural contract directly: its `sendCommand(isReadonly, args, options?)` requires an `isReadonly` first argument, while this store calls `sendCommand(args)`. Wrap the underlying master with an adapter conforming to `OidcVaultRedisClient` if you want to retain the Sentinel's connection management. Sentinel failover switches the underlying node, so vault state survives a failover subject to your Sentinel AOF/RDB durability settings.
104
+ - **Redis Sentinel**: use an underlying master client retrieved from `redis.createSentinel(...)` (e.g. acquire/use patterns documented by that driver) or an adapter conforming to `OidcVaultRedisClient`. The bare Sentinel root has `sendCommand(isReadonly, args, options?)`, not this store's `sendCommand(args)`. Caller/adapter owns master acquisition, reconnect/failover and release. Automated provider tests exercise standalone 6.2/7.2, not Sentinel failover; replication/persistence guarantees are deployment-owned, and losing unexpired replay reservations during failover weakens replay protection until their window expires.
85
105
  - **Redis Cluster**: **not supported.** Cluster-shaped official clients are rejected when the store is created. The package's atomic vault scripts touch several keys without a hash-slot routing adapter, so Cluster routing could send parts of one logical operation to different nodes. Add a Cluster routing/hash-slot adapter before enabling it.
86
106
 
87
107
  ## Client Lifecycle And Ownership
@@ -89,7 +109,7 @@ app.use(
89
109
  - **Connect the client yourself.** The store never calls `client.connect()`, `client.quit()`, or `client.disconnect()`. Pass an already-connected client and reuse it across requests.
90
110
  - **Own `error` listeners, reconnects, and shutdown.** The store reads and writes commands but does not attach `error` listeners, suppress client errors, or close the client. Always register a client `error` listener on production connections; an unhandled client error can crash the process.
91
111
  - **Graceful shutdown.** Draining `createSession`/`rotateSession`/`deleteSessionsBy*` in-flight calls, then `client.quit()`, is the supported shutdown order. The store holds no background timers, so no store-side teardown is required beyond verifying in-flight operations have settled.
92
- - **Concurrency.** `OidcVaultStoreProvider` methods are safe to call concurrently from one or more processes. Atomic operations are protected by Redis server-side scripts; concurrent duplicate ID creation, concurrent indexed revocation of overlapping indexes, concurrent rotation into the same target ID, and concurrent same-ID rotation conflicting with indexed revocation are all bounded.
112
+ - **Startup and concurrency.** There is no store `ready()` handshake: connect before accepting traffic, and provision permission to execute the documented commands and cached Lua scripts. Server-side scripts protect individual mutations from interleaving; indexed traversal and post-commit maintenance span multiple commands. They do not have a fixed total-work or completion-time bound.
93
113
 
94
114
  ## Key Namespace And Migration
95
115
 
@@ -107,25 +127,72 @@ app.use(
107
127
  ## Expiry And Index Cleanup
108
128
 
109
129
  - expiring session records are stored with Redis `PXAT`, and revocation index scores use the same absolute expiration timestamp
130
+ - authorization transactions and exchange codes also use `PXAT`. Legacy consumes and `getSession` preserve the key-TTL policy (no application-clock expiry check); legacy consumption uses one cached Lua call. The new transaction/code preflight getters compare payload expiry with Redis `TIME` (normally `GET` + `TIME`); guarded consumes compare payload expiry inside Lua with server time. They reject expired payloads even after externally altered TTLs
131
+ - **TTL integrity assumption:** preserve the store-written key TTLs and matching index scores when restoring or migrating data. Externally removing/extending a TTL can still expose past-dated payloads through legacy consumes or `getSession`; changing only a session TTL can leave it live after index pruning. New guarded operations additionally enforce payload expiry, but do not repair external index/TTL inconsistencies. Shortening a key TTL makes a record unavailable immediately even when payload expiry is later
110
132
  - stale revocation memberships are pruned with Redis `TIME`, not the application clock, so a skewed application clock cannot remove a still-live Redis session from subject, provider-session, or logical-session indexes
111
133
  - indexed revocation also checks the primary session key before deleting or returning a session; expired primary values are not returned and missing primary values remove the stale membership encountered during traversal
112
- - indexed revocation traverses Redis sorted sets with bounded cursor batches and batched record reads; it revokes sessions present in the cursor scan's view, while sessions added to the same index after the scan starts may be revoked by a later indexed revocation call
113
- - each successful session create or rotation scans up to 100 prefixed keys and removes Redis-expired members from subject, provider-session, and logical-session indexes; Redis deletes empty sorted sets, so unique expired logical-session index keys are eventually removed even without a logout for that logical session
114
- - rotated session aliases use the same expiry score as their successor session; non-expiring alias churn is bounded by cleanup on logical-session termination, and malformed stale aliases are deleted when their session ID is reused
115
- - stale index storage is therefore bounded by active session index entries plus expired entries waiting for Redis expiry and the incremental cleanup scan window; non-expiring sessions use a non-expiring index score and remain until revoked
134
+ - indexed revocation uses `ZSCAN COUNT 250` and one `MGET` for the IDs returned in each batch. `COUNT` is a hint, not a hard batch cap (compact sorted sets can return all members); the batch is not further chunked. Response memory and repair concurrency can grow with the returned batch, while deletion scripts run sequentially
135
+ - each successful session create or rotation attempts one `SCAN MATCH <prefix>:* COUNT 100` step, then `TYPE` and, for each recognized sorted-set index, `TIME`/`ZREMRANGEBYSCORE`. **COUNT 100 is a hint, not “up to 100 keys”**: Redis may return more or fewer keys, and the store processes the whole response. Cleanup includes subject, provider-session, logical-session and rotated-alias indexes. Redis removes empty sorted sets; continued successful writes advance the scan even without a logout for that logical session
136
+ - post-commit index maintenance is best-effort and never masquerades as a failed write: once the atomic create/rotate script commits, `createSession`/`rotateSession` resolve with the committed session even if the follow-up `SCAN`/`TYPE`/`TIME`/`ZREMRANGEBYSCORE` maintenance fails. The failure is reported once via a fixed-text `console.warn` identifying only `createSession` or `rotateSession`; adapter error names, messages, codes, causes, and arbitrary thrown values are never inspected or logged. The incremental scan retries on a later create/rotate. Mutation-script failures still reject, and a successful rotation is never retried merely because maintenance failed
137
+ - **Diagnostic compatibility note:** post-commit maintenance warnings no longer include a `Cause:` suffix or adapter error details, which may contain credentials. Log consumers should use the operation name and fixed maintenance-failure text
138
+ - aliases use their immediate successor's expiry score; malformed stale aliases are deleted when their session ID is reused. Alias/index cleanup can materialize a whole logical-member or alias set inside Lua; neither cursor COUNT value bounds that server work
139
+ - there is no fixed stale-index cleanup deadline or memory cap. Expired entries can remain while writes stop or maintenance fails; non-expiring entries and aliases remain until explicitly revoked/cleaned. The incremental scan is opportunistic, not a retention SLA
116
140
 
117
141
  ## Stored Data Validation And Corruption Handling
118
142
 
119
- - stored records are JSON payloads validated structurally on read; see _Stored Data Characteristics_ for the schema-migration and plaintext-storage posture
143
+ - stored records are JSON payloads validated structurally on read, with session `sessionId`, authorization transaction `state`, and exchange-code `code` required to match the requested key (including each indexed batch member); see _Stored Data Characteristics_ for the schema-migration and plaintext-storage posture
120
144
  - malformed one-time authorization transaction and exchange-code records are consumed atomically and return `null`, preventing reuse while failing closed
121
145
  - malformed session records are treated as unreadable, deleted when encountered through session reads or indexed revocation, and never returned as authenticated state
122
- - indexed revocation removes stale corrupt members and continues processing later valid sessions
146
+ - corruption repair deletes only the observed malformed payload through an atomic server-side compare-and-delete: a fresh same-ID record created after the stale read (session ID reuse is supported) is never destroyed by the repair, and the missing-session logout path applies the same guard instead of an unconditional delete
147
+ - **Identity-validation compatibility note:** shape-valid records stored under the wrong identity follow the malformed-record policy and return `null`. Session repair targets only the observed lookup key, never the embedded session ID; indexed membership repair retains the same generation guard. One-time corruption is removed inside the same atomic consume script, with no follow-up delete that could remove a replacement
148
+ - a stale repair observing corruption therefore preserves a concurrent replacement, while genuine logout of a live session (direct delete or scoped logical/subject/provider-session revocation) still deletes it; indexed revocation removes stale corrupt members and continues processing later valid sessions
123
149
  - Redis mutation scripts preflight expected key types before writing so wrong-type keys fail without deterministic partial mutation
124
- - validation errors and script errors are sanitized and do not include stored refresh tokens, ID tokens, authorization transactions, or exchange codes
150
+ - package-generated validation and deterministic script diagnostics use fixed text without stored records. Arbitrary client/adapter failures can still reject operations with sensitive original errors; they are not generally sanitized for application logs. Use fixed operation names and allowlisted categories, not raw errors, connection URLs, session IDs, state/PKCE values, exchange codes or token-valued metric labels. The secret-independent post-commit warning policy above applies specifically to that warning path
125
151
 
126
152
  ## Main Exports
127
153
 
154
+ 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.
155
+
128
156
  - `createRedisOidcVaultStore(...)`
129
157
  - `OidcVaultRedisStoreRecordError`
130
158
  - `type RedisOidcVaultStoreOptions`
131
159
  - `type OidcVaultRedisClient`
160
+
161
+ ## Device-Binding Store Capabilities
162
+
163
+ The factory returns core `OidcVaultDeviceBindingStoreProvider`, with detached live `getAuthorizationTransaction(state)` and `getExchangeCode(code)`, atomic `consumeAuthorizationTransactionIfMatches({ state, match })` and `consumeExchangeCodeIfMatches({ code, expectedSessionId, match })`, token-free `getSessionRevocationContext(sessionId)`, and `reserveDpopProof({ replayKey, expiresAt })`.
164
+
165
+ Import the stronger provider, match/consume/replay/revocation types and `OidcVaultDpopReplayCapacityError` from the **core package root**; the Redis package exports its factory/options/client and record error only. Either vault opt-in feature requires all six capabilities, while fingerprint-only flows do not reserve proofs. Use the same shared provider as API replayStore; the core shipped README includes complete DPoP issuer/API/browser usage.
166
+
167
+ `match` requires both `deviceBinding` and `browserBindingHash`. Non-null values match exactly; **null requires stored absence**, never BSON/JSON null or “ignore”. Legacy records omit both; guarded unbound records have a cookie hash; bound records have that hash plus `{ type: 'dpop', jkt }`. Hashes are canonical 43-character SHA-256 base64url. Null/malformed/extra binding fields and key-without-hash records are invalid; writes reject and corrupted data returns no credentials. No JWK, proof algorithm, or historical mode is stored.
168
+
169
+ Preflight getters are not locks. The consume script performs shape/identity/expiry, both matches, exchange-session match and deletion atomically; mismatch returns `null` without spending a live record. Matching races have one winner across independent Redis clients. **Legacy consumes now also use this script** and refuse guarded records, including cookie-only records, without spending them. There is no application-side get/unconditional-delete bypass. Malformed read cleanup retains the existing compare-and-delete generation guard.
170
+
171
+ Rotation inherits omitted binding, accepts the same key, and rejects null/malformed, changed, or newly added binding before mutation. Lua also rechecks source identity, credential generation and binding against the preflight source so same-ID replacement cannot make a new lineage inherit an old key or lose index ownership. Target-ID, alias, deletion and post-commit maintenance policies remain as documented above.
172
+
173
+ `getSessionRevocationContext` resolves a live handle or unexpired alias and all live members of its current lineage inside one Lua snapshot. A directly observed live handle always participates even if its reverse-index membership is missing, so peers cannot make that bound handle appear unbound. It returns only `logicalSessionId`, allowlisted `provider.issuer/clientId`, and optional original `deviceBinding`; no tokens/profile/metadata/arbitrary provider fields leave Redis. Alias payloads still contain only their logical ID: absence of an alias binding never means an unbound lineage. Malformed/inconsistent binding/provider authority throws a fixed diagnostic. Expired aliases/empty lineages return `null`, and `getSession(alias)` remains `null`. This call materializes the whole logical-member set and reads member primaries inside Lua; large lineages remain scale-dependent server work.
174
+
175
+ ## Per-Request DPoP Replay And Capacity
176
+
177
+ | Redis option | Default | Contract |
178
+ | ---------------------- | ------------ | ----------------------------------------------------------------------- |
179
+ | `client` | required | Connected standalone-shaped client/adapter; caller owns lifecycle |
180
+ | `keyPrefix` | `oidc-vault` | Shared vault/replay namespace; changing it is not migration |
181
+ | `dpopReplayMaxEntries` | `100000` | Positive safe integer, identical on clients sharing the prefix |
182
+ | `now` | `Date.now` | Store timestamps/backchannel preflight only; replay/TTL use server TIME |
183
+
184
+ ```ts
185
+ const storeProvider = createRedisOidcVaultStore({
186
+ client: redis,
187
+ keyPrefix: 'oidc-vault',
188
+ dpopReplayMaxEntries: 100_000,
189
+ });
190
+ ```
191
+
192
+ `dpopReplayMaxEntries` is a positive safe integer (default **100000**), shared per key prefix across clients. Every client sharing that prefix must configure the same limit. The opaque proof keys are members of **`<keyPrefix>:dpop-proofs`**, a sorted set whose scores are absolute expiry milliseconds. This set is both replay state and capacity/expiry accounting; there is no separate counter that can leak after individual key TTLs.
193
+
194
+ `reserveDpopProof` is normally **one cached `EVALSHA` round trip per proof**. Server `TIME` inside Lua owns the validity window; the optional application `now` hook cannot admit a stale proof or prune live reservations. A safe-integer future expiry is required, with remaining TTL **<=360000 ms**. Invalid/nonfinite/unsafe/fractional/expired (`<= server now`) or overlong input returns `false` without allocation. Duplicate-first `ZSCORE` rejects without extending expiry, even at capacity. Lua reclaims at most **64** expired members with `ZRANGEBYSCORE ... LIMIT 0 64`, plus the requested expired key, then atomically checks `ZCARD` and admits with `ZADD`. No session/alias scan, unbounded `ZREMRANGEBYSCORE`, or live-entry eviction occurs on replay admission.
195
+
196
+ The replay set expires at its latest retained score (`PEXPIREAT`), reclaiming it if traffic stops; this deadline never extends a member's replay window. Expired members awaiting bounded cleanup may conservatively occupy capacity. New keys at capacity throw root `OidcVaultDpopReplayCapacityError` from `@web-ts-toolkit/express-oidc-vault`, with no live eviction or acceptance without replay state. Wrong-type/client failures reject. `NOSCRIPT` recovery loads and retries once, adding two round trips to the normal call; the existing runner never retries an arbitrary committed mutation on transport failure.
197
+
198
+ Provision ACLs for cached scripts and their internal commands, including `TIME`, `TYPE`, `GET`, `DEL`, `ZSCORE`, `ZRANGEBYSCORE`, `ZREM`, `ZCARD`, `ZADD`, `ZREVRANGE` and `PEXPIREAT`. DPoP is a **per-request write**, unlike lower-volume backchannel JTIs (`SET NX PXAT`). Size capacity for unique proofs/second × validity window plus headroom (approved defaults: at most 70 seconds). Share identical namespaces/windows and synchronized clocks across instances; never release reservations after downstream failure, and retries need a fresh proof. Capacity/provider errors must map through orchestration to sanitized replay-unavailable and fail closed. Redis failover durability remains deployment-owned; these command/work bounds are not throughput/latency guarantees.
package/index.d.mts CHANGED
@@ -1,8 +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
3
  /**
4
4
  * Thrown when a stored Redis value cannot be parsed or fails structural
5
- * validation. The message never includes stored token or refresh values.
5
+ * validation (including identity mismatch with the requested key). The message
6
+ * never includes stored token or refresh values.
6
7
  * Malformed one-time records are consumed atomically and return `null`; they
7
8
  * fail closed without throwing this error. Malformed sessions are deleted when
8
9
  * encountered through reads or indexed revocation.
@@ -33,7 +34,7 @@ declare class OidcVaultRedisStoreRecordError extends Error {
33
34
  *
34
35
  * `sendCommand` is required because atomic session writes, rotations,
35
36
  * deletions, and one-time record consumption transit Redis
36
- * `EVAL`/`GETDEL`/`TYPE`/`TIME`/`ZRANGE`/`ZSCAN`/`MGET` commands through it.
37
+ * cached Lua/`TYPE`/`TIME`/`ZRANGE`/`ZSCAN`/`MGET` commands through it.
37
38
  * It was previously optional in this type but always enforced at runtime.
38
39
  */
39
40
  interface OidcVaultRedisClient {
@@ -58,6 +59,8 @@ interface OidcVaultRedisClient {
58
59
  * `disconnect()`). The package never calls `connect`, `quit`, or `disconnect`.
59
60
  */
60
61
  interface RedisOidcVaultStoreOptions {
62
+ /** Positive safe integer per shared keyPrefix; default 100000. Configure identically on all clients. */
63
+ dpopReplayMaxEntries?: number;
61
64
  /** Connected Redis client or compatible adapter implementing {@link OidcVaultRedisClient}. */
62
65
  client: OidcVaultRedisClient;
63
66
  /**
@@ -76,7 +79,7 @@ interface RedisOidcVaultStoreOptions {
76
79
  now?: () => number;
77
80
  }
78
81
  /**
79
- * Create an `OidcVaultStoreProvider` backed by a connected Redis client.
82
+ * Create an `OidcVaultDeviceBindingStoreProvider` backed by a connected Redis client.
80
83
  *
81
84
  * Pass an already-connected official `redis` standalone client (or any adapter
82
85
  * implementing {@link OidcVaultRedisClient}). For Redis Sentinel deployments,
@@ -85,12 +88,14 @@ interface RedisOidcVaultStoreOptions {
85
88
  * satisfy the structural contract. The store does not connect, disconnect, or
86
89
  * attach `error` listeners; it only issues commands.
87
90
  *
88
- * The package requires Redis 6.2 or later (it uses `GETDEL`). Redis Cluster is
89
- * not supported and is rejected here.
91
+ * The package supports Redis 6.2 or later. Redis Cluster is
92
+ * not supported and is rejected here. Guarded consumes and per-proof replay use
93
+ * cached atomic Lua; replay is shared per keyPrefix, uses server TIME, and never
94
+ * evicts live entries. All clients must use the same dpopReplayMaxEntries.
90
95
  *
91
96
  * @param options construction options; see {@link RedisOidcVaultStoreOptions}.
92
- * @returns a provider satisfying the core `OidcVaultStoreProvider` contract.
97
+ * @returns a provider satisfying the stronger `OidcVaultDeviceBindingStoreProvider` contract.
93
98
  */
94
- declare function createRedisOidcVaultStore(options: RedisOidcVaultStoreOptions): OidcVaultStoreProvider;
99
+ declare function createRedisOidcVaultStore(options: RedisOidcVaultStoreOptions): OidcVaultDeviceBindingStoreProvider;
95
100
 
96
101
  export { type OidcVaultRedisClient, OidcVaultRedisStoreRecordError, type RedisOidcVaultStoreOptions, createRedisOidcVaultStore };
package/index.d.ts CHANGED
@@ -1,8 +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
3
  /**
4
4
  * Thrown when a stored Redis value cannot be parsed or fails structural
5
- * validation. The message never includes stored token or refresh values.
5
+ * validation (including identity mismatch with the requested key). The message
6
+ * never includes stored token or refresh values.
6
7
  * Malformed one-time records are consumed atomically and return `null`; they
7
8
  * fail closed without throwing this error. Malformed sessions are deleted when
8
9
  * encountered through reads or indexed revocation.
@@ -33,7 +34,7 @@ declare class OidcVaultRedisStoreRecordError extends Error {
33
34
  *
34
35
  * `sendCommand` is required because atomic session writes, rotations,
35
36
  * deletions, and one-time record consumption transit Redis
36
- * `EVAL`/`GETDEL`/`TYPE`/`TIME`/`ZRANGE`/`ZSCAN`/`MGET` commands through it.
37
+ * cached Lua/`TYPE`/`TIME`/`ZRANGE`/`ZSCAN`/`MGET` commands through it.
37
38
  * It was previously optional in this type but always enforced at runtime.
38
39
  */
39
40
  interface OidcVaultRedisClient {
@@ -58,6 +59,8 @@ interface OidcVaultRedisClient {
58
59
  * `disconnect()`). The package never calls `connect`, `quit`, or `disconnect`.
59
60
  */
60
61
  interface RedisOidcVaultStoreOptions {
62
+ /** Positive safe integer per shared keyPrefix; default 100000. Configure identically on all clients. */
63
+ dpopReplayMaxEntries?: number;
61
64
  /** Connected Redis client or compatible adapter implementing {@link OidcVaultRedisClient}. */
62
65
  client: OidcVaultRedisClient;
63
66
  /**
@@ -76,7 +79,7 @@ interface RedisOidcVaultStoreOptions {
76
79
  now?: () => number;
77
80
  }
78
81
  /**
79
- * Create an `OidcVaultStoreProvider` backed by a connected Redis client.
82
+ * Create an `OidcVaultDeviceBindingStoreProvider` backed by a connected Redis client.
80
83
  *
81
84
  * Pass an already-connected official `redis` standalone client (or any adapter
82
85
  * implementing {@link OidcVaultRedisClient}). For Redis Sentinel deployments,
@@ -85,12 +88,14 @@ interface RedisOidcVaultStoreOptions {
85
88
  * satisfy the structural contract. The store does not connect, disconnect, or
86
89
  * attach `error` listeners; it only issues commands.
87
90
  *
88
- * The package requires Redis 6.2 or later (it uses `GETDEL`). Redis Cluster is
89
- * not supported and is rejected here.
91
+ * The package supports Redis 6.2 or later. Redis Cluster is
92
+ * not supported and is rejected here. Guarded consumes and per-proof replay use
93
+ * cached atomic Lua; replay is shared per keyPrefix, uses server TIME, and never
94
+ * evicts live entries. All clients must use the same dpopReplayMaxEntries.
90
95
  *
91
96
  * @param options construction options; see {@link RedisOidcVaultStoreOptions}.
92
- * @returns a provider satisfying the core `OidcVaultStoreProvider` contract.
97
+ * @returns a provider satisfying the stronger `OidcVaultDeviceBindingStoreProvider` contract.
93
98
  */
94
- declare function createRedisOidcVaultStore(options: RedisOidcVaultStoreOptions): OidcVaultStoreProvider;
99
+ declare function createRedisOidcVaultStore(options: RedisOidcVaultStoreOptions): OidcVaultDeviceBindingStoreProvider;
95
100
 
96
101
  export { type OidcVaultRedisClient, OidcVaultRedisStoreRecordError, type RedisOidcVaultStoreOptions, createRedisOidcVaultStore };