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.
Files changed (267) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/LICENSE +1 -1
  3. package/README.md +68 -1058
  4. package/dist/cache/cache.d.ts +30 -0
  5. package/dist/cache/cache.d.ts.map +1 -0
  6. package/dist/cache/cache.js +59 -0
  7. package/dist/cache/cache.js.map +1 -0
  8. package/dist/cache/config.d.ts +12 -0
  9. package/dist/cache/config.d.ts.map +1 -0
  10. package/dist/cache/config.js +13 -0
  11. package/dist/cache/config.js.map +1 -0
  12. package/dist/cache/types.d.ts +32 -0
  13. package/dist/cache/types.d.ts.map +1 -0
  14. package/dist/cache/types.js +5 -0
  15. package/dist/cache/types.js.map +1 -0
  16. package/dist/index.d.ts +43 -51
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +32 -44
  19. package/dist/index.js.map +1 -0
  20. package/dist/lock/config.d.ts +12 -0
  21. package/dist/lock/config.d.ts.map +1 -0
  22. package/dist/lock/config.js +8 -0
  23. package/dist/lock/config.js.map +1 -0
  24. package/dist/lock/lock.d.ts +20 -0
  25. package/dist/lock/lock.d.ts.map +1 -0
  26. package/dist/lock/lock.js +44 -0
  27. package/dist/lock/lock.js.map +1 -0
  28. package/dist/lock/types.d.ts +19 -0
  29. package/dist/lock/types.d.ts.map +1 -0
  30. package/dist/lock/types.js +2 -0
  31. package/dist/lock/types.js.map +1 -0
  32. package/dist/modules-config.d.ts +3 -0
  33. package/dist/modules-config.d.ts.map +1 -0
  34. package/dist/modules-config.js +2 -0
  35. package/dist/modules-config.js.map +1 -0
  36. package/dist/pubsub/config.d.ts +11 -0
  37. package/dist/pubsub/config.d.ts.map +1 -0
  38. package/dist/pubsub/config.js +6 -0
  39. package/dist/pubsub/config.js.map +1 -0
  40. package/dist/pubsub/pubsub.d.ts +20 -0
  41. package/dist/pubsub/pubsub.d.ts.map +1 -0
  42. package/dist/pubsub/pubsub.js +54 -0
  43. package/dist/pubsub/pubsub.js.map +1 -0
  44. package/dist/pubsub/types.d.ts +24 -0
  45. package/dist/pubsub/types.d.ts.map +1 -0
  46. package/dist/pubsub/types.js +2 -0
  47. package/dist/pubsub/types.js.map +1 -0
  48. package/dist/rate-limit/config.d.ts +12 -0
  49. package/dist/rate-limit/config.d.ts.map +1 -0
  50. package/dist/rate-limit/config.js +6 -0
  51. package/dist/rate-limit/config.js.map +1 -0
  52. package/dist/rate-limit/rate-limiter.d.ts +18 -0
  53. package/dist/rate-limit/rate-limiter.d.ts.map +1 -0
  54. package/dist/rate-limit/rate-limiter.js +37 -0
  55. package/dist/rate-limit/rate-limiter.js.map +1 -0
  56. package/dist/rate-limit/types.d.ts +27 -0
  57. package/dist/rate-limit/types.d.ts.map +1 -0
  58. package/dist/rate-limit/types.js +2 -0
  59. package/dist/rate-limit/types.js.map +1 -0
  60. package/dist/redis/client-facade.d.ts +77 -0
  61. package/dist/redis/client-facade.d.ts.map +1 -0
  62. package/dist/redis/client-facade.js +102 -0
  63. package/dist/redis/client-facade.js.map +1 -0
  64. package/dist/redis/client.d.ts +10 -0
  65. package/dist/redis/client.d.ts.map +1 -0
  66. package/dist/redis/client.js +29 -0
  67. package/dist/redis/client.js.map +1 -0
  68. package/dist/redis/cluster.d.ts +7 -0
  69. package/dist/redis/cluster.d.ts.map +1 -0
  70. package/dist/redis/cluster.js +47 -0
  71. package/dist/redis/cluster.js.map +1 -0
  72. package/dist/redis/config.d.ts +39 -0
  73. package/dist/redis/config.d.ts.map +1 -0
  74. package/dist/redis/config.js +52 -0
  75. package/dist/redis/config.js.map +1 -0
  76. package/dist/redis/errors.d.ts +5 -0
  77. package/dist/redis/errors.d.ts.map +1 -0
  78. package/dist/redis/errors.js +5 -0
  79. package/dist/redis/errors.js.map +1 -0
  80. package/dist/redis/types.d.ts +135 -0
  81. package/dist/redis/types.d.ts.map +1 -0
  82. package/dist/redis/types.js +2 -0
  83. package/dist/redis/types.js.map +1 -0
  84. package/dist/redis/wrapper.d.ts +88 -0
  85. package/dist/redis/wrapper.d.ts.map +1 -0
  86. package/dist/redis/wrapper.js +206 -0
  87. package/dist/redis/wrapper.js.map +1 -0
  88. package/dist/session/config.d.ts +47 -0
  89. package/dist/session/config.d.ts.map +1 -0
  90. package/dist/session/config.js +101 -0
  91. package/dist/session/config.js.map +1 -0
  92. package/dist/session/cookie.d.ts +16 -0
  93. package/dist/session/cookie.d.ts.map +1 -0
  94. package/dist/session/cookie.js +28 -0
  95. package/dist/session/cookie.js.map +1 -0
  96. package/dist/session/errors.d.ts +56 -0
  97. package/dist/session/errors.d.ts.map +1 -0
  98. package/dist/session/errors.js +58 -0
  99. package/dist/session/errors.js.map +1 -0
  100. package/dist/session/factory.d.ts +21 -0
  101. package/dist/session/factory.d.ts.map +1 -0
  102. package/dist/session/factory.js +30 -0
  103. package/dist/session/factory.js.map +1 -0
  104. package/dist/session/health.d.ts +12 -0
  105. package/dist/session/health.d.ts.map +1 -0
  106. package/dist/session/health.js +23 -0
  107. package/dist/session/health.js.map +1 -0
  108. package/dist/session/keys.d.ts +23 -0
  109. package/dist/session/keys.d.ts.map +1 -0
  110. package/dist/session/keys.js +27 -0
  111. package/dist/session/keys.js.map +1 -0
  112. package/dist/session/manager.d.ts +34 -0
  113. package/dist/session/manager.d.ts.map +1 -0
  114. package/dist/session/manager.js +31 -0
  115. package/dist/session/manager.js.map +1 -0
  116. package/dist/session/metrics.d.ts +11 -0
  117. package/dist/session/metrics.d.ts.map +1 -0
  118. package/dist/session/metrics.js +10 -0
  119. package/dist/session/metrics.js.map +1 -0
  120. package/dist/session/repository.d.ts +49 -0
  121. package/dist/session/repository.d.ts.map +1 -0
  122. package/dist/session/repository.js +203 -0
  123. package/dist/session/repository.js.map +1 -0
  124. package/dist/session/revocation.d.ts +22 -0
  125. package/dist/session/revocation.d.ts.map +1 -0
  126. package/dist/session/revocation.js +41 -0
  127. package/dist/session/revocation.js.map +1 -0
  128. package/dist/session/script-sources.d.ts +11 -0
  129. package/dist/session/script-sources.d.ts.map +1 -0
  130. package/dist/session/script-sources.js +140 -0
  131. package/dist/session/script-sources.js.map +1 -0
  132. package/dist/session/scripts.d.ts +15 -0
  133. package/dist/session/scripts.d.ts.map +1 -0
  134. package/dist/session/scripts.js +41 -0
  135. package/dist/session/scripts.js.map +1 -0
  136. package/dist/session/serializer.d.ts +12 -0
  137. package/dist/session/serializer.d.ts.map +1 -0
  138. package/dist/session/serializer.js +77 -0
  139. package/dist/session/serializer.js.map +1 -0
  140. package/dist/session/service.d.ts +48 -0
  141. package/dist/session/service.d.ts.map +1 -0
  142. package/dist/session/service.js +235 -0
  143. package/dist/session/service.js.map +1 -0
  144. package/dist/session/token.d.ts +16 -0
  145. package/dist/session/token.d.ts.map +1 -0
  146. package/dist/session/token.js +32 -0
  147. package/dist/session/token.js.map +1 -0
  148. package/dist/session/types.d.ts +134 -0
  149. package/dist/session/types.d.ts.map +1 -0
  150. package/dist/session/types.js +2 -0
  151. package/dist/session/types.js.map +1 -0
  152. package/dist/streams/config.d.ts +12 -0
  153. package/dist/streams/config.d.ts.map +1 -0
  154. package/dist/streams/config.js +6 -0
  155. package/dist/streams/config.js.map +1 -0
  156. package/dist/streams/streams.d.ts +24 -0
  157. package/dist/streams/streams.d.ts.map +1 -0
  158. package/dist/streams/streams.js +55 -0
  159. package/dist/streams/streams.js.map +1 -0
  160. package/dist/streams/types.d.ts +32 -0
  161. package/dist/streams/types.d.ts.map +1 -0
  162. package/dist/streams/types.js +2 -0
  163. package/dist/streams/types.js.map +1 -0
  164. package/docs/ACCEPTANCE-REPORT.md +70 -0
  165. package/docs/ARCHITECTURE.md +61 -0
  166. package/docs/CAPACITY.md +33 -0
  167. package/docs/DEPLOYMENT.md +22 -0
  168. package/docs/README-API.md +15 -0
  169. package/docs/STATE-MACHINE.md +38 -0
  170. package/docs/TESTING.md +37 -0
  171. package/docs/THREAT-MODEL.md +23 -0
  172. package/docs/TYPE-SAFETY.md +34 -0
  173. package/docs/modules/cache/README.md +7 -0
  174. package/docs/modules/cache/usage.md +156 -0
  175. package/docs/modules/lock/README.md +7 -0
  176. package/docs/modules/lock/usage.md +105 -0
  177. package/docs/modules/pubsub/README.md +7 -0
  178. package/docs/modules/pubsub/usage.md +106 -0
  179. package/docs/modules/rate-limit/README.md +7 -0
  180. package/docs/modules/rate-limit/usage.md +100 -0
  181. package/docs/modules/sessions/README.md +7 -0
  182. package/docs/modules/sessions/usage.md +262 -0
  183. package/docs/modules/streams/README.md +7 -0
  184. package/docs/modules/streams/usage.md +141 -0
  185. package/package.json +50 -60
  186. package/src/scripts/cleanup-index.lua +4 -0
  187. package/src/scripts/conditional-update.lua +21 -0
  188. package/src/scripts/consume-session.lua +21 -0
  189. package/src/scripts/create-session.lua +28 -0
  190. package/src/scripts/delete.lua +2 -0
  191. package/src/scripts/destroy-user.lua +13 -0
  192. package/src/scripts/enforce-limit.lua +17 -0
  193. package/src/scripts/revoke-session.lua +13 -0
  194. package/src/scripts/rotate.lua +24 -0
  195. package/src/scripts/touch-session.lua +28 -0
  196. package/src/scripts/update-session.lua +18 -0
  197. package/dist/cache.d.ts +0 -796
  198. package/dist/cache.js +0 -1120
  199. package/dist/client.d.ts +0 -284
  200. package/dist/client.js +0 -1114
  201. package/dist/cluster-slot.d.ts +0 -4
  202. package/dist/cluster-slot.js +0 -31
  203. package/dist/cluster.d.ts +0 -79
  204. package/dist/cluster.js +0 -156
  205. package/dist/errors.d.ts +0 -30
  206. package/dist/errors.js +0 -63
  207. package/dist/health.d.ts +0 -180
  208. package/dist/health.js +0 -239
  209. package/dist/lock.d.ts +0 -248
  210. package/dist/lock.js +0 -397
  211. package/dist/logger.d.ts +0 -12
  212. package/dist/logger.js +0 -40
  213. package/dist/pubsub.d.ts +0 -423
  214. package/dist/pubsub.js +0 -537
  215. package/dist/ratelimiter.d.ts +0 -441
  216. package/dist/ratelimiter.js +0 -539
  217. package/dist/session/index.d.ts +0 -23
  218. package/dist/session/index.js +0 -16
  219. package/dist/session/revocation-store.d.ts +0 -176
  220. package/dist/session/revocation-store.js +0 -318
  221. package/dist/session/scripts/cleanup-index.lua +0 -21
  222. package/dist/session/scripts/conditional-update-encrypted.lua +0 -60
  223. package/dist/session/scripts/conditional-update.lua +0 -63
  224. package/dist/session/scripts/create.lua +0 -83
  225. package/dist/session/scripts/delete-by-user.lua +0 -29
  226. package/dist/session/scripts/delete.lua +0 -15
  227. package/dist/session/scripts/enforce-limit.lua +0 -38
  228. package/dist/session/scripts/revoke.lua +0 -61
  229. package/dist/session/scripts/rotate-encrypted.lua +0 -110
  230. package/dist/session/scripts/rotate.lua +0 -122
  231. package/dist/session/scripts/touch-encrypted.lua +0 -89
  232. package/dist/session/scripts/touch.lua +0 -72
  233. package/dist/session/scripts/validate.lua +0 -90
  234. package/dist/session/session-circuit-breaker.d.ts +0 -42
  235. package/dist/session/session-circuit-breaker.js +0 -129
  236. package/dist/session/session-config.d.ts +0 -335
  237. package/dist/session/session-config.js +0 -162
  238. package/dist/session/session-cookie.d.ts +0 -72
  239. package/dist/session/session-cookie.js +0 -101
  240. package/dist/session/session-encryption.d.ts +0 -87
  241. package/dist/session/session-encryption.js +0 -139
  242. package/dist/session/session-errors.d.ts +0 -85
  243. package/dist/session/session-errors.js +0 -145
  244. package/dist/session/session-health.d.ts +0 -38
  245. package/dist/session/session-health.js +0 -60
  246. package/dist/session/session-keys.d.ts +0 -51
  247. package/dist/session/session-keys.js +0 -113
  248. package/dist/session/session-manager.d.ts +0 -73
  249. package/dist/session/session-manager.js +0 -94
  250. package/dist/session/session-metrics.d.ts +0 -33
  251. package/dist/session/session-metrics.js +0 -112
  252. package/dist/session/session-repository.d.ts +0 -161
  253. package/dist/session/session-repository.js +0 -683
  254. package/dist/session/session-scripts.d.ts +0 -36
  255. package/dist/session/session-scripts.js +0 -130
  256. package/dist/session/session-serializer.d.ts +0 -42
  257. package/dist/session/session-serializer.js +0 -248
  258. package/dist/session/session-service.d.ts +0 -104
  259. package/dist/session/session-service.js +0 -611
  260. package/dist/session/session-token.d.ts +0 -38
  261. package/dist/session/session-token.js +0 -86
  262. package/dist/session/session-types.d.ts +0 -253
  263. package/dist/session/session-types.js +0 -16
  264. package/dist/types.d.ts +0 -924
  265. package/dist/types.js +0 -151
  266. package/dist/utils/deepmerge.d.ts +0 -9
  267. 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"}