ioredis-toolkit 0.0.9 → 0.5.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/CHANGELOG.md +67 -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,49 @@
|
|
|
1
|
+
import type { RedisClientWrapper } from '../redis/wrapper.js';
|
|
2
|
+
import type { SessionConfig } from './config.js';
|
|
3
|
+
import type { SessionKeyStrategy } from './keys.js';
|
|
4
|
+
import type { SessionRecord } from './types.js';
|
|
5
|
+
import { SessionScriptRegistry } from './scripts.js';
|
|
6
|
+
import { SessionSerializer } from './serializer.js';
|
|
7
|
+
/** Dependencies required by the session repository. */
|
|
8
|
+
export interface SessionRepositoryOptions {
|
|
9
|
+
redis: RedisClientWrapper;
|
|
10
|
+
keys: SessionKeyStrategy;
|
|
11
|
+
serializer: SessionSerializer;
|
|
12
|
+
scripts: SessionScriptRegistry;
|
|
13
|
+
config: SessionConfig;
|
|
14
|
+
}
|
|
15
|
+
/** Persistence boundary for authoritative session state and derived indexes. */
|
|
16
|
+
export declare class SessionRepository {
|
|
17
|
+
private readonly o;
|
|
18
|
+
/** Creates the repository using shared Redis infrastructure and versioned serializers/scripts. */
|
|
19
|
+
constructor(o: SessionRepositoryOptions);
|
|
20
|
+
/** Persists a session and its derived indexes using the repository's authoritative write semantics. */
|
|
21
|
+
create(record: SessionRecord): Promise<void>;
|
|
22
|
+
/** Resolves a token hash through the secondary locator and then reads the authoritative session record. */
|
|
23
|
+
resolve(tokenHash: string): Promise<SessionRecord | null>;
|
|
24
|
+
/** Persists the current security version for a user. */
|
|
25
|
+
setSecurityVersion(userId: string, version: number): Promise<void>;
|
|
26
|
+
/** Reads a user's security version. */
|
|
27
|
+
getSecurityVersion(userId: string): Promise<string | null>;
|
|
28
|
+
/** Checks whether the session remains present in the user's derived index. */
|
|
29
|
+
isIndexed(record: SessionRecord): Promise<boolean>;
|
|
30
|
+
/** Atomically updates rolling idle expiration when the touch interval permits it. */
|
|
31
|
+
touch(record: SessionRecord, now: number): Promise<'updated' | 'skipped' | 'invalid'>;
|
|
32
|
+
/** Atomically consumes a predecessor session for rotation. */
|
|
33
|
+
consume(record: SessionRecord, now: number, tombstoneTtl: number): Promise<void>;
|
|
34
|
+
/** Applies a version-checked replacement to an authoritative session record. */
|
|
35
|
+
update(record: SessionRecord, replacement: SessionRecord, expectedVersion: number, now: number): Promise<void>;
|
|
36
|
+
/** Atomically marks a session revoked. */
|
|
37
|
+
revoke(record: SessionRecord, now: number, tombstoneTtl: number): Promise<void>;
|
|
38
|
+
/** Removes the authoritative record and derived index entries. */
|
|
39
|
+
destroy(record: SessionRecord): Promise<void>;
|
|
40
|
+
/** Loads a bounded page of sessions and cleans stale derived-index members. */
|
|
41
|
+
list(userId: string, offset: number, limit: number): Promise<SessionRecord[]>;
|
|
42
|
+
/** Revokes a bounded batch and returns the number of indexed sessions remaining. */
|
|
43
|
+
revokeAll(userId: string, limit: number): Promise<{
|
|
44
|
+
affected: number;
|
|
45
|
+
remaining: number;
|
|
46
|
+
}>;
|
|
47
|
+
private safeDel;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=repository.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repository.d.ts","sourceRoot":"","sources":["../../src/session/repository.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAE9D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEpD,uDAAuD;AACvD,MAAM,WAAW,wBAAwB;IAAG,KAAK,EAAE,kBAAkB,CAAC;IAAC,IAAI,EAAE,kBAAkB,CAAC;IAAC,UAAU,EAAE,iBAAiB,CAAC;IAAC,OAAO,EAAE,qBAAqB,CAAC;IAAC,MAAM,EAAE,aAAa,CAAC;CAAE;AAExL,gFAAgF;AAChF,qBAAa,iBAAiB;IAEhB,OAAO,CAAC,QAAQ,CAAC,CAAC;IAD9B,kGAAkG;gBACrE,CAAC,EAAE,wBAAwB;IAExD,uGAAuG;IACjG,MAAM,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAmBlD,2GAA2G;IACrG,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAa/D,wDAAwD;IAClD,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKxE,uCAAuC;IACjC,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAKhE,8EAA8E;IACxE,SAAS,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC;IAKxD,qFAAqF;IAC/E,KAAK,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;IAW3F,8DAA8D;IACxD,OAAO,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAStF,gFAAgF;IAC1E,MAAM,CAAC,MAAM,EAAE,aAAa,EAAE,WAAW,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOpH,0CAA0C;IACpC,MAAM,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQrF,kEAAkE;IAC5D,OAAO,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAQnD,+EAA+E;IACzE,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC;IAenF,oFAAoF;IAC9E,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;YAkBlF,OAAO;CACtB"}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { SessionRotationError, SessionStorageError } from './errors.js';
|
|
2
|
+
import { SessionScriptRegistry } from './scripts.js';
|
|
3
|
+
import { SessionSerializer } from './serializer.js';
|
|
4
|
+
/** Persistence boundary for authoritative session state and derived indexes. */
|
|
5
|
+
export class SessionRepository {
|
|
6
|
+
o;
|
|
7
|
+
/** Creates the repository using shared Redis infrastructure and versioned serializers/scripts. */
|
|
8
|
+
constructor(o) {
|
|
9
|
+
this.o = o;
|
|
10
|
+
}
|
|
11
|
+
/** Persists a session and its derived indexes using the repository's authoritative write semantics. */
|
|
12
|
+
async create(record) {
|
|
13
|
+
const ttl = Math.max(1, Math.ceil(record.expiresAt - record.createdAt));
|
|
14
|
+
const result = await this.o.scripts.eval('create_session', [this.o.keys.session(record.userId, record.id), this.o.keys.userIndex(record.userId)], [this.o.serializer.serialize(record), String(ttl), String(record.createdAt), record.id, String(this.o.config.maxSessionsPerUser), this.o.keys.getNamespace(), this.o.keys.userTag(record.userId)]);
|
|
15
|
+
const code = Number(result[0]);
|
|
16
|
+
if (code !== 1)
|
|
17
|
+
throw new SessionStorageError('Session creation failed');
|
|
18
|
+
let indexed;
|
|
19
|
+
try {
|
|
20
|
+
indexed = await this.o.redis.set(this.o.keys.tokenIndex(record.id), this.o.keys.userTag(record.userId), 'EX', String(ttl), 'NX');
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
throw new SessionStorageError('Session token index write failed', error);
|
|
24
|
+
}
|
|
25
|
+
if (indexed !== 'OK') {
|
|
26
|
+
await this.safeDel(this.o.keys.session(record.userId, record.id));
|
|
27
|
+
try {
|
|
28
|
+
await this.o.redis.zrem(this.o.keys.userIndex(record.userId), record.id);
|
|
29
|
+
}
|
|
30
|
+
catch { /* best effort */ }
|
|
31
|
+
throw new SessionStorageError('Session token index already occupied');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Resolves a token hash through the secondary locator and then reads the authoritative session record. */
|
|
35
|
+
async resolve(tokenHash) {
|
|
36
|
+
let userTag;
|
|
37
|
+
try {
|
|
38
|
+
userTag = await this.o.redis.get(this.o.keys.tokenIndex(tokenHash));
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
throw new SessionStorageError('Session token index unavailable', error);
|
|
42
|
+
}
|
|
43
|
+
if (!userTag)
|
|
44
|
+
return null;
|
|
45
|
+
let raw;
|
|
46
|
+
try {
|
|
47
|
+
raw = await this.o.redis.get(this.o.keys.sessionByTag(userTag, tokenHash));
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
throw new SessionStorageError('Session record unavailable', error);
|
|
51
|
+
}
|
|
52
|
+
if (!raw) {
|
|
53
|
+
await this.safeDel(this.o.keys.tokenIndex(tokenHash));
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
return this.o.serializer.deserialize(raw);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
await this.safeDel(this.o.keys.sessionByTag(userTag, tokenHash));
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** Persists the current security version for a user. */
|
|
65
|
+
async setSecurityVersion(userId, version) {
|
|
66
|
+
try {
|
|
67
|
+
await this.o.redis.set(this.o.keys.securityVersion(userId), String(version));
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
throw new SessionStorageError('Security version update failed', error);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** Reads a user's security version. */
|
|
74
|
+
async getSecurityVersion(userId) {
|
|
75
|
+
try {
|
|
76
|
+
return await this.o.redis.get(this.o.keys.securityVersion(userId));
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
throw new SessionStorageError('Security version unavailable', error);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** Checks whether the session remains present in the user's derived index. */
|
|
83
|
+
async isIndexed(record) {
|
|
84
|
+
try {
|
|
85
|
+
return (await this.o.redis.zscore(this.o.keys.userIndex(record.userId), record.id)) !== null;
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
throw new SessionStorageError('Session index unavailable', error);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** Atomically updates rolling idle expiration when the touch interval permits it. */
|
|
92
|
+
async touch(record, now) {
|
|
93
|
+
const idleDuration = record.idleTimeoutSeconds ?? this.o.config.idleTimeout ?? this.o.config.ttl;
|
|
94
|
+
const idle = record.idleExpiresAt === null ? 0 : Math.min(record.absoluteExpiresAt ?? Number.MAX_SAFE_INTEGER, now + idleDuration);
|
|
95
|
+
const absolute = record.absoluteExpiresAt ?? 0;
|
|
96
|
+
const result = await this.o.scripts.eval('touch_session', [this.o.keys.session(record.userId, record.id)], [String(now), String(this.o.config.touchInterval), String(idle), String(absolute)]);
|
|
97
|
+
const code = Number(result[0]);
|
|
98
|
+
if (code === 1)
|
|
99
|
+
return 'updated';
|
|
100
|
+
if (code === 2)
|
|
101
|
+
return 'skipped';
|
|
102
|
+
return 'invalid';
|
|
103
|
+
}
|
|
104
|
+
/** Atomically consumes a predecessor session for rotation. */
|
|
105
|
+
async consume(record, now, tombstoneTtl) {
|
|
106
|
+
const result = await this.o.scripts.eval('consume_session', [this.o.keys.session(record.userId, record.id)], [String(now), String(Math.max(1, tombstoneTtl))]);
|
|
107
|
+
const code = Number(result[0]);
|
|
108
|
+
if (code === -1)
|
|
109
|
+
throw new SessionRotationError('Session already consumed');
|
|
110
|
+
if (code !== 1)
|
|
111
|
+
throw new SessionRotationError('Session cannot be rotated');
|
|
112
|
+
try {
|
|
113
|
+
await this.o.redis.zrem(this.o.keys.userIndex(record.userId), record.id);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
throw new SessionStorageError('Session index cleanup failed', error);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/** Applies a version-checked replacement to an authoritative session record. */
|
|
120
|
+
async update(record, replacement, expectedVersion, now) {
|
|
121
|
+
const result = await this.o.scripts.eval('update_session', [this.o.keys.session(record.userId, record.id)], [String(expectedVersion), String(now), this.o.serializer.serialize(replacement)]);
|
|
122
|
+
const code = Number(result[0]);
|
|
123
|
+
if (code === -2)
|
|
124
|
+
throw new SessionStorageError('Session version conflict');
|
|
125
|
+
if (code !== 1)
|
|
126
|
+
throw new SessionStorageError('Session update failed');
|
|
127
|
+
}
|
|
128
|
+
/** Atomically marks a session revoked. */
|
|
129
|
+
async revoke(record, now, tombstoneTtl) {
|
|
130
|
+
const result = await this.o.scripts.eval('revoke_session', [this.o.keys.session(record.userId, record.id)], [String(now), String(Math.max(0, tombstoneTtl))]);
|
|
131
|
+
const code = Number(result[0]);
|
|
132
|
+
if (code !== 1 && code !== 2 && code !== 0)
|
|
133
|
+
throw new SessionStorageError('Session revocation failed');
|
|
134
|
+
try {
|
|
135
|
+
await this.o.redis.zrem(this.o.keys.userIndex(record.userId), record.id);
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
throw new SessionStorageError('Session index cleanup failed', error);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/** Removes the authoritative record and derived index entries. */
|
|
142
|
+
async destroy(record) {
|
|
143
|
+
try {
|
|
144
|
+
await this.o.redis.del(this.o.keys.session(record.userId, record.id));
|
|
145
|
+
await this.o.redis.zrem(this.o.keys.userIndex(record.userId), record.id);
|
|
146
|
+
await this.o.redis.del(this.o.keys.tokenIndex(record.id));
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
throw new SessionStorageError('Session deletion failed', error);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/** Loads a bounded page of sessions and cleans stale derived-index members. */
|
|
153
|
+
async list(userId, offset, limit) {
|
|
154
|
+
const members = await this.o.redis.zrange(this.o.keys.userIndex(userId), offset, offset + limit - 1);
|
|
155
|
+
if (!members.length)
|
|
156
|
+
return [];
|
|
157
|
+
const keys = members.map(tokenHash => this.o.keys.sessionByTag(this.o.keys.userTag(userId), tokenHash));
|
|
158
|
+
const raw = await this.o.redis.mgetClusterAware(keys, { concurrency: this.o.config.maxConcurrency });
|
|
159
|
+
const records = [];
|
|
160
|
+
const stale = [];
|
|
161
|
+
raw.forEach((value, i) => {
|
|
162
|
+
if (!value) {
|
|
163
|
+
stale.push(members[i]);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
records.push(this.o.serializer.deserialize(value));
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
stale.push(members[i]);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
if (stale.length)
|
|
174
|
+
await this.o.redis.zrem(this.o.keys.userIndex(userId), ...stale);
|
|
175
|
+
return records;
|
|
176
|
+
}
|
|
177
|
+
/** Revokes a bounded batch and returns the number of indexed sessions remaining. */
|
|
178
|
+
async revokeAll(userId, limit) {
|
|
179
|
+
const bounded = Math.min(Math.max(1, limit), this.o.config.maxBatchSize);
|
|
180
|
+
const members = await this.o.redis.zrange(this.o.keys.userIndex(userId), 0, bounded - 1);
|
|
181
|
+
if (!members.length)
|
|
182
|
+
return { affected: 0, remaining: 0 };
|
|
183
|
+
const userTag = this.o.keys.userTag(userId);
|
|
184
|
+
const pipeline = this.o.redis.pipeline();
|
|
185
|
+
for (const tokenHash of members) {
|
|
186
|
+
pipeline.del(this.o.keys.sessionByTag(userTag, tokenHash));
|
|
187
|
+
pipeline.del(this.o.keys.tokenIndex(tokenHash));
|
|
188
|
+
}
|
|
189
|
+
const results = await pipeline.exec();
|
|
190
|
+
if (!results)
|
|
191
|
+
throw new SessionStorageError('Redis pipeline unavailable');
|
|
192
|
+
if (results.some(([error]) => error))
|
|
193
|
+
throw new SessionStorageError('One or more revokeAll operations failed');
|
|
194
|
+
await this.o.redis.zrem(this.o.keys.userIndex(userId), ...members);
|
|
195
|
+
const remaining = await this.o.redis.zcard(this.o.keys.userIndex(userId));
|
|
196
|
+
return { affected: members.length, remaining };
|
|
197
|
+
}
|
|
198
|
+
async safeDel(key) { try {
|
|
199
|
+
await this.o.redis.del(key);
|
|
200
|
+
}
|
|
201
|
+
catch { /* derived cleanup is best effort */ } }
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=repository.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repository.js","sourceRoot":"","sources":["../../src/session/repository.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { RedisClientWrapper } from '../redis/wrapper.js';
|
|
2
|
+
import type { SessionKeyStrategy } from './keys.js';
|
|
3
|
+
/**
|
|
4
|
+
* Stores short-lived JTI revocation records.
|
|
5
|
+
*
|
|
6
|
+
* This is a standalone utility for credentials whose authorization model is
|
|
7
|
+
* JTI-based (e.g. externally issued JWTs) — see docs/README-API.md. It is
|
|
8
|
+
* intentionally *not* wired into {@link SessionService}: this package's own
|
|
9
|
+
* opaque sessions treat session state itself as the source of truth, and
|
|
10
|
+
* deliberately avoid an extra revocation-key lookup on every `validate()` call.
|
|
11
|
+
*/
|
|
12
|
+
export declare class RedisRevocationStore {
|
|
13
|
+
private readonly redis;
|
|
14
|
+
private readonly keys;
|
|
15
|
+
/** Creates a revocation store using the shared Redis client and session key strategy. */
|
|
16
|
+
constructor(redis: RedisClientWrapper, keys: SessionKeyStrategy);
|
|
17
|
+
/** Stores a bounded-TTL JTI revocation tombstone. `now` should be sourced from the same clock as the caller's other session operations (e.g. Redis server time) to avoid skew. */
|
|
18
|
+
revoke(jti: string, now: number, expiresAt: number, reason?: string): Promise<void>;
|
|
19
|
+
/** Checks whether a JTI currently has a revocation tombstone. */
|
|
20
|
+
isRevoked(jti: string): Promise<boolean>;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=revocation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"revocation.d.ts","sourceRoot":"","sources":["../../src/session/revocation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAE9D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAEpD;;;;;;;;GAQG;AACH,qBAAa,oBAAoB;IAEnB,OAAO,CAAC,QAAQ,CAAC,KAAK;IAAsB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAD7E,yFAAyF;gBAC5D,KAAK,EAAE,kBAAkB,EAAmB,IAAI,EAAE,kBAAkB;IACjG,kLAAkL;IAC5K,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,SAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAM5F,iEAAiE;IAC3D,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAI/C"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { SessionStorageError } from './errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* Stores short-lived JTI revocation records.
|
|
4
|
+
*
|
|
5
|
+
* This is a standalone utility for credentials whose authorization model is
|
|
6
|
+
* JTI-based (e.g. externally issued JWTs) — see docs/README-API.md. It is
|
|
7
|
+
* intentionally *not* wired into {@link SessionService}: this package's own
|
|
8
|
+
* opaque sessions treat session state itself as the source of truth, and
|
|
9
|
+
* deliberately avoid an extra revocation-key lookup on every `validate()` call.
|
|
10
|
+
*/
|
|
11
|
+
export class RedisRevocationStore {
|
|
12
|
+
redis;
|
|
13
|
+
keys;
|
|
14
|
+
/** Creates a revocation store using the shared Redis client and session key strategy. */
|
|
15
|
+
constructor(redis, keys) {
|
|
16
|
+
this.redis = redis;
|
|
17
|
+
this.keys = keys;
|
|
18
|
+
}
|
|
19
|
+
/** Stores a bounded-TTL JTI revocation tombstone. `now` should be sourced from the same clock as the caller's other session operations (e.g. Redis server time) to avoid skew. */
|
|
20
|
+
async revoke(jti, now, expiresAt, reason = 'revoked') {
|
|
21
|
+
const ttl = Math.ceil(expiresAt - now);
|
|
22
|
+
if (ttl <= 0)
|
|
23
|
+
return;
|
|
24
|
+
try {
|
|
25
|
+
await this.redis.set(`${this.keys.getNamespace()}:revoked:${jti}`, reason, 'EX', String(ttl), 'NX');
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
throw new SessionStorageError('Unable to persist revocation', error);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Checks whether a JTI currently has a revocation tombstone. */
|
|
32
|
+
async isRevoked(jti) {
|
|
33
|
+
try {
|
|
34
|
+
return (await this.redis.exists(`${this.keys.getNamespace()}:revoked:${jti}`)) === 1;
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
throw new SessionStorageError('Unable to read revocation state', error);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=revocation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"revocation.js","sourceRoot":"","sources":["../../src/session/revocation.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAGlD;;;;;;;;GAQG;AACH,MAAM,OAAO,oBAAoB;IAEF;IAA4C;IADzE,yFAAyF;IACzF,YAA6B,KAAyB,EAAmB,IAAwB;QAApE,UAAK,GAAL,KAAK,CAAoB;QAAmB,SAAI,GAAJ,IAAI,CAAoB;IAAG,CAAC;IACrG,kLAAkL;IAClL,KAAK,CAAC,MAAM,CAAC,GAAW,EAAE,GAAW,EAAE,SAAiB,EAAE,MAAM,GAAG,SAAS;QAC1E,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,GAAG,GAAG,CAAC,CAAC;QACvC,IAAI,GAAG,IAAI,CAAC;YAAE,OAAO;QACrB,IAAI,CAAC;YAAC,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,YAAY,GAAG,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,CAAC;QAAC,CAAC;QAC5G,OAAO,KAAK,EAAE,CAAC;YAAC,MAAM,IAAI,mBAAmB,CAAC,8BAA8B,EAAE,KAAK,CAAC,CAAC;QAAC,CAAC;IACzF,CAAC;IACD,iEAAiE;IACjE,KAAK,CAAC,SAAS,CAAC,GAAW;QACzB,IAAI,CAAC;YAAC,OAAO,CAAC,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,YAAY,GAAG,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC;QAAC,CAAC;QAC7F,OAAO,KAAK,EAAE,CAAC;YAAC,MAAM,IAAI,mBAAmB,CAAC,iCAAiC,EAAE,KAAK,CAAC,CAAC;QAAC,CAAC;IAC5F,CAAC;CACF"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const SCRIPT_SOURCES: {
|
|
2
|
+
readonly create_session: "-- KEYS[1] session key\n-- KEYS[2] user index\n-- ARGV[1] serialized record\n-- ARGV[2] ttl seconds\n-- ARGV[3] createdAt score\n-- ARGV[4] tokenHash\n-- ARGV[5] max sessions (0 = unlimited)\n-- ARGV[6] namespace\n-- ARGV[7] userTag\nlocal created = redis.call('SET', KEYS[1], ARGV[1], 'NX', 'EX', ARGV[2])\nif not created then return {-1} end\nredis.call('ZADD', KEYS[2], ARGV[3], ARGV[4])\nif tonumber(ARGV[5]) > 0 then\n local count = redis.call('ZCARD', KEYS[2])\n if count > tonumber(ARGV[5]) then\n local excess = count - tonumber(ARGV[5])\n local old = redis.call('ZRANGE', KEYS[2], 0, excess - 1)\n for _, evictedHash in ipairs(old) do\n redis.call('ZREM', KEYS[2], evictedHash)\n redis.call('DEL', ARGV[6] .. ':session:{' .. ARGV[7] .. '}:' .. evictedHash)\n redis.call('DEL', ARGV[6] .. ':token-index:' .. evictedHash)\n end\n return {1, excess}\n end\nend\nreturn {1}\n";
|
|
3
|
+
readonly touch_session: "-- KEYS[1] session key\n-- ARGV[1] now epoch seconds\n-- ARGV[2] touch interval\n-- ARGV[3] new idle expiry epoch seconds (0 = no idle timeout)\n-- ARGV[4] new absolute expiry epoch seconds (0 = no absolute timeout)\nlocal raw = redis.call('GET', KEYS[1])\nif not raw then return {0} end\nlocal ok, s = pcall(cjson.decode, raw)\nif not ok or not s.data then return {-4} end\nlocal r = s.data\nif r.status ~= 'active' then return {-1} end\nlocal now = tonumber(ARGV[1])\nif tonumber(r.expiresAt) <= now then return {-2} end\nif r.absoluteExpiresAt and r.absoluteExpiresAt ~= cjson.null and tonumber(r.absoluteExpiresAt) > 0 and tonumber(r.absoluteExpiresAt) <= now then return {-2} end\nif r.idleExpiresAt and tonumber(r.idleExpiresAt) > 0 and tonumber(r.idleExpiresAt) <= now then return {-3} end\nif now - tonumber(r.lastAccessedAt) < tonumber(ARGV[2]) then return {2} end\nlocal idle = tonumber(ARGV[3])\nlocal absolute = tonumber(ARGV[4])\nif absolute > 0 and idle > absolute then idle = absolute end\nr.lastAccessedAt = now\nr.idleExpiresAt = idle > 0 and idle or cjson.null\nr.version = tonumber(r.version) + 1\nlocal wrapper = {v=1, data=r}\nlocal encoded = cjson.encode(wrapper)\nlocal ttl = tonumber(r.expiresAt) - now\nif ttl <= 0 then return {-2} end\nredis.call('SET', KEYS[1], encoded, 'XX', 'EX', ttl)\nreturn {1, r.version, idle}\n";
|
|
4
|
+
readonly consume_session: "-- KEYS[1] session key\n-- ARGV[1] now epoch seconds\n-- ARGV[2] tombstone ttl seconds\nlocal raw = redis.call('GET', KEYS[1])\nif not raw then return {0} end\nlocal ok, s = pcall(cjson.decode, raw)\nif not ok or not s.data then return {-4} end\nlocal r = s.data\nif r.status == 'consumed' then return {-1} end\nif r.status == 'revoked' then return {-3} end\nlocal now = tonumber(ARGV[1])\nif tonumber(r.expiresAt) <= now then return {-2} end\nif r.absoluteExpiresAt and r.absoluteExpiresAt ~= cjson.null and tonumber(r.absoluteExpiresAt) > 0 and tonumber(r.absoluteExpiresAt) <= now then return {-2} end\nif r.idleExpiresAt and tonumber(r.idleExpiresAt) > 0 and tonumber(r.idleExpiresAt) <= now then return {-2} end\nr.status = 'consumed'\nr.consumedAt = now\nr.version = tonumber(r.version) + 1\nlocal ttl = tonumber(ARGV[2])\nlocal encoded = cjson.encode({v=1, data=r})\nredis.call('SET', KEYS[1], encoded, 'XX', 'EX', ttl)\nreturn {1, r.userId, r.jti, r.version}\n";
|
|
5
|
+
readonly revoke_session: "-- KEYS[1] session key\n-- ARGV[1] now\n-- ARGV[2] tombstone ttl\nlocal raw = redis.call('GET', KEYS[1])\nif not raw then return {0} end\nlocal ok, s = pcall(cjson.decode, raw)\nif not ok or not s.data then return {-4} end\nlocal r=s.data\nif r.status == 'revoked' then return {2} end\nr.status='revoked'; r.version=tonumber(r.version)+1\nlocal ttl=tonumber(ARGV[2])\nif ttl < 1 then redis.call('DEL', KEYS[1]) else redis.call('SET', KEYS[1], cjson.encode({v=1,data=r}), 'XX', 'EX', ttl) end\nreturn {1}\n";
|
|
6
|
+
readonly update_session: "-- KEYS[1] session key\n-- ARGV[1] expected version\n-- ARGV[2] now\n-- ARGV[3] serialized replacement\nlocal raw = redis.call('GET', KEYS[1])\nif not raw then return {0} end\nlocal ok, s = pcall(cjson.decode, raw)\nif not ok or not s.data then return {-4} end\nif s.data.status ~= 'active' then return {-3} end\nif tonumber(s.data.version) ~= tonumber(ARGV[1]) then return {-2, s.data.version} end\nlocal replacement = ARGV[3]\nlocal ok2, parsed = pcall(cjson.decode, replacement)\nif not ok2 or not parsed.data then return {-4} end\nif 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\nlocal ttl = tonumber(s.data.expiresAt) - tonumber(ARGV[2])\nif ttl <= 0 then return {-1} end\nredis.call('SET', KEYS[1], replacement, 'XX', 'EX', ttl)\nreturn {1}\n";
|
|
7
|
+
readonly delete: "-- KEYS[1] session key\nreturn redis.call('DEL', KEYS[1])\n";
|
|
8
|
+
readonly enforce_limit: "-- Bounded helper. The application must pass session keys to delete as KEYS.\n-- KEYS[1] user index; KEYS[2..N] session keys corresponding to the oldest members.\n-- ARGV[1] max sessions; ARGV[2] number of candidate keys.\nlocal max=tonumber(ARGV[1])\nlocal candidateCount=tonumber(ARGV[2])\nlocal count=redis.call('ZCARD', KEYS[1])\nif max <= 0 or count <= max then return {0} end\nlocal excess=math.min(count-max, candidateCount)\nlocal removed=0\nfor i=1,excess do\n local tokenHash=redis.call('ZRANGE', KEYS[1], 0, 0)[1]\n if not tokenHash then break end\n redis.call('ZREM', KEYS[1], tokenHash)\n redis.call('DEL', KEYS[i+1])\n removed=removed+1\nend\nreturn {removed}\n";
|
|
9
|
+
readonly cleanup_index: "-- KEYS[1] user index\n-- ARGV[1..N] stale token hashes (user-index ZSET members, not JTIs)\nfor i=1,#ARGV do redis.call('ZREM', KEYS[1], ARGV[i]) end\nreturn #ARGV\n";
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=script-sources.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"script-sources.d.ts","sourceRoot":"","sources":["../../src/session/script-sources.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,cAAc;;;;;;;;;CA0IjB,CAAC"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
export const SCRIPT_SOURCES = {
|
|
2
|
+
'create_session': `-- KEYS[1] session key
|
|
3
|
+
-- KEYS[2] user index
|
|
4
|
+
-- ARGV[1] serialized record
|
|
5
|
+
-- ARGV[2] ttl seconds
|
|
6
|
+
-- ARGV[3] createdAt score
|
|
7
|
+
-- ARGV[4] tokenHash
|
|
8
|
+
-- ARGV[5] max sessions (0 = unlimited)
|
|
9
|
+
-- ARGV[6] namespace
|
|
10
|
+
-- ARGV[7] userTag
|
|
11
|
+
local created = redis.call('SET', KEYS[1], ARGV[1], 'NX', 'EX', ARGV[2])
|
|
12
|
+
if not created then return {-1} end
|
|
13
|
+
redis.call('ZADD', KEYS[2], ARGV[3], ARGV[4])
|
|
14
|
+
if tonumber(ARGV[5]) > 0 then
|
|
15
|
+
local count = redis.call('ZCARD', KEYS[2])
|
|
16
|
+
if count > tonumber(ARGV[5]) then
|
|
17
|
+
local excess = count - tonumber(ARGV[5])
|
|
18
|
+
local old = redis.call('ZRANGE', KEYS[2], 0, excess - 1)
|
|
19
|
+
for _, evictedHash in ipairs(old) do
|
|
20
|
+
redis.call('ZREM', KEYS[2], evictedHash)
|
|
21
|
+
redis.call('DEL', ARGV[6] .. ':session:{' .. ARGV[7] .. '}:' .. evictedHash)
|
|
22
|
+
redis.call('DEL', ARGV[6] .. ':token-index:' .. evictedHash)
|
|
23
|
+
end
|
|
24
|
+
return {1, excess}
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
return {1}
|
|
28
|
+
`,
|
|
29
|
+
'touch_session': `-- KEYS[1] session key
|
|
30
|
+
-- ARGV[1] now epoch seconds
|
|
31
|
+
-- ARGV[2] touch interval
|
|
32
|
+
-- ARGV[3] new idle expiry epoch seconds (0 = no idle timeout)
|
|
33
|
+
-- ARGV[4] new absolute expiry epoch seconds (0 = no absolute timeout)
|
|
34
|
+
local raw = redis.call('GET', KEYS[1])
|
|
35
|
+
if not raw then return {0} end
|
|
36
|
+
local ok, s = pcall(cjson.decode, raw)
|
|
37
|
+
if not ok or not s.data then return {-4} end
|
|
38
|
+
local r = s.data
|
|
39
|
+
if r.status ~= 'active' then return {-1} end
|
|
40
|
+
local now = tonumber(ARGV[1])
|
|
41
|
+
if tonumber(r.expiresAt) <= now then return {-2} end
|
|
42
|
+
if r.absoluteExpiresAt and r.absoluteExpiresAt ~= cjson.null and tonumber(r.absoluteExpiresAt) > 0 and tonumber(r.absoluteExpiresAt) <= now then return {-2} end
|
|
43
|
+
if r.idleExpiresAt and tonumber(r.idleExpiresAt) > 0 and tonumber(r.idleExpiresAt) <= now then return {-3} end
|
|
44
|
+
if now - tonumber(r.lastAccessedAt) < tonumber(ARGV[2]) then return {2} end
|
|
45
|
+
local idle = tonumber(ARGV[3])
|
|
46
|
+
local absolute = tonumber(ARGV[4])
|
|
47
|
+
if absolute > 0 and idle > absolute then idle = absolute end
|
|
48
|
+
r.lastAccessedAt = now
|
|
49
|
+
r.idleExpiresAt = idle > 0 and idle or cjson.null
|
|
50
|
+
r.version = tonumber(r.version) + 1
|
|
51
|
+
local wrapper = {v=1, data=r}
|
|
52
|
+
local encoded = cjson.encode(wrapper)
|
|
53
|
+
local ttl = tonumber(r.expiresAt) - now
|
|
54
|
+
if ttl <= 0 then return {-2} end
|
|
55
|
+
redis.call('SET', KEYS[1], encoded, 'XX', 'EX', ttl)
|
|
56
|
+
return {1, r.version, idle}
|
|
57
|
+
`,
|
|
58
|
+
'consume_session': `-- KEYS[1] session key
|
|
59
|
+
-- ARGV[1] now epoch seconds
|
|
60
|
+
-- ARGV[2] tombstone ttl seconds
|
|
61
|
+
local raw = redis.call('GET', KEYS[1])
|
|
62
|
+
if not raw then return {0} end
|
|
63
|
+
local ok, s = pcall(cjson.decode, raw)
|
|
64
|
+
if not ok or not s.data then return {-4} end
|
|
65
|
+
local r = s.data
|
|
66
|
+
if r.status == 'consumed' then return {-1} end
|
|
67
|
+
if r.status == 'revoked' then return {-3} end
|
|
68
|
+
local now = tonumber(ARGV[1])
|
|
69
|
+
if tonumber(r.expiresAt) <= now then return {-2} end
|
|
70
|
+
if r.absoluteExpiresAt and r.absoluteExpiresAt ~= cjson.null and tonumber(r.absoluteExpiresAt) > 0 and tonumber(r.absoluteExpiresAt) <= now then return {-2} end
|
|
71
|
+
if r.idleExpiresAt and tonumber(r.idleExpiresAt) > 0 and tonumber(r.idleExpiresAt) <= now then return {-2} end
|
|
72
|
+
r.status = 'consumed'
|
|
73
|
+
r.consumedAt = now
|
|
74
|
+
r.version = tonumber(r.version) + 1
|
|
75
|
+
local ttl = tonumber(ARGV[2])
|
|
76
|
+
local encoded = cjson.encode({v=1, data=r})
|
|
77
|
+
redis.call('SET', KEYS[1], encoded, 'XX', 'EX', ttl)
|
|
78
|
+
return {1, r.userId, r.jti, r.version}
|
|
79
|
+
`,
|
|
80
|
+
'revoke_session': `-- KEYS[1] session key
|
|
81
|
+
-- ARGV[1] now
|
|
82
|
+
-- ARGV[2] tombstone ttl
|
|
83
|
+
local raw = redis.call('GET', KEYS[1])
|
|
84
|
+
if not raw then return {0} end
|
|
85
|
+
local ok, s = pcall(cjson.decode, raw)
|
|
86
|
+
if not ok or not s.data then return {-4} end
|
|
87
|
+
local r=s.data
|
|
88
|
+
if r.status == 'revoked' then return {2} end
|
|
89
|
+
r.status='revoked'; r.version=tonumber(r.version)+1
|
|
90
|
+
local ttl=tonumber(ARGV[2])
|
|
91
|
+
if ttl < 1 then redis.call('DEL', KEYS[1]) else redis.call('SET', KEYS[1], cjson.encode({v=1,data=r}), 'XX', 'EX', ttl) end
|
|
92
|
+
return {1}
|
|
93
|
+
`,
|
|
94
|
+
'update_session': `-- KEYS[1] session key
|
|
95
|
+
-- ARGV[1] expected version
|
|
96
|
+
-- ARGV[2] now
|
|
97
|
+
-- ARGV[3] serialized replacement
|
|
98
|
+
local raw = redis.call('GET', KEYS[1])
|
|
99
|
+
if not raw then return {0} end
|
|
100
|
+
local ok, s = pcall(cjson.decode, raw)
|
|
101
|
+
if not ok or not s.data then return {-4} end
|
|
102
|
+
if s.data.status ~= 'active' then return {-3} end
|
|
103
|
+
if tonumber(s.data.version) ~= tonumber(ARGV[1]) then return {-2, s.data.version} end
|
|
104
|
+
local replacement = ARGV[3]
|
|
105
|
+
local ok2, parsed = pcall(cjson.decode, replacement)
|
|
106
|
+
if not ok2 or not parsed.data then return {-4} end
|
|
107
|
+
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
|
|
108
|
+
local ttl = tonumber(s.data.expiresAt) - tonumber(ARGV[2])
|
|
109
|
+
if ttl <= 0 then return {-1} end
|
|
110
|
+
redis.call('SET', KEYS[1], replacement, 'XX', 'EX', ttl)
|
|
111
|
+
return {1}
|
|
112
|
+
`,
|
|
113
|
+
'delete': `-- KEYS[1] session key
|
|
114
|
+
return redis.call('DEL', KEYS[1])
|
|
115
|
+
`,
|
|
116
|
+
'enforce_limit': `-- Bounded helper. The application must pass session keys to delete as KEYS.
|
|
117
|
+
-- KEYS[1] user index; KEYS[2..N] session keys corresponding to the oldest members.
|
|
118
|
+
-- ARGV[1] max sessions; ARGV[2] number of candidate keys.
|
|
119
|
+
local max=tonumber(ARGV[1])
|
|
120
|
+
local candidateCount=tonumber(ARGV[2])
|
|
121
|
+
local count=redis.call('ZCARD', KEYS[1])
|
|
122
|
+
if max <= 0 or count <= max then return {0} end
|
|
123
|
+
local excess=math.min(count-max, candidateCount)
|
|
124
|
+
local removed=0
|
|
125
|
+
for i=1,excess do
|
|
126
|
+
local tokenHash=redis.call('ZRANGE', KEYS[1], 0, 0)[1]
|
|
127
|
+
if not tokenHash then break end
|
|
128
|
+
redis.call('ZREM', KEYS[1], tokenHash)
|
|
129
|
+
redis.call('DEL', KEYS[i+1])
|
|
130
|
+
removed=removed+1
|
|
131
|
+
end
|
|
132
|
+
return {removed}
|
|
133
|
+
`,
|
|
134
|
+
'cleanup_index': `-- KEYS[1] user index
|
|
135
|
+
-- ARGV[1..N] stale token hashes (user-index ZSET members, not JTIs)
|
|
136
|
+
for i=1,#ARGV do redis.call('ZREM', KEYS[1], ARGV[i]) end
|
|
137
|
+
return #ARGV
|
|
138
|
+
`
|
|
139
|
+
};
|
|
140
|
+
//# sourceMappingURL=script-sources.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"script-sources.js","sourceRoot":"","sources":["../../src/session/script-sources.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,gBAAgB,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BnB;IACC,eAAe,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BlB;IACC,iBAAiB,EAAE;;;;;;;;;;;;;;;;;;;;;CAqBpB;IACC,gBAAgB,EAAE;;;;;;;;;;;;;CAanB;IACC,gBAAgB,EAAE;;;;;;;;;;;;;;;;;;CAkBnB;IACC,QAAQ,EAAE;;CAEX;IACC,eAAe,EAAE;;;;;;;;;;;;;;;;;CAiBlB;IACC,eAAe,EAAE;;;;CAIlB;CACS,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { RedisClientWrapper } from '../redis/wrapper.js';
|
|
2
|
+
import { SCRIPT_SOURCES } from './script-sources.js';
|
|
3
|
+
/** Names of versioned Lua scripts used by the session repository. */
|
|
4
|
+
export type ScriptName = keyof typeof SCRIPT_SOURCES;
|
|
5
|
+
/** Loads and executes versioned session Lua scripts. */
|
|
6
|
+
export declare class SessionScriptRegistry {
|
|
7
|
+
private readonly redis;
|
|
8
|
+
private readonly shas;
|
|
9
|
+
/** Creates a script registry backed by the shared Redis client. */
|
|
10
|
+
constructor(redis: RedisClientWrapper);
|
|
11
|
+
private sha;
|
|
12
|
+
/** Executes a named script with explicit KEYS and ARGV, recovering from NOSCRIPT. */
|
|
13
|
+
eval(name: ScriptName, keys: string[], args: string[]): Promise<unknown>;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=scripts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scripts.d.ts","sourceRoot":"","sources":["../../src/session/scripts.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAErD,qEAAqE;AACrE,MAAM,MAAM,UAAU,GAAG,MAAM,OAAO,cAAc,CAAC;AACrD,wDAAwD;AACxD,qBAAa,qBAAqB;IAGpB,OAAO,CAAC,QAAQ,CAAC,KAAK;IAFlC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAiC;IACtD,mEAAmE;gBACtC,KAAK,EAAE,kBAAkB;IACtD,OAAO,CAAC,GAAG;IAKX,qFAAqF;IAC/E,IAAI,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC;CAY/E"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { SessionStorageError } from './errors.js';
|
|
3
|
+
import { SCRIPT_SOURCES } from './script-sources.js';
|
|
4
|
+
/** Loads and executes versioned session Lua scripts. */
|
|
5
|
+
export class SessionScriptRegistry {
|
|
6
|
+
redis;
|
|
7
|
+
shas = new Map();
|
|
8
|
+
/** Creates a script registry backed by the shared Redis client. */
|
|
9
|
+
constructor(redis) {
|
|
10
|
+
this.redis = redis;
|
|
11
|
+
}
|
|
12
|
+
sha(name) {
|
|
13
|
+
let sha = this.shas.get(name);
|
|
14
|
+
if (sha === undefined) {
|
|
15
|
+
sha = createHash('sha1').update(SCRIPT_SOURCES[name]).digest('hex');
|
|
16
|
+
this.shas.set(name, sha);
|
|
17
|
+
}
|
|
18
|
+
return sha;
|
|
19
|
+
}
|
|
20
|
+
/** Executes a named script with explicit KEYS and ARGV, recovering from NOSCRIPT. */
|
|
21
|
+
async eval(name, keys, args) {
|
|
22
|
+
if (!keys.length)
|
|
23
|
+
throw new SessionStorageError('Lua script requires at least one key');
|
|
24
|
+
const sha = this.sha(name);
|
|
25
|
+
try {
|
|
26
|
+
return await this.redis.evalsha(sha, keys.length, ...keys, ...args);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
const message = error instanceof Error ? error.message : '';
|
|
30
|
+
if (!message.includes('NOSCRIPT'))
|
|
31
|
+
throw new SessionStorageError('Redis script execution failed', error);
|
|
32
|
+
try {
|
|
33
|
+
return await this.redis.eval(SCRIPT_SOURCES[name], keys.length, ...keys, ...args);
|
|
34
|
+
}
|
|
35
|
+
catch (fallbackError) {
|
|
36
|
+
throw new SessionStorageError('Redis script execution failed', fallbackError);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=scripts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scripts.js","sourceRoot":"","sources":["../../src/session/scripts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAElD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAIrD,wDAAwD;AACxD,MAAM,OAAO,qBAAqB;IAGH;IAFZ,IAAI,GAAG,IAAI,GAAG,EAAsB,CAAC;IACtD,mEAAmE;IACnE,YAA6B,KAAyB;QAAzB,UAAK,GAAL,KAAK,CAAoB;IAAG,CAAC;IAClD,GAAG,CAAC,IAAgB;QAC1B,IAAI,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC9B,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;YAAC,GAAG,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QAAC,CAAC;QACzH,OAAO,GAAG,CAAC;IACb,CAAC;IACD,qFAAqF;IACrF,KAAK,CAAC,IAAI,CAAC,IAAgB,EAAE,IAAc,EAAE,IAAc;QACzD,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,MAAM,IAAI,mBAAmB,CAAC,sCAAsC,CAAC,CAAC;QACxF,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC3B,IAAI,CAAC;YACH,OAAO,MAAM,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC,CAAC;QACtE,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;YAC5D,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC;gBAAE,MAAM,IAAI,mBAAmB,CAAC,+BAA+B,EAAE,KAAK,CAAC,CAAC;YACzG,IAAI,CAAC;gBAAC,OAAO,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC,CAAC;YAAC,CAAC;YAC1F,OAAO,aAAa,EAAE,CAAC;gBAAC,MAAM,IAAI,mBAAmB,CAAC,+BAA+B,EAAE,aAAa,CAAC,CAAC;YAAC,CAAC;QAC1G,CAAC;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { KeyManager, SessionRecord } from './types.js';
|
|
2
|
+
/** Versioned serializer with optional authenticated encryption for session records. */
|
|
3
|
+
export declare class SessionSerializer {
|
|
4
|
+
private readonly keyManager?;
|
|
5
|
+
/** Creates a serializer. Supplying a key manager enables AES-256-GCM envelopes. */
|
|
6
|
+
constructor(keyManager?: KeyManager | undefined);
|
|
7
|
+
/** Serializes and optionally encrypts a validated session record. */
|
|
8
|
+
serialize(session: SessionRecord): string;
|
|
9
|
+
/** Parses, optionally decrypts, validates, and returns a session record. */
|
|
10
|
+
deserialize(serialized: string): SessionRecord;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=serializer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../../src/session/serializer.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAI5D,uFAAuF;AACvF,qBAAa,iBAAiB;IAEhB,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC;IADxC,mFAAmF;gBACtD,UAAU,CAAC,EAAE,UAAU,YAAA;IAEpD,qEAAqE;IACrE,SAAS,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM;IAkBzC,4EAA4E;IAC5E,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,aAAa;CAoB/C"}
|