ioredis-toolkit 0.0.9 → 0.0.11
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/CHANGELOG.md +73 -0
- package/LICENSE +1 -1
- package/README.md +68 -1058
- package/dist/cache/cache.d.ts +30 -0
- package/dist/cache/cache.d.ts.map +1 -0
- package/dist/cache/cache.js +59 -0
- package/dist/cache/cache.js.map +1 -0
- package/dist/cache/config.d.ts +12 -0
- package/dist/cache/config.d.ts.map +1 -0
- package/dist/cache/config.js +13 -0
- package/dist/cache/config.js.map +1 -0
- package/dist/cache/types.d.ts +32 -0
- package/dist/cache/types.d.ts.map +1 -0
- package/dist/cache/types.js +5 -0
- package/dist/cache/types.js.map +1 -0
- package/dist/index.d.ts +43 -51
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -44
- package/dist/index.js.map +1 -0
- package/dist/lock/config.d.ts +12 -0
- package/dist/lock/config.d.ts.map +1 -0
- package/dist/lock/config.js +8 -0
- package/dist/lock/config.js.map +1 -0
- package/dist/lock/lock.d.ts +20 -0
- package/dist/lock/lock.d.ts.map +1 -0
- package/dist/lock/lock.js +44 -0
- package/dist/lock/lock.js.map +1 -0
- package/dist/lock/types.d.ts +19 -0
- package/dist/lock/types.d.ts.map +1 -0
- package/dist/lock/types.js +2 -0
- package/dist/lock/types.js.map +1 -0
- package/dist/modules-config.d.ts +3 -0
- package/dist/modules-config.d.ts.map +1 -0
- package/dist/modules-config.js +2 -0
- package/dist/modules-config.js.map +1 -0
- package/dist/pubsub/config.d.ts +11 -0
- package/dist/pubsub/config.d.ts.map +1 -0
- package/dist/pubsub/config.js +6 -0
- package/dist/pubsub/config.js.map +1 -0
- package/dist/pubsub/pubsub.d.ts +20 -0
- package/dist/pubsub/pubsub.d.ts.map +1 -0
- package/dist/pubsub/pubsub.js +54 -0
- package/dist/pubsub/pubsub.js.map +1 -0
- package/dist/pubsub/types.d.ts +24 -0
- package/dist/pubsub/types.d.ts.map +1 -0
- package/dist/pubsub/types.js +2 -0
- package/dist/pubsub/types.js.map +1 -0
- package/dist/rate-limit/config.d.ts +12 -0
- package/dist/rate-limit/config.d.ts.map +1 -0
- package/dist/rate-limit/config.js +6 -0
- package/dist/rate-limit/config.js.map +1 -0
- package/dist/rate-limit/rate-limiter.d.ts +18 -0
- package/dist/rate-limit/rate-limiter.d.ts.map +1 -0
- package/dist/rate-limit/rate-limiter.js +37 -0
- package/dist/rate-limit/rate-limiter.js.map +1 -0
- package/dist/rate-limit/types.d.ts +27 -0
- package/dist/rate-limit/types.d.ts.map +1 -0
- package/dist/rate-limit/types.js +2 -0
- package/dist/rate-limit/types.js.map +1 -0
- package/dist/redis/client-facade.d.ts +77 -0
- package/dist/redis/client-facade.d.ts.map +1 -0
- package/dist/redis/client-facade.js +102 -0
- package/dist/redis/client-facade.js.map +1 -0
- package/dist/redis/client.d.ts +10 -0
- package/dist/redis/client.d.ts.map +1 -0
- package/dist/redis/client.js +29 -0
- package/dist/redis/client.js.map +1 -0
- package/dist/redis/cluster.d.ts +7 -0
- package/dist/redis/cluster.d.ts.map +1 -0
- package/dist/redis/cluster.js +47 -0
- package/dist/redis/cluster.js.map +1 -0
- package/dist/redis/config.d.ts +39 -0
- package/dist/redis/config.d.ts.map +1 -0
- package/dist/redis/config.js +52 -0
- package/dist/redis/config.js.map +1 -0
- package/dist/redis/errors.d.ts +5 -0
- package/dist/redis/errors.d.ts.map +1 -0
- package/dist/redis/errors.js +5 -0
- package/dist/redis/errors.js.map +1 -0
- package/dist/redis/types.d.ts +135 -0
- package/dist/redis/types.d.ts.map +1 -0
- package/dist/redis/types.js +2 -0
- package/dist/redis/types.js.map +1 -0
- package/dist/redis/wrapper.d.ts +88 -0
- package/dist/redis/wrapper.d.ts.map +1 -0
- package/dist/redis/wrapper.js +206 -0
- package/dist/redis/wrapper.js.map +1 -0
- package/dist/session/config.d.ts +47 -0
- package/dist/session/config.d.ts.map +1 -0
- package/dist/session/config.js +101 -0
- package/dist/session/config.js.map +1 -0
- package/dist/session/cookie.d.ts +16 -0
- package/dist/session/cookie.d.ts.map +1 -0
- package/dist/session/cookie.js +28 -0
- package/dist/session/cookie.js.map +1 -0
- package/dist/session/errors.d.ts +56 -0
- package/dist/session/errors.d.ts.map +1 -0
- package/dist/session/errors.js +58 -0
- package/dist/session/errors.js.map +1 -0
- package/dist/session/factory.d.ts +21 -0
- package/dist/session/factory.d.ts.map +1 -0
- package/dist/session/factory.js +30 -0
- package/dist/session/factory.js.map +1 -0
- package/dist/session/health.d.ts +12 -0
- package/dist/session/health.d.ts.map +1 -0
- package/dist/session/health.js +23 -0
- package/dist/session/health.js.map +1 -0
- package/dist/session/keys.d.ts +23 -0
- package/dist/session/keys.d.ts.map +1 -0
- package/dist/session/keys.js +27 -0
- package/dist/session/keys.js.map +1 -0
- package/dist/session/manager.d.ts +34 -0
- package/dist/session/manager.d.ts.map +1 -0
- package/dist/session/manager.js +31 -0
- package/dist/session/manager.js.map +1 -0
- package/dist/session/metrics.d.ts +11 -0
- package/dist/session/metrics.d.ts.map +1 -0
- package/dist/session/metrics.js +10 -0
- package/dist/session/metrics.js.map +1 -0
- package/dist/session/repository.d.ts +49 -0
- package/dist/session/repository.d.ts.map +1 -0
- package/dist/session/repository.js +203 -0
- package/dist/session/repository.js.map +1 -0
- package/dist/session/revocation.d.ts +22 -0
- package/dist/session/revocation.d.ts.map +1 -0
- package/dist/session/revocation.js +41 -0
- package/dist/session/revocation.js.map +1 -0
- package/dist/session/script-sources.d.ts +11 -0
- package/dist/session/script-sources.d.ts.map +1 -0
- package/dist/session/script-sources.js +140 -0
- package/dist/session/script-sources.js.map +1 -0
- package/dist/session/scripts.d.ts +15 -0
- package/dist/session/scripts.d.ts.map +1 -0
- package/dist/session/scripts.js +41 -0
- package/dist/session/scripts.js.map +1 -0
- package/dist/session/serializer.d.ts +12 -0
- package/dist/session/serializer.d.ts.map +1 -0
- package/dist/session/serializer.js +77 -0
- package/dist/session/serializer.js.map +1 -0
- package/dist/session/service.d.ts +48 -0
- package/dist/session/service.d.ts.map +1 -0
- package/dist/session/service.js +235 -0
- package/dist/session/service.js.map +1 -0
- package/dist/session/token.d.ts +16 -0
- package/dist/session/token.d.ts.map +1 -0
- package/dist/session/token.js +32 -0
- package/dist/session/token.js.map +1 -0
- package/dist/session/types.d.ts +134 -0
- package/dist/session/types.d.ts.map +1 -0
- package/dist/session/types.js +2 -0
- package/dist/session/types.js.map +1 -0
- package/dist/streams/config.d.ts +12 -0
- package/dist/streams/config.d.ts.map +1 -0
- package/dist/streams/config.js +6 -0
- package/dist/streams/config.js.map +1 -0
- package/dist/streams/streams.d.ts +24 -0
- package/dist/streams/streams.d.ts.map +1 -0
- package/dist/streams/streams.js +55 -0
- package/dist/streams/streams.js.map +1 -0
- package/dist/streams/types.d.ts +32 -0
- package/dist/streams/types.d.ts.map +1 -0
- package/dist/streams/types.js +2 -0
- package/dist/streams/types.js.map +1 -0
- package/docs/ACCEPTANCE-REPORT.md +70 -0
- package/docs/ARCHITECTURE.md +61 -0
- package/docs/CAPACITY.md +33 -0
- package/docs/DEPLOYMENT.md +22 -0
- package/docs/README-API.md +15 -0
- package/docs/STATE-MACHINE.md +38 -0
- package/docs/TESTING.md +37 -0
- package/docs/THREAT-MODEL.md +23 -0
- package/docs/TYPE-SAFETY.md +34 -0
- package/docs/modules/cache/README.md +7 -0
- package/docs/modules/cache/usage.md +156 -0
- package/docs/modules/lock/README.md +7 -0
- package/docs/modules/lock/usage.md +105 -0
- package/docs/modules/pubsub/README.md +7 -0
- package/docs/modules/pubsub/usage.md +106 -0
- package/docs/modules/rate-limit/README.md +7 -0
- package/docs/modules/rate-limit/usage.md +100 -0
- package/docs/modules/sessions/README.md +7 -0
- package/docs/modules/sessions/usage.md +262 -0
- package/docs/modules/streams/README.md +7 -0
- package/docs/modules/streams/usage.md +141 -0
- package/package.json +50 -60
- package/src/scripts/cleanup-index.lua +4 -0
- package/src/scripts/conditional-update.lua +21 -0
- package/src/scripts/consume-session.lua +21 -0
- package/src/scripts/create-session.lua +28 -0
- package/src/scripts/delete.lua +2 -0
- package/src/scripts/destroy-user.lua +13 -0
- package/src/scripts/enforce-limit.lua +17 -0
- package/src/scripts/revoke-session.lua +13 -0
- package/src/scripts/rotate.lua +24 -0
- package/src/scripts/touch-session.lua +28 -0
- package/src/scripts/update-session.lua +18 -0
- package/dist/cache.d.ts +0 -796
- package/dist/cache.js +0 -1120
- package/dist/client.d.ts +0 -284
- package/dist/client.js +0 -1114
- package/dist/cluster-slot.d.ts +0 -4
- package/dist/cluster-slot.js +0 -31
- package/dist/cluster.d.ts +0 -79
- package/dist/cluster.js +0 -156
- package/dist/errors.d.ts +0 -30
- package/dist/errors.js +0 -63
- package/dist/health.d.ts +0 -180
- package/dist/health.js +0 -239
- package/dist/lock.d.ts +0 -248
- package/dist/lock.js +0 -397
- package/dist/logger.d.ts +0 -12
- package/dist/logger.js +0 -40
- package/dist/pubsub.d.ts +0 -423
- package/dist/pubsub.js +0 -537
- package/dist/ratelimiter.d.ts +0 -441
- package/dist/ratelimiter.js +0 -539
- package/dist/session/index.d.ts +0 -23
- package/dist/session/index.js +0 -16
- package/dist/session/revocation-store.d.ts +0 -176
- package/dist/session/revocation-store.js +0 -318
- package/dist/session/scripts/cleanup-index.lua +0 -21
- package/dist/session/scripts/conditional-update-encrypted.lua +0 -60
- package/dist/session/scripts/conditional-update.lua +0 -63
- package/dist/session/scripts/create.lua +0 -83
- package/dist/session/scripts/delete-by-user.lua +0 -29
- package/dist/session/scripts/delete.lua +0 -15
- package/dist/session/scripts/enforce-limit.lua +0 -38
- package/dist/session/scripts/revoke.lua +0 -61
- package/dist/session/scripts/rotate-encrypted.lua +0 -110
- package/dist/session/scripts/rotate.lua +0 -122
- package/dist/session/scripts/touch-encrypted.lua +0 -89
- package/dist/session/scripts/touch.lua +0 -72
- package/dist/session/scripts/validate.lua +0 -90
- package/dist/session/session-circuit-breaker.d.ts +0 -42
- package/dist/session/session-circuit-breaker.js +0 -129
- package/dist/session/session-config.d.ts +0 -335
- package/dist/session/session-config.js +0 -162
- package/dist/session/session-cookie.d.ts +0 -72
- package/dist/session/session-cookie.js +0 -101
- package/dist/session/session-encryption.d.ts +0 -87
- package/dist/session/session-encryption.js +0 -139
- package/dist/session/session-errors.d.ts +0 -85
- package/dist/session/session-errors.js +0 -145
- package/dist/session/session-health.d.ts +0 -38
- package/dist/session/session-health.js +0 -60
- package/dist/session/session-keys.d.ts +0 -51
- package/dist/session/session-keys.js +0 -113
- package/dist/session/session-manager.d.ts +0 -73
- package/dist/session/session-manager.js +0 -94
- package/dist/session/session-metrics.d.ts +0 -33
- package/dist/session/session-metrics.js +0 -112
- package/dist/session/session-repository.d.ts +0 -161
- package/dist/session/session-repository.js +0 -683
- package/dist/session/session-scripts.d.ts +0 -36
- package/dist/session/session-scripts.js +0 -130
- package/dist/session/session-serializer.d.ts +0 -42
- package/dist/session/session-serializer.js +0 -248
- package/dist/session/session-service.d.ts +0 -104
- package/dist/session/session-service.js +0 -611
- package/dist/session/session-token.d.ts +0 -38
- package/dist/session/session-token.js +0 -86
- package/dist/session/session-types.d.ts +0 -253
- package/dist/session/session-types.js +0 -16
- package/dist/types.d.ts +0 -924
- package/dist/types.js +0 -151
- package/dist/utils/deepmerge.d.ts +0 -9
- package/dist/utils/deepmerge.js +0 -61
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# Sessions Module — Complete Usage Guide
|
|
2
|
+
|
|
3
|
+
`SessionManager` is a framework-independent Redis-backed authentication-session subsystem. Raw session credentials are returned only at creation/rotation time and are not stored as plaintext Redis values.
|
|
4
|
+
|
|
5
|
+
## Setup with the unified client
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { createRedisClient } from 'ioredis-toolkit';
|
|
9
|
+
|
|
10
|
+
const redis = createRedisClient({
|
|
11
|
+
mode: 'standalone',
|
|
12
|
+
host: '127.0.0.1',
|
|
13
|
+
port: 6379,
|
|
14
|
+
sessions: {
|
|
15
|
+
enabled: true,
|
|
16
|
+
namespace: 'auth',
|
|
17
|
+
tokenBytes: 32,
|
|
18
|
+
ttl: 60 * 60 * 24 * 30,
|
|
19
|
+
rolling: true,
|
|
20
|
+
idleTimeout: 60 * 60 * 24 * 7,
|
|
21
|
+
absoluteTimeout: 60 * 60 * 24 * 30,
|
|
22
|
+
touchInterval: 60,
|
|
23
|
+
securityVersionEnabled: true,
|
|
24
|
+
maxSessionsPerUser: 20,
|
|
25
|
+
maxMetadataBytes: 8192,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const sessions = redis.sessions;
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
To supply a metrics sink or an encryption key manager alongside `createRedisClient()`, pass them as the second argument:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
const redis = createRedisClient(
|
|
36
|
+
{ mode: 'standalone', host: '127.0.0.1', port: 6379, sessions: { enabled: true } },
|
|
37
|
+
{ metrics: myMetricsSink, encryptionKeyManager: myKeyManager },
|
|
38
|
+
);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Sessions must be explicitly enabled. Creating the client does not require session initialization until `redis.sessions` is accessed.
|
|
42
|
+
|
|
43
|
+
## Configuration
|
|
44
|
+
|
|
45
|
+
| Option | Type | Default | Description |
|
|
46
|
+
|---|---|---:|---|
|
|
47
|
+
| `enabled` | `boolean` | `false` | Enables the session subsystem. |
|
|
48
|
+
| `namespace` | `string` | `app` | Session key namespace. |
|
|
49
|
+
| `tokenBytes` | `number` | `32` | Random secret bytes; minimum 32 (256 bits). |
|
|
50
|
+
| `ttl` | `number` | 30 days | Hard Redis/session lifetime in seconds. |
|
|
51
|
+
| `idleTimeout` | `number` | unset | Optional rolling inactivity lifetime. |
|
|
52
|
+
| `absoluteTimeout` | `number` | unset | Optional hard application lifetime. |
|
|
53
|
+
| `touchInterval` | `number` | 60 | Minimum interval between rolling touch writes. |
|
|
54
|
+
| `rolling` | `boolean` | `true` | Enables rolling idle expiration behavior. |
|
|
55
|
+
| `securityVersionEnabled` | `boolean` | `false` | Enables per-user security-version invalidation. |
|
|
56
|
+
| `maxSessionsPerUser` | `number` | 20 | Maximum configured indexed sessions per user. |
|
|
57
|
+
| `maxMetadataBytes` | `number` | 8192 | Maximum metadata JSON size. |
|
|
58
|
+
| `maxBatchSize` | `number` | 200 | Maximum bounded administrative batch. |
|
|
59
|
+
| `maxConcurrency` | `number` | 8 | Maximum bounded cross-slot/application fan-out. |
|
|
60
|
+
| `storeIpAddress` | `boolean` | false | Persists IP metadata when enabled. |
|
|
61
|
+
| `storeUserAgent` | `boolean` | false | Persists user-agent metadata when enabled. |
|
|
62
|
+
| `storeDeviceId` | `boolean` | false | Persists device ID metadata when enabled. |
|
|
63
|
+
| `encryption.enabled` | `boolean` | false | Enables authenticated encryption for serialized session values when a key manager is supplied. |
|
|
64
|
+
| `circuitBreaker` | object | see below | Reserved configuration, fully defaulted and validated like every other nested section. Not yet consulted by any repository/service code path — there is no circuit-breaker behavior wired up in this release. |
|
|
65
|
+
| `cookie` | object | secure defaults | Cookie serialization settings. |
|
|
66
|
+
|
|
67
|
+
`circuitBreaker` defaults to `{ enabled: false, failureThreshold: 5, resetTimeoutMs: 10_000, halfOpenMaxRequests: 1 }` and, like `encryption` and `cookie`, may be omitted entirely.
|
|
68
|
+
|
|
69
|
+
## `create(input)`
|
|
70
|
+
|
|
71
|
+
Creates a session and returns the raw opaque token exactly once.
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
const created = await sessions.create({
|
|
75
|
+
userId: 'user_123',
|
|
76
|
+
deviceId: 'device_1',
|
|
77
|
+
ipAddress: '203.0.113.10',
|
|
78
|
+
userAgent: 'Mozilla/5.0',
|
|
79
|
+
metadata: { loginMethod: 'password', rememberMe: true },
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Store this only in the client's secure cookie/session transport.
|
|
83
|
+
const token = created.token;
|
|
84
|
+
console.log(created.session.id, created.session.expiresAt);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The token is an opaque credential composed of a random JTI and random secret. The authoritative Redis session record does not contain the raw token.
|
|
88
|
+
|
|
89
|
+
## `validate(token)`
|
|
90
|
+
|
|
91
|
+
Returns a discriminated result instead of throwing for ordinary invalid-session conditions.
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
const result = await sessions.validate(request.cookies.session ?? '');
|
|
95
|
+
if (!result.valid) {
|
|
96
|
+
// reason may be not_found, expired, idle_timeout, absolute_timeout,
|
|
97
|
+
// revoked, consumed, or invalid.
|
|
98
|
+
return unauthorized();
|
|
99
|
+
}
|
|
100
|
+
const userId = result.session.userId;
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Infrastructure failures are different from an invalid credential and are surfaced as storage errors; applications should not convert infrastructure failures into successful authentication.
|
|
104
|
+
|
|
105
|
+
## `get(token)`
|
|
106
|
+
|
|
107
|
+
Gets an active authoritative session or throws a typed session error.
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
const session = await sessions.get(token);
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Use this when the caller wants exception-based control flow rather than the discriminated `validate()` result.
|
|
114
|
+
|
|
115
|
+
## `touch(token)`
|
|
116
|
+
|
|
117
|
+
Applies rolling idle-expiration rules. Touches are throttled by `touchInterval` and are bounded by the absolute expiration.
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
await sessions.touch(token);
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
It is usually appropriate to call this on authenticated requests without writing every request to Redis.
|
|
124
|
+
|
|
125
|
+
## `update(token, patch, expectedVersion?)`
|
|
126
|
+
|
|
127
|
+
Updates mutable metadata with optional optimistic concurrency.
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
const current = await sessions.get(token);
|
|
131
|
+
const updated = await sessions.update(token, {
|
|
132
|
+
metadata: { ...current.metadata, theme: 'dark' },
|
|
133
|
+
}, current.version);
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
If another writer changed the record first, the expected-version check prevents an unintended lost update.
|
|
137
|
+
|
|
138
|
+
## `rotate(token)`
|
|
139
|
+
|
|
140
|
+
Consumes the predecessor and creates a successor token atomically within the session's Redis hash-tag slot.
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
const rotated = await sessions.rotate(token);
|
|
144
|
+
// Replace the client cookie with rotated.token.
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
After successful rotation, the predecessor is no longer accepted. Concurrent rotation attempts must not produce multiple valid successors. The successor's rolling idle-timeout window preserves the duration originally configured when the session (or its earliest ancestor) was created, rather than being re-derived from the predecessor's most recent touch — so the idle window does not silently shrink across repeated rotations. If the predecessor is consumed but successor creation then fails, `rotate()` throws `SessionRotationError` rather than leaving the caller with an ambiguous storage error.
|
|
148
|
+
|
|
149
|
+
## `revoke(token)`
|
|
150
|
+
|
|
151
|
+
Marks a session unusable.
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
await sessions.revoke(token);
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Revocation is idempotent from the application's perspective. `revoke()` (and rotation's internal predecessor `consume()`) remove the session from the user's index immediately rather than only relying on `list()`'s lazy self-heal, so a revoked/rotated session stops counting toward `maxSessionsPerUser` right away instead of only once its tombstone expires.
|
|
158
|
+
|
|
159
|
+
## `destroy(token)`
|
|
160
|
+
|
|
161
|
+
Permanently removes the session record and derived indexes.
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
await sessions.destroy(token);
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Use `revoke()` when you need an explicit invalidation state; use `destroy()` when removing stored session data is preferable.
|
|
168
|
+
|
|
169
|
+
## `setSecurityVersion(userId, version)`
|
|
170
|
+
|
|
171
|
+
Sets a per-user security version when the feature is enabled. Sessions carrying an older version fail authentication.
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
await sessions.setSecurityVersion('user_123', 8);
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
A common password-reset/logout-all pattern is to increment the user's security version and then let old sessions fail validation.
|
|
178
|
+
|
|
179
|
+
## `revokeAll(userId, limit?)`
|
|
180
|
+
|
|
181
|
+
Revokes a bounded batch and returns `{ affected, remaining }` so large user indexes do not require loading every session into memory.
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
let page = await sessions.revokeAll('user_123', 200);
|
|
185
|
+
while (page.remaining > 0) {
|
|
186
|
+
page = await sessions.revokeAll('user_123', 200);
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Use bounded loops or background jobs for users with unusually large session counts.
|
|
191
|
+
|
|
192
|
+
## `list(userId, offset?, limit?)`
|
|
193
|
+
|
|
194
|
+
Returns a bounded page of authoritative sessions for an application user.
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
const sessionsForUser = await sessions.list('user_123', 0, 50);
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
This is an administrative/management operation, not an authentication primitive. A `limit` greater than the configured `maxBatchSize` throws `SessionLimitError` (a malformed `offset`/`limit` throws `SessionInvalidError`).
|
|
201
|
+
|
|
202
|
+
## Cookie helpers
|
|
203
|
+
|
|
204
|
+
The package also exposes cookie serializers:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
import { serializeCookie, serializeDeletionCookie } from 'ioredis-toolkit';
|
|
208
|
+
|
|
209
|
+
const header = serializeCookie(token, {
|
|
210
|
+
name: 'session',
|
|
211
|
+
httpOnly: true,
|
|
212
|
+
secure: true,
|
|
213
|
+
sameSite: 'lax',
|
|
214
|
+
path: '/',
|
|
215
|
+
maxAge: 60 * 60 * 24 * 30,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const clear = serializeDeletionCookie({ name: 'session', path: '/' });
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Cookie authentication requires a CSRF strategy appropriate to the application. `HttpOnly` protects against direct JavaScript reads but does not itself prevent CSRF.
|
|
222
|
+
|
|
223
|
+
## Security and topology model
|
|
224
|
+
|
|
225
|
+
Session records and per-user indexes use a user-derived Redis hash tag so operations concerning one user's sessions can be atomic within one Cluster slot. Operations spanning different users are not treated as cross-slot transactions.
|
|
226
|
+
|
|
227
|
+
Never log raw session tokens. Do not put them into metrics labels, traces, exception messages, or persistent application logs.
|
|
228
|
+
|
|
229
|
+
`RedisRevocationStore` is a separate, standalone utility for credentials whose authorization model is JTI-based (such as externally issued JWTs) — see [docs/README-API.md](../../README-API.md). It is intentionally **not** consulted by `SessionService`: this module's opaque sessions treat session state itself as the source of truth and deliberately avoid an extra revocation-key lookup on every `validate()` call.
|
|
230
|
+
|
|
231
|
+
## Errors
|
|
232
|
+
|
|
233
|
+
All typed errors extend `SessionError` and carry a stable `code`:
|
|
234
|
+
|
|
235
|
+
| Class | Code | Thrown by |
|
|
236
|
+
|---|---|---|
|
|
237
|
+
| `SessionNotFoundError` | `SESSION_NOT_FOUND` | `get()` when the token resolves to no record |
|
|
238
|
+
| `SessionExpiredError` | `SESSION_EXPIRED` | `get()`/`touch()`/`rotate()` on an expired/idle-timed-out session |
|
|
239
|
+
| `SessionRevokedError` | `SESSION_REVOKED` | `get()` on a revoked session |
|
|
240
|
+
| `SessionReplayError` | `SESSION_REPLAY` | `rotate()` on a non-active (already consumed/revoked) session |
|
|
241
|
+
| `SessionRotationError` | `SESSION_ROTATION` | `rotate()` when the predecessor was already consumed, cannot be rotated, or the successor could not be created after the predecessor was consumed |
|
|
242
|
+
| `SessionConflictError` | `SESSION_CONFLICT` | `update()` on a stale `expectedVersion` |
|
|
243
|
+
| `SessionLimitError` | `SESSION_LIMIT` | `list()` when `limit` exceeds `maxBatchSize` |
|
|
244
|
+
| `SessionInvalidError` | `SESSION_INVALID` | malformed input, or a session that fails an index/security-version check |
|
|
245
|
+
| `SessionStorageError` | `SESSION_STORAGE` | Redis-level read/write failures |
|
|
246
|
+
| `SessionSerializationError` | `SESSION_SERIALIZATION` | a record cannot be serialized, or fails schema/decryption validation on read |
|
|
247
|
+
| `SessionConfigurationError` | `SESSION_CONFIGURATION` | invalid session configuration, or constructing the manager while `enabled: false` |
|
|
248
|
+
|
|
249
|
+
`validate()` never throws for ordinary invalid-credential conditions — see the discriminated result above. These errors surface from `get()` and the other exception-based methods.
|
|
250
|
+
|
|
251
|
+
## Metrics
|
|
252
|
+
|
|
253
|
+
`SessionService` accepts an optional `metrics: SessionMetrics` sink (via `createSessionManager({ ..., metrics })` or `createRedisClient(config, { metrics })`) and defaults to a no-op implementation (`NoopMetrics`) when none is supplied. When configured, it receives `increment()` calls for `session.created`, `session.validate` (labeled `result: 'valid' | 'invalid'`, plus `reason` on the invalid path), `session.rotated`, `session.revoked`, and `session.destroyed`.
|
|
254
|
+
|
|
255
|
+
## Unified configuration overrides
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
redis.withSessions({ idleTimeout: 60 * 60 });
|
|
259
|
+
redis.withSessions({ namespace: 'auth:v2', rolling: false }, 'replace');
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
`merge` preserves existing normalized values; `replace` applies the supplied values on top of module defaults.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Streams module
|
|
2
|
+
|
|
3
|
+
This directory contains the complete usage documentation for the streams module.
|
|
4
|
+
|
|
5
|
+
**Full guide:** [usage.md](./usage.md)
|
|
6
|
+
|
|
7
|
+
The usage guide documents configuration, public types, every public method, arguments, return values, semantics, error/edge-case behavior, and multiple examples.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Streams Module — Complete Usage Guide
|
|
2
|
+
|
|
3
|
+
`RedisStreams` wraps Redis Streams for append, consumer-group creation, reads, acknowledgements, deletion, and length inspection.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { createRedisClient } from 'ioredis-toolkit';
|
|
9
|
+
|
|
10
|
+
const redis = createRedisClient({
|
|
11
|
+
mode: 'standalone',
|
|
12
|
+
host: '127.0.0.1',
|
|
13
|
+
port: 6379,
|
|
14
|
+
streams: { enabled: true, keyPrefix: 'myapp:stream', maxEntries: 100_000, blockMs: 5_000 },
|
|
15
|
+
});
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`streams.enabled` defaults to `false`. Accessing `redis.streams` while disabled throws `RedisConfigurationError`; set `enabled: true` to use the module.
|
|
19
|
+
|
|
20
|
+
## Configuration
|
|
21
|
+
|
|
22
|
+
| Option | Type | Default | Description |
|
|
23
|
+
|---|---|---:|---|
|
|
24
|
+
| `enabled` | `boolean` | `false` | Enables the module. `redis.streams` throws `RedisConfigurationError` while disabled; `new RedisStreams(...)` remains directly constructible either way. |
|
|
25
|
+
| `keyPrefix` | `string` | `stream` | Physical stream prefix. |
|
|
26
|
+
| `maxEntries` | `number` | `100000` | Approximate retained entry limit used by `add()`. |
|
|
27
|
+
| `blockMs` | `number` | `5000` | Default blocking duration applied to `read()` whenever a call does not pass its own `blockMs`. |
|
|
28
|
+
|
|
29
|
+
## `key(name)`
|
|
30
|
+
|
|
31
|
+
Builds the physical stream key.
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
redis.streams.key('orders'); // myapp:stream:orders
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## `add(name, fields, maxEntries?)`
|
|
38
|
+
|
|
39
|
+
Appends a record with Redis-generated `*` ID and approximate trimming.
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
const id = await redis.streams.add('orders', {
|
|
43
|
+
type: 'created',
|
|
44
|
+
orderId: 'ord_123',
|
|
45
|
+
userId: 'user_42',
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Override retention for a particular stream:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
await redis.streams.add('audit', { event: 'login' }, 1_000_000);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Fields are strings because Redis Streams are field/value strings. Serialize structured values explicitly.
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
await redis.streams.add('events', {
|
|
59
|
+
payload: JSON.stringify({ orderId: 'ord_1', amount: 25 }),
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## `createGroup(name, group, startId?, mkStream?)`
|
|
64
|
+
|
|
65
|
+
Creates a consumer group. `BUSYGROUP` is treated as idempotent success.
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
await redis.streams.createGroup('orders', 'workers', '0', true);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Using `$` starts at the end of the stream, while `0` allows existing entries to be delivered.
|
|
72
|
+
|
|
73
|
+
## `read(name, options?)`
|
|
74
|
+
|
|
75
|
+
Reads normal stream entries or consumer-group entries. Every read now blocks for `blockMs` (per-call, or the configured default when omitted) — pass a small `blockMs` (or `0`, which blocks indefinitely per Redis semantics) deliberately rather than relying on an implicit non-blocking read.
|
|
76
|
+
|
|
77
|
+
Normal read:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
const entries = await redis.streams.read('orders', {
|
|
81
|
+
id: '0-0',
|
|
82
|
+
count: 50,
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Consumer-group read:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
const entries = await redis.streams.read('orders', {
|
|
90
|
+
group: 'workers',
|
|
91
|
+
consumer: 'worker-1',
|
|
92
|
+
count: 20,
|
|
93
|
+
blockMs: 5000,
|
|
94
|
+
});
|
|
95
|
+
for (const entry of entries) {
|
|
96
|
+
console.log(entry.id, entry.fields);
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`StreamReadOptions`:
|
|
101
|
+
|
|
102
|
+
- `group`: consumer group name.
|
|
103
|
+
- `consumer`: consumer name; **required** when `group` is supplied — `read()` throws a `RangeError` if `group` is set without `consumer`, instead of silently falling back to a plain, incorrectly-cursored `XREAD`.
|
|
104
|
+
- `count`: maximum requested entries.
|
|
105
|
+
- `blockMs`: optional Redis blocking read duration; defaults to the module's configured `blockMs` when omitted.
|
|
106
|
+
- `id`: starting/continuation ID; group reads commonly use `>` for new messages.
|
|
107
|
+
|
|
108
|
+
## `ack(name, group, ...ids)`
|
|
109
|
+
|
|
110
|
+
Acknowledges processed consumer-group messages.
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
await redis.streams.ack('orders', 'workers', '1750000000000-0');
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## `delete(name, ...ids)`
|
|
117
|
+
|
|
118
|
+
Deletes specific entries.
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
await redis.streams.delete('orders', '1750000000000-0');
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## `length(name)`
|
|
125
|
+
|
|
126
|
+
Returns current stream length.
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
const size = await redis.streams.length('orders');
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Operational guidance
|
|
133
|
+
|
|
134
|
+
Consumer groups create pending-entry state. Production consumers should also implement a pending-entry recovery strategy using native Redis commands appropriate to their workload. This wrapper intentionally keeps its surface small and does not pretend that a single `read()` loop is a complete queue-processing framework.
|
|
135
|
+
|
|
136
|
+
## Overrides
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
redis.withStreams({ maxEntries: 500_000, blockMs: 10_000 });
|
|
140
|
+
redis.withStreams({ keyPrefix: 'critical-streams' }, 'replace');
|
|
141
|
+
```
|
package/package.json
CHANGED
|
@@ -1,50 +1,53 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ioredis-toolkit",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Production-grade
|
|
3
|
+
"version": "0.0.11",
|
|
4
|
+
"description": "Production-grade Redis sessions, cache, locks, rate limiting, Pub/Sub, and Streams for Node.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=22.0.0"
|
|
10
|
+
},
|
|
8
11
|
"exports": {
|
|
9
12
|
".": {
|
|
10
13
|
"types": "./dist/index.d.ts",
|
|
11
14
|
"import": "./dist/index.js"
|
|
12
15
|
},
|
|
13
|
-
"./
|
|
14
|
-
"types": "./dist/
|
|
15
|
-
"import": "./dist/
|
|
16
|
+
"./cache": {
|
|
17
|
+
"types": "./dist/cache/cache.d.ts",
|
|
18
|
+
"import": "./dist/cache/cache.js"
|
|
16
19
|
},
|
|
17
|
-
"./
|
|
18
|
-
"types": "./dist/
|
|
19
|
-
"import": "./dist/
|
|
20
|
+
"./lock": {
|
|
21
|
+
"types": "./dist/lock/lock.d.ts",
|
|
22
|
+
"import": "./dist/lock/lock.js"
|
|
20
23
|
},
|
|
21
|
-
"./
|
|
22
|
-
"types": "./dist/
|
|
23
|
-
"import": "./dist/
|
|
24
|
+
"./rate-limit": {
|
|
25
|
+
"types": "./dist/rate-limit/rate-limiter.d.ts",
|
|
26
|
+
"import": "./dist/rate-limit/rate-limiter.js"
|
|
24
27
|
},
|
|
25
28
|
"./pubsub": {
|
|
26
|
-
"types": "./dist/pubsub.d.ts",
|
|
27
|
-
"import": "./dist/pubsub.js"
|
|
29
|
+
"types": "./dist/pubsub/pubsub.d.ts",
|
|
30
|
+
"import": "./dist/pubsub/pubsub.js"
|
|
28
31
|
},
|
|
29
|
-
"./
|
|
30
|
-
"types": "./dist/
|
|
31
|
-
"import": "./dist/
|
|
32
|
-
},
|
|
33
|
-
"./health": {
|
|
34
|
-
"types": "./dist/health.d.ts",
|
|
35
|
-
"import": "./dist/health.js"
|
|
32
|
+
"./streams": {
|
|
33
|
+
"types": "./dist/streams/streams.d.ts",
|
|
34
|
+
"import": "./dist/streams/streams.js"
|
|
36
35
|
},
|
|
37
|
-
"./
|
|
38
|
-
"types": "./dist/
|
|
39
|
-
"import": "./dist/
|
|
36
|
+
"./sessions": {
|
|
37
|
+
"types": "./dist/session/manager.d.ts",
|
|
38
|
+
"import": "./dist/session/manager.js"
|
|
40
39
|
},
|
|
41
|
-
"./
|
|
42
|
-
"types": "./dist/session/index.d.ts",
|
|
43
|
-
"import": "./dist/session/index.js"
|
|
44
|
-
}
|
|
40
|
+
"./package.json": "./package.json"
|
|
45
41
|
},
|
|
42
|
+
"files": [
|
|
43
|
+
"dist",
|
|
44
|
+
"src/scripts",
|
|
45
|
+
"docs",
|
|
46
|
+
"LICENSE",
|
|
47
|
+
"README.md",
|
|
48
|
+
"CHANGELOG.md"
|
|
49
|
+
],
|
|
46
50
|
"author": "Anwar <anwarkamal1434@gmail.com>",
|
|
47
|
-
"license": "MIT",
|
|
48
51
|
"repository": {
|
|
49
52
|
"type": "git",
|
|
50
53
|
"url": "https://github.com/org-utils/ioredis-toolkit.git"
|
|
@@ -58,49 +61,36 @@
|
|
|
58
61
|
"registry": "https://registry.npmjs.org/"
|
|
59
62
|
},
|
|
60
63
|
"scripts": {
|
|
61
|
-
"build": "tsc
|
|
62
|
-
"
|
|
64
|
+
"build": "tsc -p tsconfig.json",
|
|
65
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
66
|
+
"lint": "eslint .",
|
|
63
67
|
"test": "vitest run",
|
|
64
68
|
"test:watch": "vitest",
|
|
65
|
-
"
|
|
69
|
+
"test:integration": "vitest run --config vitest.integration.config.ts",
|
|
66
70
|
"test:coverage": "vitest run --coverage",
|
|
67
|
-
"
|
|
68
|
-
"clean:git": "git clean -xdf",
|
|
71
|
+
"clean": "rm -rf dist",
|
|
69
72
|
"changeset:init": "changeset init",
|
|
70
73
|
"changeset": "changeset",
|
|
71
74
|
"version": "changeset version",
|
|
72
75
|
"publish": "changeset publish",
|
|
73
76
|
"release": "bun build && changeset publish",
|
|
74
77
|
"prebuild": "bun typecheck",
|
|
75
|
-
"
|
|
78
|
+
"prepublishOnly": "npm run clean && npm run build && npm test"
|
|
76
79
|
},
|
|
77
|
-
"keywords": [
|
|
78
|
-
"redis",
|
|
79
|
-
"cluster",
|
|
80
|
-
"sentinel",
|
|
81
|
-
"distributed",
|
|
82
|
-
"cache",
|
|
83
|
-
"pubsub",
|
|
84
|
-
"locking"
|
|
85
|
-
],
|
|
86
80
|
"dependencies": {
|
|
87
|
-
"ioredis": "^5.
|
|
88
|
-
"zod": "^4.
|
|
89
|
-
},
|
|
90
|
-
"engines": {
|
|
91
|
-
"node": ">=22.0.0"
|
|
81
|
+
"ioredis": "^5.6.1",
|
|
82
|
+
"zod": "^4.1.5"
|
|
92
83
|
},
|
|
93
|
-
"files": [
|
|
94
|
-
"dist",
|
|
95
|
-
"README.md",
|
|
96
|
-
"LICENSE"
|
|
97
|
-
],
|
|
98
84
|
"devDependencies": {
|
|
99
|
-
"@changesets/changelog-github": "^0.
|
|
100
|
-
"@changesets/cli": "^
|
|
101
|
-
"@types/node": "^
|
|
102
|
-
"
|
|
103
|
-
"
|
|
104
|
-
"
|
|
105
|
-
|
|
85
|
+
"@changesets/changelog-github": "^1.0.1",
|
|
86
|
+
"@changesets/cli": "^3.0.2",
|
|
87
|
+
"@types/node": "^24.3.0",
|
|
88
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
89
|
+
"eslint": "^9.34.0",
|
|
90
|
+
"typescript": "^5.9.2",
|
|
91
|
+
"typescript-eslint": "^8.42.0",
|
|
92
|
+
"vitest": "^3.2.4"
|
|
93
|
+
},
|
|
94
|
+
"license": "MIT",
|
|
95
|
+
"sideEffects": false
|
|
106
96
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
-- Reference copy only. This duplicates update-session.lua: both must mirror the
|
|
2
|
+
-- authoritative script actually executed at runtime, SCRIPT_SOURCES.update_session
|
|
3
|
+
-- in src/session/script-sources.ts. Do not use this script directly.
|
|
4
|
+
-- KEYS[1] session key
|
|
5
|
+
-- ARGV[1] expected version
|
|
6
|
+
-- ARGV[2] now
|
|
7
|
+
-- ARGV[3] serialized replacement
|
|
8
|
+
local raw = redis.call('GET', KEYS[1])
|
|
9
|
+
if not raw then return {0} end
|
|
10
|
+
local ok, s = pcall(cjson.decode, raw)
|
|
11
|
+
if not ok or not s.data then return {-4} end
|
|
12
|
+
if s.data.status ~= 'active' then return {-3} end
|
|
13
|
+
if tonumber(s.data.version) ~= tonumber(ARGV[1]) then return {-2, s.data.version} end
|
|
14
|
+
local replacement = ARGV[3]
|
|
15
|
+
local ok2, parsed = pcall(cjson.decode, replacement)
|
|
16
|
+
if not ok2 or not parsed.data then return {-4} end
|
|
17
|
+
if parsed.data.userId ~= s.data.userId or parsed.data.jti ~= s.data.jti or parsed.data.id ~= s.data.id or parsed.data.createdAt ~= s.data.createdAt or parsed.data.absoluteExpiresAt ~= s.data.absoluteExpiresAt then return {-5} end
|
|
18
|
+
local ttl = tonumber(s.data.expiresAt) - tonumber(ARGV[2])
|
|
19
|
+
if ttl <= 0 then return {-1} end
|
|
20
|
+
redis.call('SET', KEYS[1], replacement, 'XX', 'EX', ttl)
|
|
21
|
+
return {1}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
-- KEYS[1] session key
|
|
2
|
+
-- ARGV[1] now epoch seconds
|
|
3
|
+
-- ARGV[2] tombstone ttl seconds
|
|
4
|
+
local raw = redis.call('GET', KEYS[1])
|
|
5
|
+
if not raw then return {0} end
|
|
6
|
+
local ok, s = pcall(cjson.decode, raw)
|
|
7
|
+
if not ok or not s.data then return {-4} end
|
|
8
|
+
local r = s.data
|
|
9
|
+
if r.status == 'consumed' then return {-1} end
|
|
10
|
+
if r.status == 'revoked' then return {-3} end
|
|
11
|
+
local now = tonumber(ARGV[1])
|
|
12
|
+
if tonumber(r.expiresAt) <= now then return {-2} end
|
|
13
|
+
if r.absoluteExpiresAt and r.absoluteExpiresAt ~= cjson.null and tonumber(r.absoluteExpiresAt) > 0 and tonumber(r.absoluteExpiresAt) <= now then return {-2} end
|
|
14
|
+
if r.idleExpiresAt and tonumber(r.idleExpiresAt) > 0 and tonumber(r.idleExpiresAt) <= now then return {-2} end
|
|
15
|
+
r.status = 'consumed'
|
|
16
|
+
r.consumedAt = now
|
|
17
|
+
r.version = tonumber(r.version) + 1
|
|
18
|
+
local ttl = tonumber(ARGV[2])
|
|
19
|
+
local encoded = cjson.encode({v=1, data=r})
|
|
20
|
+
redis.call('SET', KEYS[1], encoded, 'XX', 'EX', ttl)
|
|
21
|
+
return {1, r.userId, r.jti, r.version}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
-- Reference copy only. The authoritative script actually executed at runtime is
|
|
2
|
+
-- SCRIPT_SOURCES.create_session in src/session/script-sources.ts — keep this file in sync with it.
|
|
3
|
+
-- KEYS[1] session key
|
|
4
|
+
-- KEYS[2] user index
|
|
5
|
+
-- ARGV[1] serialized record
|
|
6
|
+
-- ARGV[2] ttl seconds
|
|
7
|
+
-- ARGV[3] createdAt score
|
|
8
|
+
-- ARGV[4] tokenHash
|
|
9
|
+
-- ARGV[5] max sessions (0 = unlimited)
|
|
10
|
+
-- ARGV[6] namespace
|
|
11
|
+
-- ARGV[7] userTag
|
|
12
|
+
local created = redis.call('SET', KEYS[1], ARGV[1], 'NX', 'EX', ARGV[2])
|
|
13
|
+
if not created then return {-1} end
|
|
14
|
+
redis.call('ZADD', KEYS[2], ARGV[3], ARGV[4])
|
|
15
|
+
if tonumber(ARGV[5]) > 0 then
|
|
16
|
+
local count = redis.call('ZCARD', KEYS[2])
|
|
17
|
+
if count > tonumber(ARGV[5]) then
|
|
18
|
+
local excess = count - tonumber(ARGV[5])
|
|
19
|
+
local old = redis.call('ZRANGE', KEYS[2], 0, excess - 1)
|
|
20
|
+
for _, evictedHash in ipairs(old) do
|
|
21
|
+
redis.call('ZREM', KEYS[2], evictedHash)
|
|
22
|
+
redis.call('DEL', ARGV[6] .. ':session:{' .. ARGV[7] .. '}:' .. evictedHash)
|
|
23
|
+
redis.call('DEL', ARGV[6] .. ':token-index:' .. evictedHash)
|
|
24
|
+
end
|
|
25
|
+
return {1, excess}
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
return {1}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
-- Reference copy only. Not present in SCRIPT_SOURCES and not executed by the
|
|
2
|
+
-- repository at runtime (SessionRepository.revokeAll implements the equivalent
|
|
3
|
+
-- operation in TypeScript via a pipeline). Bounded helper: the application must
|
|
4
|
+
-- pass session keys to delete as KEYS, matching enforce-limit.lua's pattern.
|
|
5
|
+
-- KEYS[1] user index; KEYS[2..N] session keys corresponding to the returned members.
|
|
6
|
+
-- ARGV[1] max batch
|
|
7
|
+
local n = tonumber(ARGV[1])
|
|
8
|
+
local members = redis.call('ZRANGE', KEYS[1], 0, n - 1)
|
|
9
|
+
for i, tokenHash in ipairs(members) do
|
|
10
|
+
redis.call('ZREM', KEYS[1], tokenHash)
|
|
11
|
+
redis.call('DEL', KEYS[i + 1])
|
|
12
|
+
end
|
|
13
|
+
return members
|