ioredis-toolkit 0.0.10 → 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 -1613
- 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 -797
- package/dist/cache.js +0 -1115
- package/dist/client.d.ts +0 -287
- package/dist/client.js +0 -1113
- 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 -233
- package/dist/lock.js +0 -440
- 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 -149
- package/dist/session/scripts/rotate.lua +0 -167
- 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 -355
- package/dist/session/session-config.js +0 -171
- 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 -64
- package/dist/session/session-keys.js +0 -128
- 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 -41
- package/dist/session/session-metrics.js +0 -135
- package/dist/session/session-repository.d.ts +0 -184
- package/dist/session/session-repository.js +0 -763
- 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 -267
- package/dist/session/session-service.d.ts +0 -123
- package/dist/session/session-service.js +0 -670
- 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 -281
- 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
|
@@ -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,149 +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/fam) and Redis server time; the header mirrors
|
|
9
|
-
-- of the written payloads are overwritten from the checked values so a
|
|
10
|
-
-- stale app payload can never misrepresent state.
|
|
11
|
-
--
|
|
12
|
-
-- Token-family reuse detection: mirrors rotate.lua (see that file for the
|
|
13
|
-
-- full rationale). The plaintext `fam` header mirror lets this script make
|
|
14
|
-
-- the same family-head decision the plain path makes, even though it can
|
|
15
|
-
-- never see or alter the ciphertext itself: the app is responsible for
|
|
16
|
-
-- baking the correct familyId into both the ciphertext and the `fam`
|
|
17
|
-
-- mirror before calling this script (see SessionRepository.rotate), and
|
|
18
|
-
-- this script cross-checks/forces the mirror the same way it already
|
|
19
|
-
-- forces st/ver/la/idle/exp/rn/rj - if the app ever got it wrong, the next
|
|
20
|
-
-- read's assertHeaderMatches() catches the ciphertext/header disagreement
|
|
21
|
-
-- and fails closed rather than silently establishing the wrong lineage.
|
|
22
|
-
--
|
|
23
|
-
-- KEYS[1] = old session record key
|
|
24
|
-
-- KEYS[2] = successor session record key
|
|
25
|
-
-- KEYS[3] = user session index key
|
|
26
|
-
--
|
|
27
|
-
-- ARGV[1] = consumed serialized envelope (app-built, re-encrypted)
|
|
28
|
-
-- ARGV[2] = successor serialized envelope (app-built, re-encrypted)
|
|
29
|
-
-- ARGV[3] = successor jti
|
|
30
|
-
-- ARGV[4] = expected version of the old session ('' = no check)
|
|
31
|
-
-- ARGV[5] = rotation nonce hash ('' = none)
|
|
32
|
-
-- ARGV[6] = retain consumed tombstone (1/0)
|
|
33
|
-
-- ARGV[7] = old jti
|
|
34
|
-
-- ARGV[8] = successor TTL (clamped >= 1)
|
|
35
|
-
-- ARGV[9] = consumed tombstone TTL (clamped >= 1)
|
|
36
|
-
-- ARGV[10] = session key prefix for this user (same hash tag => same slot)
|
|
37
|
-
-- ARGV[11] = family-head key prefix for this user (same hash tag)
|
|
38
|
-
-- ARGV[12] = revoke the family head on genuine replay (1/0)
|
|
39
|
-
--
|
|
40
|
-
-- Returns: same codes as rotate.lua (1/2/0/-1/-2/-3/-4/5/{-6,familyId,headJti}),
|
|
41
|
-
-- plus 6 when the envelope is plain (caller must use the plain path).
|
|
42
|
-
local raw = redis.call('GET', KEYS[1])
|
|
43
|
-
|
|
44
|
-
if not raw then
|
|
45
|
-
return 0
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
local env = cjson.decode(raw)
|
|
49
|
-
|
|
50
|
-
if env.v ~= 2 then
|
|
51
|
-
return 6
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
local t = redis.call('TIME')
|
|
55
|
-
local now = tonumber(t[1])
|
|
56
|
-
|
|
57
|
-
-- Immutable identity field: unchanged across every rotation of this
|
|
58
|
-
-- lineage. Legacy pre-migration records lacking it adopt their own jti as
|
|
59
|
-
-- the familyId from this point forward (self-healing) - see rotate.lua.
|
|
60
|
-
local familyId = env.fam
|
|
61
|
-
if familyId == nil or familyId == cjson.null then
|
|
62
|
-
familyId = ARGV[7]
|
|
63
|
-
end
|
|
64
|
-
local familyHeadKey = ARGV[11] .. familyId
|
|
65
|
-
|
|
66
|
-
-- Retry-safe replay detection BEFORE rejecting consumed records.
|
|
67
|
-
-- The rotation nonce uniquely identifies the rotation: when the consumed
|
|
68
|
-
-- record carries the same nonce hash, this is a retry of an already-applied
|
|
69
|
-
-- rotation. The stored rotatedTo jti is returned (the retry's own freshly
|
|
70
|
-
-- generated successor jti is discarded - it can never match).
|
|
71
|
-
if env.st == 'consumed' then
|
|
72
|
-
if ARGV[5] ~= '' and env.rn == ARGV[5] then
|
|
73
|
-
return { 2, env.rj }
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
-- Genuine reuse of an already-rotated-away token.
|
|
77
|
-
if ARGV[12] == '1' then
|
|
78
|
-
local headJti = redis.call('GET', familyHeadKey)
|
|
79
|
-
if headJti then
|
|
80
|
-
redis.call('DEL', ARGV[10] .. headJti)
|
|
81
|
-
redis.call('ZREM', KEYS[3], headJti)
|
|
82
|
-
end
|
|
83
|
-
redis.call('DEL', familyHeadKey)
|
|
84
|
-
return { -6, familyId, headJti or '' }
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
return -1
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
if env.st ~= 'active' then
|
|
91
|
-
return -1
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
if tonumber(env.exp) <= now then
|
|
95
|
-
redis.call('DEL', KEYS[1])
|
|
96
|
-
redis.call('ZREM', KEYS[3], ARGV[7])
|
|
97
|
-
return -2
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
if ARGV[4] ~= '' and tostring(env.ver) ~= ARGV[4] then
|
|
101
|
-
return -3
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
if redis.call('EXISTS', KEYS[2]) == 1 then
|
|
105
|
-
return -4
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
local consumedEnv = cjson.decode(ARGV[1])
|
|
109
|
-
consumedEnv.st = 'consumed'
|
|
110
|
-
consumedEnv.ver = env.ver
|
|
111
|
-
consumedEnv.la = env.la
|
|
112
|
-
consumedEnv.idle = env.idle
|
|
113
|
-
consumedEnv.exp = env.exp
|
|
114
|
-
consumedEnv.rn = cjson.null
|
|
115
|
-
consumedEnv.rj = ARGV[3]
|
|
116
|
-
consumedEnv.fam = familyId
|
|
117
|
-
|
|
118
|
-
if ARGV[5] ~= '' then
|
|
119
|
-
consumedEnv.rn = ARGV[5]
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
local nextEnv = cjson.decode(ARGV[2])
|
|
123
|
-
nextEnv.st = 'active'
|
|
124
|
-
nextEnv.ver = 1
|
|
125
|
-
nextEnv.la = now
|
|
126
|
-
if nextEnv.idle ~= cjson.null then
|
|
127
|
-
nextEnv.idle = math.min(tonumber(nextEnv.idle), tonumber(nextEnv.exp))
|
|
128
|
-
end
|
|
129
|
-
nextEnv.rn = cjson.null
|
|
130
|
-
nextEnv.rj = cjson.null
|
|
131
|
-
nextEnv.fam = familyId
|
|
132
|
-
|
|
133
|
-
local nextTtl = math.max(1, tonumber(ARGV[8]))
|
|
134
|
-
|
|
135
|
-
if ARGV[6] == '1' then
|
|
136
|
-
redis.call('SET', KEYS[1], cjson.encode(consumedEnv), 'EX', math.max(1, tonumber(ARGV[9])))
|
|
137
|
-
else
|
|
138
|
-
redis.call('DEL', KEYS[1])
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
redis.call('SET', KEYS[2], cjson.encode(nextEnv), 'EX', nextTtl)
|
|
142
|
-
redis.call('ZREM', KEYS[3], ARGV[7])
|
|
143
|
-
-- Microsecond-resolution ordering score - see create.lua for why
|
|
144
|
-
-- second-granularity scores break oldest-first eviction ordering.
|
|
145
|
-
redis.call('ZADD', KEYS[3], tonumber(t[1]) + (tonumber(t[2]) / 1000000), ARGV[3])
|
|
146
|
-
-- Advance the family head to the new generation.
|
|
147
|
-
redis.call('SET', familyHeadKey, ARGV[3], 'EX', nextTtl)
|
|
148
|
-
|
|
149
|
-
return { 1, ARGV[3] }
|
|
@@ -1,167 +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
|
-
-- Token-family reuse detection: every session carries a stable familyId
|
|
15
|
-
-- (the first generation's own jti), unchanged across rotations. A
|
|
16
|
-
-- {userId}-scoped "family-head" pointer key tracks the jti of the family's
|
|
17
|
-
-- current active generation, updated atomically on every successful
|
|
18
|
-
-- rotation. When a CONSUMED predecessor is replayed for real (not a
|
|
19
|
-
-- same-nonce retry), that is a strong signal the old token was stolen: if
|
|
20
|
-
-- ARGV[9] enables it, the script atomically revokes whatever the family
|
|
21
|
-
-- head currently points to (the live session an attacker could otherwise
|
|
22
|
-
-- keep using) and clears the head pointer, so the entire lineage dies
|
|
23
|
-
-- rather than just rejecting the one replayed request. The head pointer is
|
|
24
|
-
-- purely a defensive correlation key: per I7 it is never consulted by
|
|
25
|
-
-- validate() and can never itself grant authentication.
|
|
26
|
-
--
|
|
27
|
-
-- KEYS[1] = old session record key
|
|
28
|
-
-- KEYS[2] = successor session record key
|
|
29
|
-
-- KEYS[3] = user session index key
|
|
30
|
-
--
|
|
31
|
-
-- ARGV[1] = successor serialized envelope (app-built static fields)
|
|
32
|
-
-- ARGV[2] = successor jti
|
|
33
|
-
-- ARGV[3] = expected version of the old session ('' = no check)
|
|
34
|
-
-- ARGV[4] = rotation nonce hash ('' = none)
|
|
35
|
-
-- ARGV[5] = retain consumed tombstone (1/0)
|
|
36
|
-
-- ARGV[6] = old jti
|
|
37
|
-
-- ARGV[7] = session key prefix for this user (same hash tag => same slot)
|
|
38
|
-
-- ARGV[8] = family-head key prefix for this user (same hash tag)
|
|
39
|
-
-- ARGV[9] = revoke the family head on genuine replay (1/0)
|
|
40
|
-
--
|
|
41
|
-
-- Returns:
|
|
42
|
-
-- {1, successorJti} success
|
|
43
|
-
-- {2, successorJti} idempotent replay of a rotation with the same nonce
|
|
44
|
-
-- 0 old session not found
|
|
45
|
-
-- -1 already consumed / revoked (replay; family not
|
|
46
|
-
-- touched - either ARGV[9] is off, or the status
|
|
47
|
-
-- was already 'revoked' rather than 'consumed')
|
|
48
|
-
-- {-6, familyId, headJti} genuine replay of a consumed predecessor: the
|
|
49
|
-
-- family head (headJti, or "" if none was set)
|
|
50
|
-
-- was revoked and the family head pointer cleared
|
|
51
|
-
-- -2 old session expired (record removed)
|
|
52
|
-
-- -3 version conflict
|
|
53
|
-
-- -4 successor jti collision
|
|
54
|
-
-- 5 envelope is encrypted (use the encrypted path)
|
|
55
|
-
local raw = redis.call('GET', KEYS[1])
|
|
56
|
-
|
|
57
|
-
if not raw then
|
|
58
|
-
return 0
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
local env = cjson.decode(raw)
|
|
62
|
-
|
|
63
|
-
if env.v ~= 1 then
|
|
64
|
-
return 5
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
local s = env.s
|
|
68
|
-
local t = redis.call('TIME')
|
|
69
|
-
local now = tonumber(t[1])
|
|
70
|
-
|
|
71
|
-
-- Immutable identity field: unchanged across every rotation of this
|
|
72
|
-
-- lineage. Legacy pre-migration records lacking it adopt their own jti as
|
|
73
|
-
-- the familyId from this point forward (self-healing).
|
|
74
|
-
local familyId = s.familyId
|
|
75
|
-
if familyId == nil or familyId == cjson.null then
|
|
76
|
-
familyId = s.jti
|
|
77
|
-
end
|
|
78
|
-
local familyHeadKey = ARGV[8] .. familyId
|
|
79
|
-
|
|
80
|
-
-- Retry-safe replay detection BEFORE rejecting consumed records.
|
|
81
|
-
-- The rotation nonce uniquely identifies the rotation: when the consumed
|
|
82
|
-
-- record carries the same nonce hash, this is a retry of an already-applied
|
|
83
|
-
-- rotation. The stored rotatedTo jti is returned (the retry's own freshly
|
|
84
|
-
-- generated successor jti is discarded - it can never match).
|
|
85
|
-
if s.status == 'consumed' then
|
|
86
|
-
if ARGV[4] ~= '' and s.rotationNonceHash == ARGV[4] then
|
|
87
|
-
return { 2, s.rotatedTo }
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
-- Genuine reuse of an already-rotated-away token.
|
|
91
|
-
if ARGV[9] == '1' then
|
|
92
|
-
local headJti = redis.call('GET', familyHeadKey)
|
|
93
|
-
if headJti then
|
|
94
|
-
redis.call('DEL', ARGV[7] .. headJti)
|
|
95
|
-
redis.call('ZREM', KEYS[3], headJti)
|
|
96
|
-
end
|
|
97
|
-
redis.call('DEL', familyHeadKey)
|
|
98
|
-
return { -6, familyId, headJti or '' }
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
return -1
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
if s.status ~= 'active' then
|
|
105
|
-
return -1
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
if tonumber(s.absoluteExpiresAt) <= now then
|
|
109
|
-
redis.call('DEL', KEYS[1])
|
|
110
|
-
redis.call('ZREM', KEYS[3], ARGV[6])
|
|
111
|
-
return -2
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
if ARGV[3] ~= '' and tostring(s.version) ~= ARGV[3] then
|
|
115
|
-
return -3
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
-- Successor collision: cryptographically improbable, but never overwrite.
|
|
119
|
-
if redis.call('EXISTS', KEYS[2]) == 1 then
|
|
120
|
-
return -4
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
-- Build the successor record; server time is authoritative for ordering.
|
|
124
|
-
local nextEnv = cjson.decode(ARGV[1])
|
|
125
|
-
local ns = nextEnv.s
|
|
126
|
-
|
|
127
|
-
ns.createdAt = now
|
|
128
|
-
ns.lastAccessedAt = now
|
|
129
|
-
ns.rotatedFrom = s.jti
|
|
130
|
-
ns.familyId = familyId
|
|
131
|
-
ns.version = 1
|
|
132
|
-
|
|
133
|
-
if ns.idleExpiresAt and ns.idleExpiresAt ~= cjson.null then
|
|
134
|
-
ns.idleExpiresAt = math.min(tonumber(ns.idleExpiresAt), tonumber(ns.absoluteExpiresAt))
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
local nextTtl = math.max(1, tonumber(ns.absoluteExpiresAt) - now)
|
|
138
|
-
|
|
139
|
-
-- Consume the old session.
|
|
140
|
-
s.status = 'consumed'
|
|
141
|
-
s.consumedAt = now
|
|
142
|
-
s.rotatedTo = ARGV[2]
|
|
143
|
-
|
|
144
|
-
if ARGV[4] ~= '' then
|
|
145
|
-
s.rotationNonceHash = ARGV[4]
|
|
146
|
-
else
|
|
147
|
-
s.rotationNonceHash = cjson.null
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
local oldTtl = math.max(1, tonumber(s.absoluteExpiresAt) - now)
|
|
151
|
-
|
|
152
|
-
if ARGV[5] == '1' then
|
|
153
|
-
-- Short-lived consumed tombstone for replay detection (bounded TTL).
|
|
154
|
-
redis.call('SET', KEYS[1], cjson.encode(env), 'EX', oldTtl)
|
|
155
|
-
else
|
|
156
|
-
redis.call('DEL', KEYS[1])
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
redis.call('SET', KEYS[2], cjson.encode(nextEnv), 'EX', nextTtl)
|
|
160
|
-
redis.call('ZREM', KEYS[3], ARGV[6])
|
|
161
|
-
-- Microsecond-resolution ordering score - see create.lua for why
|
|
162
|
-
-- second-granularity scores break oldest-first eviction ordering.
|
|
163
|
-
redis.call('ZADD', KEYS[3], tonumber(t[1]) + (tonumber(t[2]) / 1000000), ARGV[2])
|
|
164
|
-
-- Advance the family head to the new generation.
|
|
165
|
-
redis.call('SET', familyHeadKey, ARGV[2], 'EX', nextTtl)
|
|
166
|
-
|
|
167
|
-
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
|