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
@@ -1,83 +0,0 @@
1
- -- create.lua (version 1)
2
- -- Atomically creates a session record, registers it in the user index
3
- -- (ZSET, score = microsecond-resolution Redis server time, used purely
4
- -- for eviction ordering - see below), optionally claims an idempotency
5
- -- key, and enforces maxSessionsPerUser with bounded oldest-first eviction.
6
- --
7
- -- KEYS[1] = session record key
8
- -- KEYS[2] = user session index key (ZSET)
9
- -- KEYS[3] = idempotency claim key (only passed when idempotency is enabled)
10
- --
11
- -- ARGV[1] = serialized session envelope (plain v1 or encrypted v2)
12
- -- ARGV[2] = jti
13
- -- ARGV[3] = createdAt (Unix seconds)
14
- -- ARGV[4] = session TTL (absoluteExpiresAt - now, clamped >= 1)
15
- -- ARGV[5] = maxSessionsPerUser (0 disables)
16
- -- ARGV[6] = session key prefix for this user (same hash tag => same slot)
17
- -- ARGV[7] = max evictions per call (bounded Lua work)
18
- -- ARGV[8] = idempotency claim TTL ('' when idempotency is disabled)
19
- --
20
- -- Returns:
21
- -- {1, {evictedJtis...}} success (evicted list may be empty)
22
- -- {3, jti} idempotent replay: a previous attempt created
23
- -- the session; jti identifies it
24
- -- -5 jti collision: a session with this id exists
25
- --
26
- -- All keys share the {userId} hash tag, so this script is atomic on a
27
- -- single Cluster slot. No cross-slot keys are touched.
28
- local key = KEYS[1]
29
- local index = KEYS[2]
30
-
31
- -- Idempotent replay wins over collision detection: a retry with the same
32
- -- idempotencyKey must return the original session, even though the session
33
- -- record already exists.
34
- if ARGV[8] ~= '' then
35
- local claimKey = KEYS[3]
36
- local claimJti = redis.call('GET', claimKey)
37
- if claimJti then
38
- return { 3, claimJti }
39
- end
40
- redis.call('SET', claimKey, ARGV[2], 'EX', tonumber(ARGV[8]))
41
- end
42
-
43
- if redis.call('EXISTS', key) == 1 then
44
- return -5
45
- end
46
-
47
- redis.call('SET', key, ARGV[1], 'EX', tonumber(ARGV[4]))
48
-
49
- -- The user index score is used purely for oldest-first eviction ordering
50
- -- (never as the record's createdAt, which is app-stamped seconds inside the
51
- -- envelope). Redis breaks equal ZSET scores by lexicographic member order,
52
- -- not insertion order, so scoring by second-granularity createdAt would
53
- -- make eviction pick an arbitrary (jti-lexicographic) session rather than
54
- -- the actual oldest one whenever a user creates more than one session in
55
- -- the same wall-clock second - a realistic case under normal traffic, not
56
- -- just a burst edge case. Redis is single-threaded, so two scripts touching
57
- -- the same user slot never observe the same TIME() reading in practice;
58
- -- microsecond-resolution server time therefore gives a strictly monotonic,
59
- -- clock-skew-free ordering key for this user's index.
60
- local t = redis.call('TIME')
61
- local orderScore = tonumber(t[1]) + (tonumber(t[2]) / 1000000)
62
- redis.call('ZADD', index, orderScore, ARGV[2])
63
-
64
- local evicted = {}
65
- local limit = tonumber(ARGV[5])
66
-
67
- if limit and limit > 0 then
68
- local card = redis.call('ZCARD', index)
69
- if card > limit then
70
- local excess = card - limit
71
- local n = math.min(excess, tonumber(ARGV[7]))
72
- local members = redis.call('ZRANGE', index, 0, n - 1)
73
- local prefix = ARGV[6]
74
-
75
- for _, member in ipairs(members) do
76
- redis.call('DEL', prefix .. member)
77
- redis.call('ZREM', index, member)
78
- table.insert(evicted, member)
79
- end
80
- end
81
- end
82
-
83
- return { 1, evicted }
@@ -1,29 +0,0 @@
1
- -- delete-by-user.lua (version 2)
2
- -- Deletes a bounded batch of a user's sessions and removes them from the
3
- -- user index, in one same-slot script. The caller chunks the user's jtis
4
- -- into batches (limits.maxBatchSize), so a user with an arbitrarily large
5
- -- session count never produces one giant script.
6
- --
7
- -- KEYS[1] = user session index key
8
- -- KEYS[2..] = session record keys (same slot as KEYS[1])
9
- --
10
- -- ARGV[1..] = matching jtis (index i pairs with KEYS[i + 1])
11
- --
12
- -- Returns: the jtis whose records were actually deleted. The index key is
13
- -- deleted at the end (the whole-user operation is complete when the caller
14
- -- has processed every batch).
15
- local deleted = {}
16
-
17
- for i = 2, #KEYS do
18
- local jti = ARGV[i - 1]
19
-
20
- if redis.call('DEL', KEYS[i]) == 1 then
21
- table.insert(deleted, jti)
22
- end
23
-
24
- redis.call('ZREM', KEYS[1], jti)
25
- end
26
-
27
- redis.call('DEL', KEYS[1])
28
-
29
- return deleted
@@ -1,15 +0,0 @@
1
- -- delete.lua (version 1)
2
- -- Physically deletes a session record and removes it from the user index.
3
- -- Idempotent: deleting a missing session returns 0 and is not an error.
4
- --
5
- -- KEYS[1] = session record key
6
- -- KEYS[2] = user session index key
7
- --
8
- -- ARGV[1] = jti
9
- --
10
- -- Returns: 1 if the record existed, 0 otherwise.
11
- local existed = redis.call('DEL', KEYS[1])
12
-
13
- redis.call('ZREM', KEYS[2], ARGV[1])
14
-
15
- return existed
@@ -1,38 +0,0 @@
1
- -- enforce-limit.lua (version 1)
2
- -- Standalone max-session enforcement (admin/repair paths). Evicts the
3
- -- oldest excess sessions from a user's index using ZSET ordering and
4
- -- bounded work (never loads the whole index).
5
- --
6
- -- KEYS[1] = user session index key
7
- --
8
- -- ARGV[1] = maxSessionsPerUser (0 disables)
9
- -- ARGV[2] = max evictions per call (bounded Lua work; callers loop to
10
- -- converge when the excess exceeds this bound)
11
- -- ARGV[3] = session key prefix for this user (same hash tag => same slot)
12
- --
13
- -- Returns: { evictedCount, {evictedJtis...} }
14
- local card = redis.call('ZCARD', KEYS[1])
15
- local limit = tonumber(ARGV[1])
16
-
17
- if limit <= 0 then
18
- return { 0, {} }
19
- end
20
-
21
- local excess = card - limit
22
-
23
- if excess <= 0 then
24
- return { 0, {} }
25
- end
26
-
27
- local n = math.min(excess, tonumber(ARGV[2]))
28
- local members = redis.call('ZRANGE', KEYS[1], 0, n - 1)
29
- local prefix = ARGV[3]
30
- local evicted = {}
31
-
32
- for _, member in ipairs(members) do
33
- redis.call('DEL', prefix .. member)
34
- redis.call('ZREM', KEYS[1], member)
35
- table.insert(evicted, member)
36
- end
37
-
38
- return { #evicted, evicted }
@@ -1,61 +0,0 @@
1
- -- revoke.lua (version 1)
2
- -- Logically revokes a session: status -> 'revoked' with a bounded tombstone
3
- -- TTL (remaining absolute lifetime). Revoked sessions never authenticate,
4
- -- even if a stale copy of the record survives somewhere. Idempotent: a
5
- -- second revoke returns 2.
6
- --
7
- -- KEYS[1] = session record key
8
- --
9
- -- ARGV[1] = new serialized envelope for encrypted (v2) sessions,
10
- -- '' for plain (v1) sessions (the script builds it itself)
11
- -- ARGV[2] = tombstone TTL (remaining absolute lifetime, clamped >= 1)
12
- --
13
- -- Returns:
14
- -- 1 revoked
15
- -- 2 already revoked (idempotent)
16
- -- 0 not found
17
- -- 3 unknown envelope (neither v1 nor v2)
18
- local raw = redis.call('GET', KEYS[1])
19
-
20
- if not raw then
21
- return 0
22
- end
23
-
24
- local env = cjson.decode(raw)
25
- local ttl = tonumber(ARGV[2])
26
-
27
- if env.v == 1 then
28
- local s = env.s
29
-
30
- if s.status == 'revoked' then
31
- return 2
32
- end
33
-
34
- s.status = 'revoked'
35
- redis.call('SET', KEYS[1], cjson.encode(env), 'EX', ttl)
36
- return 1
37
- end
38
-
39
- if env.v == 2 then
40
- if env.st == 'revoked' then
41
- return 2
42
- end
43
-
44
- if ARGV[1] == '' then
45
- return 4
46
- end
47
-
48
- local newEnv = cjson.decode(ARGV[1])
49
- newEnv.st = 'revoked'
50
- newEnv.ver = env.ver
51
- newEnv.la = env.la
52
- newEnv.idle = env.idle
53
- newEnv.exp = env.exp
54
- newEnv.rn = env.rn
55
- newEnv.rj = env.rj
56
-
57
- redis.call('SET', KEYS[1], cjson.encode(newEnv), 'EX', ttl)
58
- return 1
59
- end
60
-
61
- return 3
@@ -1,110 +0,0 @@
1
- -- rotate-encrypted.lua (version 1) - encrypted (v2) envelopes
2
- -- Atomic, single-use session rotation for encrypted sessions.
3
- --
4
- -- The ciphertext cannot be decoded inside Lua, so the app decrypts the
5
- -- old record, builds the consumed + successor payloads (re-encrypted with
6
- -- the current key) and passes them as ARGV. The script is the only
7
- -- authority for the state transition, decided from the plaintext header
8
- -- mirrors (st/ver/exp/rn/rj) and Redis server time; the header mirrors of
9
- -- the written payloads are overwritten from the checked values so a stale
10
- -- app payload can never misrepresent state.
11
- --
12
- -- KEYS[1] = old session record key
13
- -- KEYS[2] = successor session record key
14
- -- KEYS[3] = user session index key
15
- --
16
- -- ARGV[1] = consumed serialized envelope (app-built, re-encrypted)
17
- -- ARGV[2] = successor serialized envelope (app-built, re-encrypted)
18
- -- ARGV[3] = successor jti
19
- -- ARGV[4] = expected version of the old session ('' = no check)
20
- -- ARGV[5] = rotation nonce hash ('' = none)
21
- -- ARGV[6] = retain consumed tombstone (1/0)
22
- -- ARGV[7] = old jti
23
- -- ARGV[8] = successor TTL (clamped >= 1)
24
- -- ARGV[9] = consumed tombstone TTL (clamped >= 1)
25
- --
26
- -- Returns: same codes as rotate.lua (1/2/0/-1/-2/-3/-4/5), plus 6 when the
27
- -- envelope is plain (caller must use the plain path).
28
- local raw = redis.call('GET', KEYS[1])
29
-
30
- if not raw then
31
- return 0
32
- end
33
-
34
- local env = cjson.decode(raw)
35
-
36
- if env.v ~= 2 then
37
- return 6
38
- end
39
-
40
- local t = redis.call('TIME')
41
- local now = tonumber(t[1])
42
-
43
- -- Retry-safe replay detection BEFORE rejecting consumed records.
44
- -- The rotation nonce uniquely identifies the rotation: when the consumed
45
- -- record carries the same nonce hash, this is a retry of an already-applied
46
- -- rotation. The stored rotatedTo jti is returned (the retry's own freshly
47
- -- generated successor jti is discarded - it can never match).
48
- if env.st == 'consumed' then
49
- if ARGV[5] ~= '' and env.rn == ARGV[5] then
50
- return { 2, env.rj }
51
- end
52
- return -1
53
- end
54
-
55
- if env.st ~= 'active' then
56
- return -1
57
- end
58
-
59
- if tonumber(env.exp) <= now then
60
- redis.call('DEL', KEYS[1])
61
- redis.call('ZREM', KEYS[3], ARGV[7])
62
- return -2
63
- end
64
-
65
- if ARGV[4] ~= '' and tostring(env.ver) ~= ARGV[4] then
66
- return -3
67
- end
68
-
69
- if redis.call('EXISTS', KEYS[2]) == 1 then
70
- return -4
71
- end
72
-
73
- local consumedEnv = cjson.decode(ARGV[1])
74
- consumedEnv.st = 'consumed'
75
- consumedEnv.ver = env.ver
76
- consumedEnv.la = env.la
77
- consumedEnv.idle = env.idle
78
- consumedEnv.exp = env.exp
79
- consumedEnv.rn = cjson.null
80
- consumedEnv.rj = ARGV[3]
81
-
82
- if ARGV[5] ~= '' then
83
- consumedEnv.rn = ARGV[5]
84
- end
85
-
86
- local nextEnv = cjson.decode(ARGV[2])
87
- nextEnv.st = 'active'
88
- nextEnv.ver = 1
89
- nextEnv.la = now
90
- if nextEnv.idle ~= cjson.null then
91
- nextEnv.idle = math.min(tonumber(nextEnv.idle), tonumber(nextEnv.exp))
92
- end
93
- nextEnv.rn = cjson.null
94
- nextEnv.rj = cjson.null
95
-
96
- local nextTtl = math.max(1, tonumber(ARGV[8]))
97
-
98
- if ARGV[6] == '1' then
99
- redis.call('SET', KEYS[1], cjson.encode(consumedEnv), 'EX', math.max(1, tonumber(ARGV[9])))
100
- else
101
- redis.call('DEL', KEYS[1])
102
- end
103
-
104
- redis.call('SET', KEYS[2], cjson.encode(nextEnv), 'EX', nextTtl)
105
- redis.call('ZREM', KEYS[3], ARGV[7])
106
- -- Microsecond-resolution ordering score - see create.lua for why
107
- -- second-granularity scores break oldest-first eviction ordering.
108
- redis.call('ZADD', KEYS[3], tonumber(t[1]) + (tonumber(t[2]) / 1000000), ARGV[3])
109
-
110
- return { 1, ARGV[3] }
@@ -1,122 +0,0 @@
1
- -- rotate.lua (version 1) - plain (v1) envelopes
2
- -- Atomic, single-use session rotation.
3
- --
4
- -- The old session is consumed (status=consumed, consumedAt, rotatedTo,
5
- -- rotationNonceHash) and the successor is created, all in one same-slot
6
- -- script. No preliminary GET can make this safe; the script is the only
7
- -- authority. Timestamps are stamped from Redis server time.
8
- --
9
- -- Retry-safe idempotency: when the caller retries a rotation that already
10
- -- succeeded (response lost), the consumed record retains rotationNonceHash
11
- -- and rotatedTo. If both match the retry, the script returns {2, jti} and
12
- -- the caller returns the existing successor instead of a replay error.
13
- --
14
- -- KEYS[1] = old session record key
15
- -- KEYS[2] = successor session record key
16
- -- KEYS[3] = user session index key
17
- --
18
- -- ARGV[1] = successor serialized envelope (app-built static fields)
19
- -- ARGV[2] = successor jti
20
- -- ARGV[3] = expected version of the old session ('' = no check)
21
- -- ARGV[4] = rotation nonce hash ('' = none)
22
- -- ARGV[5] = retain consumed tombstone (1/0)
23
- -- ARGV[6] = old jti
24
- --
25
- -- Returns:
26
- -- {1, successorJti} success
27
- -- {2, successorJti} idempotent replay of a rotation with the same nonce
28
- -- 0 old session not found
29
- -- -1 already consumed / revoked (replay)
30
- -- -2 old session expired (record removed)
31
- -- -3 version conflict
32
- -- -4 successor jti collision
33
- -- 5 envelope is encrypted (use the encrypted path)
34
- local raw = redis.call('GET', KEYS[1])
35
-
36
- if not raw then
37
- return 0
38
- end
39
-
40
- local env = cjson.decode(raw)
41
-
42
- if env.v ~= 1 then
43
- return 5
44
- end
45
-
46
- local s = env.s
47
- local t = redis.call('TIME')
48
- local now = tonumber(t[1])
49
-
50
- -- Retry-safe replay detection BEFORE rejecting consumed records.
51
- -- The rotation nonce uniquely identifies the rotation: when the consumed
52
- -- record carries the same nonce hash, this is a retry of an already-applied
53
- -- rotation. The stored rotatedTo jti is returned (the retry's own freshly
54
- -- generated successor jti is discarded - it can never match).
55
- if s.status == 'consumed' then
56
- if ARGV[4] ~= '' and s.rotationNonceHash == ARGV[4] then
57
- return { 2, s.rotatedTo }
58
- end
59
- return -1
60
- end
61
-
62
- if s.status ~= 'active' then
63
- return -1
64
- end
65
-
66
- if tonumber(s.absoluteExpiresAt) <= now then
67
- redis.call('DEL', KEYS[1])
68
- redis.call('ZREM', KEYS[3], ARGV[6])
69
- return -2
70
- end
71
-
72
- if ARGV[3] ~= '' and tostring(s.version) ~= ARGV[3] then
73
- return -3
74
- end
75
-
76
- -- Successor collision: cryptographically improbable, but never overwrite.
77
- if redis.call('EXISTS', KEYS[2]) == 1 then
78
- return -4
79
- end
80
-
81
- -- Build the successor record; server time is authoritative for ordering.
82
- local nextEnv = cjson.decode(ARGV[1])
83
- local ns = nextEnv.s
84
-
85
- ns.createdAt = now
86
- ns.lastAccessedAt = now
87
- ns.rotatedFrom = s.jti
88
- ns.version = 1
89
-
90
- if ns.idleExpiresAt and ns.idleExpiresAt ~= cjson.null then
91
- ns.idleExpiresAt = math.min(tonumber(ns.idleExpiresAt), tonumber(ns.absoluteExpiresAt))
92
- end
93
-
94
- local nextTtl = math.max(1, tonumber(ns.absoluteExpiresAt) - now)
95
-
96
- -- Consume the old session.
97
- s.status = 'consumed'
98
- s.consumedAt = now
99
- s.rotatedTo = ARGV[2]
100
-
101
- if ARGV[4] ~= '' then
102
- s.rotationNonceHash = ARGV[4]
103
- else
104
- s.rotationNonceHash = cjson.null
105
- end
106
-
107
- local oldTtl = math.max(1, tonumber(s.absoluteExpiresAt) - now)
108
-
109
- if ARGV[5] == '1' then
110
- -- Short-lived consumed tombstone for replay detection (bounded TTL).
111
- redis.call('SET', KEYS[1], cjson.encode(env), 'EX', oldTtl)
112
- else
113
- redis.call('DEL', KEYS[1])
114
- end
115
-
116
- redis.call('SET', KEYS[2], cjson.encode(nextEnv), 'EX', nextTtl)
117
- redis.call('ZREM', KEYS[3], ARGV[6])
118
- -- Microsecond-resolution ordering score - see create.lua for why
119
- -- second-granularity scores break oldest-first eviction ordering.
120
- redis.call('ZADD', KEYS[3], tonumber(t[1]) + (tonumber(t[2]) / 1000000), ARGV[2])
121
-
122
- return { 1, ARGV[2] }
@@ -1,89 +0,0 @@
1
- -- touch-encrypted.lua (version 1) - encrypted (v2) envelopes
2
- -- Throttled, monotonic activity refresh for encrypted sessions.
3
- --
4
- -- The ciphertext cannot be decoded inside Lua, so the app re-encrypts the
5
- -- record (with its own clock) and passes the new envelope as ARGV[4]. The
6
- -- script validates state via the plaintext header mirrors and enforces
7
- -- monotonicity: a stale request can never move lastAccessedAt backwards.
8
- --
9
- -- KEYS[1] = session record key
10
- --
11
- -- ARGV[1] = touchInterval (seconds)
12
- -- ARGV[2] = idleTimeout (seconds, '' when disabled)
13
- -- ARGV[3] = force (1 = ignore throttle)
14
- -- ARGV[4] = new serialized envelope (re-encrypted with the current key)
15
- -- ARGV[5] = new lastAccessedAt (app clock, seconds)
16
- -- ARGV[6] = new idleExpiresAt (app clock, seconds, '' when disabled)
17
- -- ARGV[7] = new TTL (absoluteExpiresAt - now, clamped >= 1)
18
- --
19
- -- Returns:
20
- -- 1 touched (write performed)
21
- -- 2 skipped: inside touchInterval
22
- -- 3 skipped: request older than recorded activity
23
- -- 0 not found
24
- -- -1 consumed or revoked
25
- -- -2 absolute expiry passed; record deleted
26
- -- -3 idle expired; NOT resurrected
27
- -- 4 envelope is plain (caller must use the plain touch path)
28
- -- 5 stale request: ARGV[5] older than recorded activity
29
- local raw = redis.call('GET', KEYS[1])
30
-
31
- if not raw then
32
- return 0
33
- end
34
-
35
- local env = cjson.decode(raw)
36
-
37
- if env.v ~= 2 then
38
- return 4
39
- end
40
-
41
- if env.st ~= 'active' then
42
- return -1
43
- end
44
-
45
- local now = tonumber(redis.call('TIME')[1])
46
-
47
- if tonumber(env.exp) <= now then
48
- redis.call('DEL', KEYS[1])
49
- return -2
50
- end
51
-
52
- if env.idle ~= cjson.null and tonumber(env.idle) <= now then
53
- return -3
54
- end
55
-
56
- local interval = tonumber(ARGV[1])
57
-
58
- if tonumber(ARGV[3]) ~= 1 and now - tonumber(env.la) < interval then
59
- return 2
60
- end
61
-
62
- -- Monotonic guard: a stale app request must never regress activity.
63
- -- Equal-second requests are allowed (the app clock is second-granular and
64
- -- cannot advance within the same second); only strictly older writes are
65
- -- rejected.
66
- local newLa = tonumber(ARGV[5])
67
-
68
- if newLa < tonumber(env.la) then
69
- return 5
70
- end
71
-
72
- local newEnv = cjson.decode(ARGV[4])
73
-
74
- -- Keep the plaintext header mirrors in sync with the checked values.
75
- newEnv.st = env.st
76
- newEnv.la = newLa
77
- if ARGV[6] ~= '' then
78
- newEnv.idle = tonumber(ARGV[6])
79
- else
80
- newEnv.idle = cjson.null
81
- end
82
- newEnv.ver = env.ver
83
- newEnv.exp = env.exp
84
- newEnv.rn = env.rn
85
- newEnv.rj = env.rj
86
-
87
- redis.call('SET', KEYS[1], cjson.encode(newEnv), 'EX', tonumber(ARGV[7]))
88
-
89
- return 1
@@ -1,72 +0,0 @@
1
- -- touch.lua (version 1) - plain (v1) envelopes
2
- -- Throttled, monotonic activity refresh for plain sessions.
3
- --
4
- -- The script is authoritative for all state checks (exists, status,
5
- -- absolute expiry, idle expiry, throttle). Timestamps come from Redis
6
- -- server time, so distributed application clocks cannot race each other.
7
- --
8
- -- KEYS[1] = session record key
9
- --
10
- -- ARGV[1] = touchInterval (seconds)
11
- -- ARGV[2] = idleTimeout (seconds, '' when idle timeout disabled)
12
- -- ARGV[3] = force (1 = ignore throttle)
13
- --
14
- -- Returns:
15
- -- 1 touched (write performed)
16
- -- 2 skipped: inside touchInterval
17
- -- 3 skipped: request older than recorded activity (not possible with
18
- -- server time; retained for parity with the encrypted path)
19
- -- 0 not found
20
- -- -1 consumed or revoked
21
- -- -2 absolute expiry passed; record deleted
22
- -- -3 idle expired; NOT resurrected
23
- -- 4 envelope is encrypted (caller must use the encrypted touch path)
24
- local raw = redis.call('GET', KEYS[1])
25
-
26
- if not raw then
27
- return 0
28
- end
29
-
30
- local env = cjson.decode(raw)
31
-
32
- if env.v ~= 1 then
33
- return 4
34
- end
35
-
36
- local s = env.s
37
-
38
- if s.status ~= 'active' then
39
- return -1
40
- end
41
-
42
- local now = tonumber(redis.call('TIME')[1])
43
-
44
- if tonumber(s.absoluteExpiresAt) <= now then
45
- redis.call('DEL', KEYS[1])
46
- return -2
47
- end
48
-
49
- if s.idleExpiresAt and s.idleExpiresAt ~= cjson.null and tonumber(s.idleExpiresAt) <= now then
50
- return -3
51
- end
52
-
53
- local interval = tonumber(ARGV[1])
54
-
55
- if tonumber(ARGV[3]) ~= 1 and now - tonumber(s.lastAccessedAt) < interval then
56
- return 2
57
- end
58
-
59
- s.lastAccessedAt = now
60
-
61
- if ARGV[2] ~= '' then
62
- local idle = now + tonumber(ARGV[2])
63
- local abs = tonumber(s.absoluteExpiresAt)
64
- -- Rolling extends the idle boundary but NEVER the absolute boundary.
65
- s.idleExpiresAt = math.min(idle, abs)
66
- end
67
-
68
- local ttl = math.max(1, tonumber(s.absoluteExpiresAt) - now)
69
-
70
- redis.call('SET', KEYS[1], cjson.encode(env), 'EX', ttl)
71
-
72
- return 1