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.
Files changed (267) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/LICENSE +1 -1
  3. package/README.md +68 -1613
  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 -797
  198. package/dist/cache.js +0 -1115
  199. package/dist/client.d.ts +0 -287
  200. package/dist/client.js +0 -1113
  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 -233
  210. package/dist/lock.js +0 -440
  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 -149
  230. package/dist/session/scripts/rotate.lua +0 -167
  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 -355
  237. package/dist/session/session-config.js +0 -171
  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 -64
  247. package/dist/session/session-keys.js +0 -128
  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 -41
  251. package/dist/session/session-metrics.js +0 -135
  252. package/dist/session/session-repository.d.ts +0 -184
  253. package/dist/session/session-repository.js +0 -763
  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 -267
  258. package/dist/session/session-service.d.ts +0 -123
  259. package/dist/session/session-service.js +0 -670
  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 -281
  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,36 +0,0 @@
1
- import type { RedisClientWrapper } from '../client.js';
2
- export declare const SCRIPT_NAMES: readonly ["create", "touch", "touchEncrypted", "rotate", "rotateEncrypted", "delete", "revoke", "conditionalUpdate", "conditionalUpdateEncrypted", "deleteByUser", "cleanupIndex", "enforceLimit", "validate"];
3
- export type ScriptName = (typeof SCRIPT_NAMES)[number];
4
- /** Loads a script source from disk relative to this module. */
5
- export declare function loadScriptSource(name: ScriptName): string;
6
- export interface SessionScriptRegistryOptions {
7
- /** Override script sources (used by tests to inject fixtures). */
8
- sources?: Partial<Record<ScriptName, string>>;
9
- }
10
- /**
11
- * Loads and executes the session Lua scripts through a single client.
12
- * Stateless besides the script sources and SHA digests.
13
- */
14
- export declare class SessionScriptRegistry {
15
- private readonly sources;
16
- private readonly client;
17
- private shas;
18
- private loaded;
19
- constructor(client: RedisClientWrapper, options?: SessionScriptRegistryOptions);
20
- /** Returns the raw source of a script (for tests and audits). */
21
- source(name: ScriptName): string;
22
- /**
23
- * Pre-loads every script with SCRIPT LOAD. Best-effort: when loading
24
- * fails (e.g. Redis briefly unavailable at startup), the EVALSHA +
25
- * NOSCRIPT fallback keeps working, so authentication is never blocked
26
- * by a failed preload.
27
- */
28
- preload(): Promise<void>;
29
- /**
30
- * Executes a script by name with EVALSHA, falling back to EVAL on
31
- * NOSCRIPT (script cache evicted / node restarted).
32
- */
33
- eval(name: ScriptName, numKeys: number, ...args: Array<string | number | Buffer>): Promise<unknown>;
34
- /** Marks the registry dirty (e.g. after tests replaced the client). */
35
- invalidate(): void;
36
- }
@@ -1,130 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
- import { SessionConfigurationError } from './session-errors.js';
3
- /* -------------------------------------------------------------------------- */
4
- /* Lua script registry. */
5
- /* */
6
- /* Scripts live as versioned .lua files (src/session/scripts/) and are */
7
- /* loaded at construction. The registry handles SCRIPT LOAD + EVALSHA with */
8
- /* NOSCRIPT fallback, so scripts survive server script-cache eviction and */
9
- /* cluster node restarts. */
10
- /* */
11
- /* Script contract (enforced by review, not by machinery): */
12
- /* - every key is declared in KEYS and shares the user's hash slot */
13
- /* - no cross-slot keys, no KEYS/SCAN, no dynamic Lua construction */
14
- /* - stable integer result codes (documented in each script header) */
15
- /* - bounded loops (batch sizes passed in ARGV) */
16
- /* -------------------------------------------------------------------------- */
17
- export const SCRIPT_NAMES = [
18
- 'create',
19
- 'touch',
20
- 'touchEncrypted',
21
- 'rotate',
22
- 'rotateEncrypted',
23
- 'delete',
24
- 'revoke',
25
- 'conditionalUpdate',
26
- 'conditionalUpdateEncrypted',
27
- 'deleteByUser',
28
- 'cleanupIndex',
29
- 'enforceLimit',
30
- 'validate',
31
- ];
32
- const SCRIPT_FILE = {
33
- create: 'create.lua',
34
- touch: 'touch.lua',
35
- touchEncrypted: 'touch-encrypted.lua',
36
- rotate: 'rotate.lua',
37
- rotateEncrypted: 'rotate-encrypted.lua',
38
- delete: 'delete.lua',
39
- revoke: 'revoke.lua',
40
- conditionalUpdate: 'conditional-update.lua',
41
- conditionalUpdateEncrypted: 'conditional-update-encrypted.lua',
42
- deleteByUser: 'delete-by-user.lua',
43
- cleanupIndex: 'cleanup-index.lua',
44
- enforceLimit: 'enforce-limit.lua',
45
- validate: 'validate.lua',
46
- };
47
- /** Loads a script source from disk relative to this module. */
48
- export function loadScriptSource(name) {
49
- const url = new URL(`./scripts/${SCRIPT_FILE[name]}`, import.meta.url);
50
- try {
51
- return readFileSync(url, 'utf8');
52
- }
53
- catch (error) {
54
- throw new SessionConfigurationError(`Failed to load Lua script "${SCRIPT_FILE[name]}" (${url.pathname}): ${error instanceof Error ? error.message : String(error)}`);
55
- }
56
- }
57
- /**
58
- * Loads and executes the session Lua scripts through a single client.
59
- * Stateless besides the script sources and SHA digests.
60
- */
61
- export class SessionScriptRegistry {
62
- sources;
63
- client;
64
- shas = {};
65
- loaded = false;
66
- constructor(client, options = {}) {
67
- this.client = client;
68
- const sources = {};
69
- for (const name of SCRIPT_NAMES) {
70
- sources[name] = options.sources?.[name] ?? loadScriptSource(name);
71
- }
72
- this.sources = sources;
73
- }
74
- /** Returns the raw source of a script (for tests and audits). */
75
- source(name) {
76
- return this.sources[name];
77
- }
78
- /**
79
- * Pre-loads every script with SCRIPT LOAD. Best-effort: when loading
80
- * fails (e.g. Redis briefly unavailable at startup), the EVALSHA +
81
- * NOSCRIPT fallback keeps working, so authentication is never blocked
82
- * by a failed preload.
83
- */
84
- async preload() {
85
- if (this.loaded)
86
- return;
87
- const results = await Promise.allSettled(SCRIPT_NAMES.map((name) => this.client
88
- .scriptLoad(this.sources[name])
89
- .then((sha) => {
90
- this.shas[name] = sha;
91
- })));
92
- const failed = results.filter((r) => r.status === 'rejected').length;
93
- if (failed > 0) {
94
- // Scripts will still work via the EVAL fallback path.
95
- this.loaded = false;
96
- return;
97
- }
98
- this.loaded = true;
99
- }
100
- /**
101
- * Executes a script by name with EVALSHA, falling back to EVAL on
102
- * NOSCRIPT (script cache evicted / node restarted).
103
- */
104
- async eval(name, numKeys, ...args) {
105
- const sha = this.shas[name];
106
- const source = this.sources[name];
107
- if (sha) {
108
- try {
109
- return await this.client.evalsha(sha, source, numKeys, ...args);
110
- }
111
- catch (error) {
112
- const message = error instanceof Error ? error.message : String(error);
113
- if (!message.includes('NOSCRIPT'))
114
- throw error;
115
- // Fall through to EVAL and refresh the cached SHA.
116
- }
117
- }
118
- const result = await this.client.eval(source, numKeys, ...args);
119
- if (sha) {
120
- // EVAL succeeds only when the server has the script; update the SHA.
121
- this.shas[name] = sha;
122
- }
123
- return result;
124
- }
125
- /** Marks the registry dirty (e.g. after tests replaced the client). */
126
- invalidate() {
127
- this.loaded = false;
128
- this.shas = {};
129
- }
130
- }
@@ -1,42 +0,0 @@
1
- import type { SessionKeyProvider } from './session-encryption.js';
2
- import type { EncryptedSessionEnvelope, SessionRecord } from './session-types.js';
3
- export type EnvelopeKind = 'plain' | 'encrypted' | 'unknown';
4
- /**
5
- * Runtime validation of an arbitrary parsed value against the SessionRecord
6
- * shape. Returns the validated record or throws SessionSerializationError
7
- * with a machine-readable reason. Never casts blindly.
8
- */
9
- export declare function validateSessionRecord(value: unknown): SessionRecord;
10
- /**
11
- * Serializes a record into the plain v1 envelope.
12
- */
13
- export declare function serializeSession(record: SessionRecord): string;
14
- /**
15
- * Serializes a record into the encrypted v2 envelope, mirroring the
16
- * script-readable plaintext header from the record.
17
- */
18
- export declare function serializeEncryptedSession(record: SessionRecord, provider: SessionKeyProvider): string;
19
- /** Cheap kind detection without full parsing (for script mode selection). */
20
- export declare function envelopeKind(raw: string): EnvelopeKind;
21
- /**
22
- * Deserializes a stored envelope into a validated SessionRecord.
23
- *
24
- * @throws {SessionSerializationError} for unknown schema versions, malformed
25
- * JSON, malformed records, and encryption failures (auth tag, unknown key
26
- * version). The caller decides how to handle the corrupt record (invalidate
27
- * + clean up); this never crashes the process.
28
- */
29
- export declare function deserializeSession(raw: string, keyProvider?: SessionKeyProvider): SessionRecord;
30
- /**
31
- * Builds the plaintext header mirrors for an encrypted envelope from a
32
- * record. Used by the repository when re-encrypting on touch/rotate/update.
33
- */
34
- export declare function encryptedHeaderOf(record: SessionRecord): Pick<EncryptedSessionEnvelope, 'st' | 'ver' | 'la' | 'idle' | 'exp' | 'rn' | 'rj' | 'fam'>;
35
- /**
36
- * Verifies that a decrypted v2 record agrees with the envelope's plaintext
37
- * header mirrors. The ciphertext is authoritative; a disagreement means the
38
- * envelope was built from stale or inconsistent state and MUST fail closed.
39
- *
40
- * @throws {SessionSerializationError} on any mismatch.
41
- */
42
- export declare function assertHeaderMatches(envelope: EncryptedSessionEnvelope, record: SessionRecord): void;
@@ -1,267 +0,0 @@
1
- import { SessionSerializationError } from './session-errors.js';
2
- import { decryptJson, encryptJson } from './session-encryption.js';
3
- const BASE64URL = /^[A-Za-z0-9_-]+$/;
4
- function isString(v) {
5
- return typeof v === 'string';
6
- }
7
- function isNullish(v) {
8
- return v === null || v === undefined;
9
- }
10
- function isSecondsTimestamp(v) {
11
- return typeof v === 'number' && Number.isSafeInteger(v) && v > 0;
12
- }
13
- function isOptionalSecondsTimestamp(v) {
14
- return isNullish(v) || isSecondsTimestamp(v);
15
- }
16
- function isStatus(v) {
17
- return v === 'active' || v === 'consumed' || v === 'revoked';
18
- }
19
- function isOptionalString(v, maxLength = 1024) {
20
- return isNullish(v) || (typeof v === 'string' && v.length <= maxLength);
21
- }
22
- const MAX_METADATA_DEPTH = 5;
23
- const MAX_METADATA_KEYS = 128;
24
- function isPlainMetadata(v, depth = 0) {
25
- if (v === null || v === undefined)
26
- return true;
27
- if (typeof v === 'string' || typeof v === 'boolean')
28
- return true;
29
- if (typeof v === 'number')
30
- return Number.isFinite(v);
31
- if (Array.isArray(v)) {
32
- if (depth >= MAX_METADATA_DEPTH)
33
- return false;
34
- return v.every((item) => isPlainMetadata(item, depth + 1));
35
- }
36
- if (typeof v === 'object') {
37
- if (depth >= MAX_METADATA_DEPTH)
38
- return false;
39
- const keys = Object.keys(v);
40
- if (keys.length > MAX_METADATA_KEYS)
41
- return false;
42
- return keys.every((key) => key.length <= 128 && isPlainMetadata(v[key], depth + 1));
43
- }
44
- return false;
45
- }
46
- /**
47
- * Runtime validation of an arbitrary parsed value against the SessionRecord
48
- * shape. Returns the validated record or throws SessionSerializationError
49
- * with a machine-readable reason. Never casts blindly.
50
- */
51
- export function validateSessionRecord(value) {
52
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
53
- throw new SessionSerializationError({ reason: 'not_an_object' });
54
- }
55
- const r = value;
56
- const jti = r.jti;
57
- if (!isString(jti) || jti.length < 20 || jti.length > 100 || !BASE64URL.test(jti)) {
58
- throw new SessionSerializationError({ reason: 'invalid_jti' });
59
- }
60
- const userId = r.userId;
61
- if (!isString(userId) || userId.length === 0 || userId.length > 512) {
62
- throw new SessionSerializationError({ reason: 'invalid_user_id' });
63
- }
64
- const createdAt = r.createdAt;
65
- const lastAccessedAt = r.lastAccessedAt;
66
- const absoluteExpiresAt = r.absoluteExpiresAt;
67
- if (!isSecondsTimestamp(createdAt))
68
- throw new SessionSerializationError({ reason: 'invalid_created_at' });
69
- if (!isSecondsTimestamp(lastAccessedAt))
70
- throw new SessionSerializationError({ reason: 'invalid_last_accessed_at' });
71
- if (!isSecondsTimestamp(absoluteExpiresAt))
72
- throw new SessionSerializationError({ reason: 'invalid_absolute_expiry' });
73
- const idleExpiresAt = r.idleExpiresAt;
74
- if (!isOptionalSecondsTimestamp(idleExpiresAt)) {
75
- throw new SessionSerializationError({ reason: 'invalid_idle_expiry' });
76
- }
77
- const status = r.status;
78
- if (!isStatus(status))
79
- throw new SessionSerializationError({ reason: 'invalid_status' });
80
- const version = r.version;
81
- if (typeof version !== 'number' || !Number.isSafeInteger(version) || version < 0) {
82
- throw new SessionSerializationError({ reason: 'invalid_version' });
83
- }
84
- const securityVersion = r.securityVersion;
85
- if (!isNullish(securityVersion) && (typeof securityVersion !== 'number' || !Number.isSafeInteger(securityVersion) || securityVersion < 0)) {
86
- throw new SessionSerializationError({ reason: 'invalid_security_version' });
87
- }
88
- const validatedSecurityVersion = securityVersion ?? null;
89
- if (!isOptionalString(r.deviceId))
90
- throw new SessionSerializationError({ reason: 'invalid_device_id' });
91
- if (!isOptionalString(r.ipAddress))
92
- throw new SessionSerializationError({ reason: 'invalid_ip_address' });
93
- if (!isOptionalString(r.userAgent))
94
- throw new SessionSerializationError({ reason: 'invalid_user_agent' });
95
- const metadata = r.metadata;
96
- if (!isNullish(metadata) && !isPlainMetadata(metadata)) {
97
- throw new SessionSerializationError({ reason: 'invalid_metadata' });
98
- }
99
- if (!isNullish(r.rotatedFrom) && !isString(r.rotatedFrom)) {
100
- throw new SessionSerializationError({ reason: 'invalid_rotated_from' });
101
- }
102
- // familyId is an identity field, immutable across a lineage's rotations.
103
- // Legacy records written before this field existed lack it: they adopt
104
- // their own jti as the familyId (self-healing - see rotate.lua, which
105
- // does the same fallback for records it reads directly).
106
- let familyId;
107
- if (isNullish(r.familyId)) {
108
- familyId = jti;
109
- }
110
- else if (isString(r.familyId) && r.familyId.length >= 20 && r.familyId.length <= 100 && BASE64URL.test(r.familyId)) {
111
- familyId = r.familyId;
112
- }
113
- else {
114
- throw new SessionSerializationError({ reason: 'invalid_family_id' });
115
- }
116
- if (!isNullish(r.rotatedTo) && !isString(r.rotatedTo)) {
117
- throw new SessionSerializationError({ reason: 'invalid_rotated_to' });
118
- }
119
- if (!isNullish(r.consumedAt) && !isSecondsTimestamp(r.consumedAt)) {
120
- throw new SessionSerializationError({ reason: 'invalid_consumed_at' });
121
- }
122
- if (!isNullish(r.rotationNonceHash) && !isString(r.rotationNonceHash)) {
123
- throw new SessionSerializationError({ reason: 'invalid_rotation_nonce_hash' });
124
- }
125
- return {
126
- jti,
127
- userId,
128
- createdAt,
129
- lastAccessedAt,
130
- absoluteExpiresAt,
131
- idleExpiresAt: idleExpiresAt ?? null,
132
- status,
133
- version,
134
- securityVersion: validatedSecurityVersion,
135
- deviceId: r.deviceId ?? null,
136
- ipAddress: r.ipAddress ?? null,
137
- userAgent: r.userAgent ?? null,
138
- metadata: metadata ?? null,
139
- rotatedFrom: r.rotatedFrom ?? null,
140
- familyId,
141
- rotatedTo: r.rotatedTo ?? null,
142
- consumedAt: r.consumedAt ?? null,
143
- rotationNonceHash: r.rotationNonceHash ?? null,
144
- };
145
- }
146
- /**
147
- * Serializes a record into the plain v1 envelope.
148
- */
149
- export function serializeSession(record) {
150
- return JSON.stringify({ v: 1, s: record });
151
- }
152
- /**
153
- * Serializes a record into the encrypted v2 envelope, mirroring the
154
- * script-readable plaintext header from the record.
155
- */
156
- export function serializeEncryptedSession(record, provider) {
157
- const body = encryptJson(JSON.stringify(record), provider);
158
- const envelope = {
159
- v: 2,
160
- e: 1,
161
- ...body,
162
- st: record.status,
163
- ver: record.version,
164
- la: record.lastAccessedAt,
165
- idle: record.idleExpiresAt,
166
- exp: record.absoluteExpiresAt,
167
- rn: record.rotationNonceHash,
168
- rj: record.rotatedTo,
169
- fam: record.familyId,
170
- };
171
- return JSON.stringify(envelope);
172
- }
173
- /** Cheap kind detection without full parsing (for script mode selection). */
174
- export function envelopeKind(raw) {
175
- try {
176
- const parsed = JSON.parse(raw);
177
- if (parsed && typeof parsed === 'object' && parsed.v === 1)
178
- return 'plain';
179
- if (parsed && typeof parsed === 'object' && parsed.v === 2 && parsed.e === 1)
180
- return 'encrypted';
181
- return 'unknown';
182
- }
183
- catch {
184
- return 'unknown';
185
- }
186
- }
187
- /**
188
- * Deserializes a stored envelope into a validated SessionRecord.
189
- *
190
- * @throws {SessionSerializationError} for unknown schema versions, malformed
191
- * JSON, malformed records, and encryption failures (auth tag, unknown key
192
- * version). The caller decides how to handle the corrupt record (invalidate
193
- * + clean up); this never crashes the process.
194
- */
195
- export function deserializeSession(raw, keyProvider) {
196
- let parsed;
197
- try {
198
- parsed = JSON.parse(raw);
199
- }
200
- catch {
201
- throw new SessionSerializationError({ reason: 'invalid_json' });
202
- }
203
- if (typeof parsed !== 'object' || parsed === null) {
204
- throw new SessionSerializationError({ reason: 'not_an_object' });
205
- }
206
- const envelope = parsed;
207
- if (envelope.v === 1) {
208
- return validateSessionRecord(envelope.s);
209
- }
210
- if (envelope.v === 2) {
211
- if (!keyProvider) {
212
- throw new SessionSerializationError({ reason: 'encrypted_without_provider' });
213
- }
214
- const body = envelope;
215
- if (body.e !== 1) {
216
- throw new SessionSerializationError({ reason: 'unknown_encryption_version' });
217
- }
218
- return validateSessionRecord(decryptJson(body, keyProvider));
219
- }
220
- throw new SessionSerializationError({ reason: 'unsupported_schema_version' });
221
- }
222
- /**
223
- * Builds the plaintext header mirrors for an encrypted envelope from a
224
- * record. Used by the repository when re-encrypting on touch/rotate/update.
225
- */
226
- export function encryptedHeaderOf(record) {
227
- return {
228
- st: record.status,
229
- ver: record.version,
230
- la: record.lastAccessedAt,
231
- idle: record.idleExpiresAt,
232
- exp: record.absoluteExpiresAt,
233
- rn: record.rotationNonceHash,
234
- rj: record.rotatedTo,
235
- fam: record.familyId,
236
- };
237
- }
238
- /**
239
- * Verifies that a decrypted v2 record agrees with the envelope's plaintext
240
- * header mirrors. The ciphertext is authoritative; a disagreement means the
241
- * envelope was built from stale or inconsistent state and MUST fail closed.
242
- *
243
- * @throws {SessionSerializationError} on any mismatch.
244
- */
245
- export function assertHeaderMatches(envelope, record) {
246
- const header = encryptedHeaderOf(record);
247
- const mismatches = [];
248
- if (envelope.st !== header.st)
249
- mismatches.push('st');
250
- if (envelope.ver !== header.ver)
251
- mismatches.push('ver');
252
- if (envelope.la !== header.la)
253
- mismatches.push('la');
254
- if (envelope.idle !== header.idle)
255
- mismatches.push('idle');
256
- if (envelope.exp !== header.exp)
257
- mismatches.push('exp');
258
- if (envelope.rn !== header.rn)
259
- mismatches.push('rn');
260
- if (envelope.rj !== header.rj)
261
- mismatches.push('rj');
262
- if (envelope.fam !== header.fam)
263
- mismatches.push('fam');
264
- if (mismatches.length > 0) {
265
- throw new SessionSerializationError({ reason: 'header_mismatch', fields: mismatches });
266
- }
267
- }
@@ -1,123 +0,0 @@
1
- import type { RedisClientWrapper } from '../client.js';
2
- import type { RevocationStore } from './session-types.js';
3
- import { SessionCircuitBreaker } from './session-circuit-breaker.js';
4
- import type { SessionConfig } from './session-config.js';
5
- import { SessionHealthChecker } from './session-health.js';
6
- import type { SessionKeyStrategy } from './session-keys.js';
7
- import { SessionMetrics } from './session-metrics.js';
8
- import { SessionRepository } from './session-repository.js';
9
- import type { SessionTokenManager } from './session-token.js';
10
- import type { CreatedSession, ListOptions, ReconcileUserResult, RotateOptions, RotatedSession, SessionCreateInput, SessionRecord, SessionUpdatePatch, SessionValidationResult, TouchOptions, TouchOutcome, UpdateOptions, ValidateOptions } from './session-types.js';
11
- export interface SessionServiceDeps {
12
- config: SessionConfig;
13
- client: RedisClientWrapper;
14
- repository: SessionRepository;
15
- token: SessionTokenManager;
16
- keys: SessionKeyStrategy;
17
- revocationStore?: RevocationStore;
18
- metrics?: SessionMetrics;
19
- circuitBreaker?: SessionCircuitBreaker;
20
- health?: SessionHealthChecker;
21
- now?: () => number;
22
- }
23
- export declare class SessionService {
24
- private readonly deps;
25
- private readonly throttle;
26
- constructor(deps: SessionServiceDeps);
27
- private get config();
28
- private get repository();
29
- private get metrics();
30
- private get breaker();
31
- private get healthChecker();
32
- private now;
33
- private guard;
34
- /**
35
- * Creates a session and returns the raw token exactly once.
36
- *
37
- * Idempotent creation: when `input.idempotencyKey` is provided (and
38
- * config.enableCreateIdempotency is on), the idempotencyKey IS the token.
39
- * A retry with the same key returns the existing session with
40
- * `replayed: true` instead of creating a duplicate.
41
- */
42
- create(input: SessionCreateInput): Promise<CreatedSession>;
43
- /**
44
- * Validates a session token. Single Redis round trip when userId is known.
45
- * Never throws for invalid sessions; throws only for infrastructure
46
- * failures (fail closed) and configuration errors.
47
- */
48
- validate(token: string, options?: ValidateOptions): Promise<SessionValidationResult>;
49
- /**
50
- * Refreshes activity. Throttled by touchInterval (in-script + in-memory
51
- * optimizations). Never resurrects an idle-expired session.
52
- */
53
- touch(token: string, options?: TouchOptions): Promise<TouchOutcome>;
54
- /**
55
- * Single-use atomic rotation with retry-safe idempotency (rotationNonce).
56
- */
57
- rotate(token: string, options?: RotateOptions): Promise<RotatedSession>;
58
- /**
59
- * Patch update of non-security fields (device/ip/ua/metadata) with
60
- * optimistic concurrency.
61
- */
62
- update(token: string, patch: SessionUpdatePatch, options?: UpdateOptions): Promise<SessionRecord>;
63
- /** Physically deletes a session (idempotent). */
64
- destroy(token: string, options?: {
65
- userId?: string;
66
- }): Promise<boolean>;
67
- /**
68
- * Logically revokes a session (keeps a bounded tombstone).
69
- * Returns 'revoked' | 'already_revoked' | 'not_found'.
70
- */
71
- revoke(token: string, options?: {
72
- userId?: string;
73
- }): Promise<string>;
74
- /** Revokes every session of a user (bounded, fail-closed on partial). */
75
- revokeAll(userId: string): Promise<number>;
76
- /** Deletes every session of a user (physical, bounded). */
77
- deleteByUser(userId: string): Promise<string[]>;
78
- /** Lists a user's sessions (oldest first). */
79
- findByUser(userId: string, options?: ListOptions): Promise<SessionRecord[]>;
80
- /** Alias of {@link findByUser} for listing. */
81
- list(userId: string, options?: ListOptions): Promise<SessionRecord[]>;
82
- /**
83
- * Sets (or bumps) the user's security version, invalidating every session
84
- * captured at an older version. Use after password/MFA changes.
85
- */
86
- setSecurityVersion(userId: string, version?: number): Promise<number>;
87
- getSecurityVersion(userId: string): Promise<number | null>;
88
- /**
89
- * Bounded administrative repair pass for one user: prunes stale
90
- * user-index entries and, when the global jti index is enabled, rewrites
91
- * any missing/stale jti-index entry for that user's live active sessions.
92
- *
93
- * This is NOT required for authentication correctness - every read path
94
- * (validate/touch/rotate) already treats the session record as
95
- * authoritative and self-heals stale index entries lazily. This exists
96
- * purely to shrink the window during which JTI-only lookup (`find(jti)`
97
- * without a known userId) can miss a live session after a partial write
98
- * (ยง67), and to give operators a way to proactively repair known drift
99
- * (e.g. after a Redis incident) instead of waiting for it to be hit
100
- * randomly. Safe to call repeatedly; every effect is idempotent.
101
- *
102
- * Bounded by config.limits.maxSessionsPerUserHardCap, same as
103
- * revokeAll/deleteByUser - never scans the cluster and is not called
104
- * from a hot auth path.
105
- */
106
- reconcileUser(userId: string): Promise<ReconcileUserResult>;
107
- /** Dependency health (PING latency + recent error rate). */
108
- health(): Promise<ReturnType<SessionHealthChecker['check']>>;
109
- /**
110
- * Resolves the userId for a jti: explicit when provided (fast path), via
111
- * the JTI index otherwise. Returns null when the index has no entry.
112
- */
113
- private resolveUserId;
114
- /**
115
- * Accepts tokens in the strict issued format (base64url of the configured
116
- * entropy) and caller-supplied idempotency keys (bounded printable ASCII),
117
- * which are used as tokens for idempotent creation. Rejects everything
118
- * else (DoS guard: bounded length, bounded alphabet).
119
- */
120
- private isAcceptableToken;
121
- private checkBinding;
122
- private bestEffortCleanup;
123
- }