ioredis-toolkit 0.0.1
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/LICENSE +21 -0
- package/README.md +645 -0
- package/dist/cache.d.ts +298 -0
- package/dist/cache.js +606 -0
- package/dist/client.d.ts +177 -0
- package/dist/client.js +958 -0
- package/dist/cluster-slot.d.ts +4 -0
- package/dist/cluster-slot.js +31 -0
- package/dist/cluster.d.ts +79 -0
- package/dist/cluster.js +156 -0
- package/dist/errors.d.ts +30 -0
- package/dist/errors.js +63 -0
- package/dist/health.d.ts +39 -0
- package/dist/health.js +106 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +44 -0
- package/dist/lock.d.ts +215 -0
- package/dist/lock.js +385 -0
- package/dist/logger.d.ts +12 -0
- package/dist/logger.js +40 -0
- package/dist/pubsub.d.ts +171 -0
- package/dist/pubsub.js +285 -0
- package/dist/ratelimiter.d.ts +162 -0
- package/dist/ratelimiter.js +289 -0
- package/dist/session/index.d.ts +23 -0
- package/dist/session/index.js +16 -0
- package/dist/session/revocation-store.d.ts +171 -0
- package/dist/session/revocation-store.js +310 -0
- package/dist/session/scripts/cleanup-index.lua +21 -0
- package/dist/session/scripts/conditional-update-encrypted.lua +60 -0
- package/dist/session/scripts/conditional-update.lua +63 -0
- package/dist/session/scripts/create.lua +68 -0
- package/dist/session/scripts/delete-by-user.lua +29 -0
- package/dist/session/scripts/delete.lua +15 -0
- package/dist/session/scripts/enforce-limit.lua +38 -0
- package/dist/session/scripts/revoke.lua +61 -0
- package/dist/session/scripts/rotate-encrypted.lua +107 -0
- package/dist/session/scripts/rotate.lua +119 -0
- package/dist/session/scripts/touch-encrypted.lua +89 -0
- package/dist/session/scripts/touch.lua +72 -0
- package/dist/session/scripts/validate.lua +90 -0
- package/dist/session/session-circuit-breaker.d.ts +42 -0
- package/dist/session/session-circuit-breaker.js +129 -0
- package/dist/session/session-config.d.ts +335 -0
- package/dist/session/session-config.js +162 -0
- package/dist/session/session-cookie.d.ts +72 -0
- package/dist/session/session-cookie.js +101 -0
- package/dist/session/session-encryption.d.ts +87 -0
- package/dist/session/session-encryption.js +139 -0
- package/dist/session/session-errors.d.ts +85 -0
- package/dist/session/session-errors.js +145 -0
- package/dist/session/session-health.d.ts +38 -0
- package/dist/session/session-health.js +60 -0
- package/dist/session/session-keys.d.ts +51 -0
- package/dist/session/session-keys.js +113 -0
- package/dist/session/session-manager.d.ts +59 -0
- package/dist/session/session-manager.js +94 -0
- package/dist/session/session-metrics.d.ts +33 -0
- package/dist/session/session-metrics.js +112 -0
- package/dist/session/session-repository.d.ts +161 -0
- package/dist/session/session-repository.js +683 -0
- package/dist/session/session-scripts.d.ts +36 -0
- package/dist/session/session-scripts.js +130 -0
- package/dist/session/session-serializer.d.ts +42 -0
- package/dist/session/session-serializer.js +248 -0
- package/dist/session/session-service.d.ts +104 -0
- package/dist/session/session-service.js +611 -0
- package/dist/session/session-token.d.ts +38 -0
- package/dist/session/session-token.js +86 -0
- package/dist/session/session-types.d.ts +253 -0
- package/dist/session/session-types.js +16 -0
- package/dist/types.d.ts +782 -0
- package/dist/types.js +140 -0
- package/package.json +97 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { SessionConfigurationError, SessionInvalidError } from './session-errors.js';
|
|
3
|
+
/* -------------------------------------------------------------------------- */
|
|
4
|
+
/* Session token security. */
|
|
5
|
+
/* */
|
|
6
|
+
/* token - cryptographically secure random bytes (>= 128 bits), encoded */
|
|
7
|
+
/* base64url. Generated with crypto.randomBytes(). */
|
|
8
|
+
/* jti - SHA-256(token) in base64url. The ONLY value persisted in */
|
|
9
|
+
/* Redis / logged / used as a key component. */
|
|
10
|
+
/* */
|
|
11
|
+
/* The raw token is never stored, logged, or embedded in keys/errors. */
|
|
12
|
+
/* -------------------------------------------------------------------------- */
|
|
13
|
+
const BASE64URL_REGEX = /^[A-Za-z0-9_-]+$/;
|
|
14
|
+
/** Regex matching the canonical base64url padding-free alphabet. */
|
|
15
|
+
function tokenRegex(bytes) {
|
|
16
|
+
return new RegExp(`^[A-Za-z0-9_-]{${Math.ceil((bytes * 4) / 3)}}$`);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Generates and hashes session tokens.
|
|
20
|
+
*
|
|
21
|
+
* All functions are synchronous; entropy comes from the OS CSPRNG.
|
|
22
|
+
*/
|
|
23
|
+
export class SessionTokenManager {
|
|
24
|
+
tokenBytes;
|
|
25
|
+
/**
|
|
26
|
+
* @param tokenBytes - Raw entropy in bytes (min 16 = 128 bits). 32 is
|
|
27
|
+
* recommended (256 bits).
|
|
28
|
+
*/
|
|
29
|
+
constructor(tokenBytes = 32) {
|
|
30
|
+
if (!Number.isInteger(tokenBytes) || tokenBytes < 16 || tokenBytes > 64) {
|
|
31
|
+
throw new SessionConfigurationError('tokenBytes must be an integer between 16 (128 bits) and 64 (512 bits).');
|
|
32
|
+
}
|
|
33
|
+
this.tokenBytes = tokenBytes;
|
|
34
|
+
}
|
|
35
|
+
/** Generates a new raw session token (base64url, no padding). */
|
|
36
|
+
generate() {
|
|
37
|
+
return randomBytes(this.tokenBytes).toString('base64url');
|
|
38
|
+
}
|
|
39
|
+
/** Generates a caller-supplied rotation nonce of the same strength. */
|
|
40
|
+
generateNonce() {
|
|
41
|
+
return randomBytes(this.tokenBytes).toString('base64url');
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* SHA-256 hashes a raw token into its persisted jti (base64url).
|
|
45
|
+
* The token itself is never stored; only this digest is.
|
|
46
|
+
*/
|
|
47
|
+
hash(token) {
|
|
48
|
+
return createHash('sha256').update(token, 'utf8').digest('base64url');
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Validates that a value looks like a well-formed raw token of the
|
|
52
|
+
* configured length. Used to reject garbage before hashing/lookup.
|
|
53
|
+
*/
|
|
54
|
+
validateFormat(token) {
|
|
55
|
+
if (typeof token !== 'string' || token.length === 0)
|
|
56
|
+
return false;
|
|
57
|
+
if (!BASE64URL_REGEX.test(token))
|
|
58
|
+
return false;
|
|
59
|
+
const expected = Math.ceil((this.tokenBytes * 4) / 3);
|
|
60
|
+
// Accept tokens of the configured length only (tokens from older configs
|
|
61
|
+
// are rejected as invalid rather than mislooked-up).
|
|
62
|
+
return token.length === expected;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Constant-time comparison of two strings (uses byte length, then
|
|
66
|
+
* timingSafeEqual). Returns false when lengths differ without leaking
|
|
67
|
+
* the difference.
|
|
68
|
+
*/
|
|
69
|
+
safeEquals(a, b) {
|
|
70
|
+
const aBuf = Buffer.from(a, 'utf8');
|
|
71
|
+
const bBuf = Buffer.from(b, 'utf8');
|
|
72
|
+
if (aBuf.length !== bBuf.length)
|
|
73
|
+
return false;
|
|
74
|
+
return timingSafeEqual(aBuf, bBuf);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Validates a token and returns its jti, or throws SessionInvalidError
|
|
78
|
+
* for malformed input (so a malformed token is never looked up).
|
|
79
|
+
*/
|
|
80
|
+
tokenToJti(token) {
|
|
81
|
+
if (!this.validateFormat(token)) {
|
|
82
|
+
throw new SessionInvalidError({ reason: 'malformed_token' });
|
|
83
|
+
}
|
|
84
|
+
return this.hash(token);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/** Lifecycle state of a session record. */
|
|
2
|
+
export type SessionStatus = 'active' | 'consumed' | 'revoked';
|
|
3
|
+
/**
|
|
4
|
+
* The persisted session record.
|
|
5
|
+
*
|
|
6
|
+
* Identity fields (jti, userId, createdAt) are immutable: they are set at
|
|
7
|
+
* creation and never touched again. All timestamps are Unix seconds.
|
|
8
|
+
*/
|
|
9
|
+
export type SessionRecord = {
|
|
10
|
+
/** Session id: SHA-256 hash of the raw session token, base64url encoded. */
|
|
11
|
+
jti: string;
|
|
12
|
+
/** Owner of the session. Identity field - immutable. */
|
|
13
|
+
userId: string;
|
|
14
|
+
/** Creation time (Unix seconds). Identity field - immutable. */
|
|
15
|
+
createdAt: number;
|
|
16
|
+
/** Last activity timestamp (Unix seconds), used for idle and touch logic. */
|
|
17
|
+
lastAccessedAt: number;
|
|
18
|
+
/**
|
|
19
|
+
* Absolute lifetime boundary (Unix seconds). Activity may extend the idle
|
|
20
|
+
* boundary but NEVER this one. The Redis TTL is derived from this value.
|
|
21
|
+
*/
|
|
22
|
+
absoluteExpiresAt: number;
|
|
23
|
+
/**
|
|
24
|
+
* Idle timeout boundary (Unix seconds), or null when idle timeout is
|
|
25
|
+
* disabled. A session with idleExpiresAt <= now is idle-expired and must
|
|
26
|
+
* not be extended by touch.
|
|
27
|
+
*/
|
|
28
|
+
idleExpiresAt: number | null;
|
|
29
|
+
/** Lifecycle state: active, consumed (by rotation), or revoked. */
|
|
30
|
+
status: SessionStatus;
|
|
31
|
+
/** Optimistic-concurrency version. Bumped on every security-relevant write. */
|
|
32
|
+
version: number;
|
|
33
|
+
/** Account security version captured at creation, or null when disabled. */
|
|
34
|
+
securityVersion: number | null;
|
|
35
|
+
/** Optional device identifier (advisory binding only). */
|
|
36
|
+
deviceId: string | null;
|
|
37
|
+
/** Optional IP address recorded at last access (advisory binding only). */
|
|
38
|
+
ipAddress: string | null;
|
|
39
|
+
/** Optional user agent (advisory binding only). */
|
|
40
|
+
userAgent: string | null;
|
|
41
|
+
/** Arbitrary application metadata. Bounded by config (maxMetadataSize). */
|
|
42
|
+
metadata: Record<string, unknown> | null;
|
|
43
|
+
/** JTI this session was rotated from (rotation chains, reuse detection). */
|
|
44
|
+
rotatedFrom: string | null;
|
|
45
|
+
/** JTI this session was rotated to (enables retry-safe rotation). */
|
|
46
|
+
rotatedTo: string | null;
|
|
47
|
+
/** When the session was consumed by a rotation (Unix seconds), or null. */
|
|
48
|
+
consumedAt: number | null;
|
|
49
|
+
/** Hash of the rotation nonce that consumed this session (retry safety). */
|
|
50
|
+
rotationNonceHash: string | null;
|
|
51
|
+
};
|
|
52
|
+
/** Input for session creation. Only caller-controlled fields. */
|
|
53
|
+
export type SessionCreateInput = {
|
|
54
|
+
userId: string;
|
|
55
|
+
/** Optional device identifier. Only stored when config.storeDeviceId is true. */
|
|
56
|
+
deviceId?: string;
|
|
57
|
+
/** Optional IP address. Only stored when config.storeIpAddress is true. */
|
|
58
|
+
ipAddress?: string;
|
|
59
|
+
/** Optional user agent. Only stored when config.storeUserAgent is true. */
|
|
60
|
+
userAgent?: string;
|
|
61
|
+
/** Arbitrary metadata (bounded by config.maxMetadataSize). */
|
|
62
|
+
metadata?: Record<string, unknown>;
|
|
63
|
+
/**
|
|
64
|
+
* Idempotency key: when provided, a create that was already applied with
|
|
65
|
+
* the same key returns the existing session instead of creating a duplicate.
|
|
66
|
+
* Requires config.enableCreateIdempotency (stores a short-lived claim).
|
|
67
|
+
*/
|
|
68
|
+
idempotencyKey?: string;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Mutable fields for {@link SessionUpdatePatch}. Every other field is
|
|
72
|
+
* identity or security-critical and can only change through dedicated
|
|
73
|
+
* operations (rotate, revoke, touch).
|
|
74
|
+
*/
|
|
75
|
+
export type SessionUpdatePatch = {
|
|
76
|
+
deviceId?: string | null;
|
|
77
|
+
ipAddress?: string | null;
|
|
78
|
+
userAgent?: string | null;
|
|
79
|
+
metadata?: Record<string, unknown> | null;
|
|
80
|
+
};
|
|
81
|
+
/** Result of a successful creation: the raw token is returned exactly once. */
|
|
82
|
+
export type CreatedSession = {
|
|
83
|
+
/** The raw session token. Give this to the client; store it nowhere. */
|
|
84
|
+
token: string;
|
|
85
|
+
/** The persisted session record (contains only the jti, never the token). */
|
|
86
|
+
session: SessionRecord;
|
|
87
|
+
/**
|
|
88
|
+
* True when the create was an idempotent replay (a previous attempt with
|
|
89
|
+
* the same idempotencyKey created the session). The token is the
|
|
90
|
+
* idempotencyKey itself, so it resolves to the same session.
|
|
91
|
+
*/
|
|
92
|
+
replayed?: boolean;
|
|
93
|
+
};
|
|
94
|
+
/** Result of a successful rotation. */
|
|
95
|
+
export type RotatedSession = {
|
|
96
|
+
/**
|
|
97
|
+
* The successor raw token. Give this to the client; store it nowhere.
|
|
98
|
+
* Absent on idempotent replays: the successor's token was only ever
|
|
99
|
+
* returned to the original caller, so a retry cannot recover it.
|
|
100
|
+
*/
|
|
101
|
+
token?: string;
|
|
102
|
+
/** The successor session record. */
|
|
103
|
+
session: SessionRecord;
|
|
104
|
+
/**
|
|
105
|
+
* True when the rotation was a retry of an already-applied rotation with
|
|
106
|
+
* the same rotation nonce (the response to the first attempt was lost).
|
|
107
|
+
*/
|
|
108
|
+
replayed: boolean;
|
|
109
|
+
};
|
|
110
|
+
/** Machine-readable invalidation reason for a session. */
|
|
111
|
+
export type SessionInvalidReason = 'not_found' | 'expired' | 'idle_timeout' | 'absolute_timeout' | 'revoked' | 'invalid' | 'binding_mismatch';
|
|
112
|
+
export type SessionValidationResult = {
|
|
113
|
+
valid: true;
|
|
114
|
+
session: SessionRecord;
|
|
115
|
+
binding?: BindingMismatch;
|
|
116
|
+
} | {
|
|
117
|
+
valid: false;
|
|
118
|
+
reason: SessionInvalidReason;
|
|
119
|
+
session?: never;
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* Touch outcome codes, mirroring the Lua script result codes.
|
|
123
|
+
* touched - a write was performed (idle boundary extended).
|
|
124
|
+
* skipped_throttled - inside touchInterval; no write performed.
|
|
125
|
+
* skipped_stale - request was older than recorded activity; no write.
|
|
126
|
+
* not_found - no record (or record gone).
|
|
127
|
+
* consumed - session was consumed by rotation / revoked.
|
|
128
|
+
* expired - absolute expiry passed; record removed.
|
|
129
|
+
* idle_expired - idle timeout passed; record NOT touched (not resurrected).
|
|
130
|
+
*/
|
|
131
|
+
export type TouchOutcome = 'touched' | 'skipped_throttled' | 'skipped_stale' | 'not_found' | 'consumed' | 'expired' | 'idle_expired';
|
|
132
|
+
/** Options for {@link SessionService.touch}. */
|
|
133
|
+
export type TouchOptions = {
|
|
134
|
+
/** Force a write regardless of touchInterval (rarely needed). */
|
|
135
|
+
force?: boolean;
|
|
136
|
+
/** When known, avoids the JTI lookup index round trip. */
|
|
137
|
+
userId?: string;
|
|
138
|
+
};
|
|
139
|
+
/** Options for {@link SessionService.validate}. */
|
|
140
|
+
export type ValidateOptions = {
|
|
141
|
+
/**
|
|
142
|
+
* When the caller already knows the user id (the common case in an
|
|
143
|
+
* authentication layer), passing it avoids the JTI lookup index round
|
|
144
|
+
* trip and makes validation a single Redis read.
|
|
145
|
+
*/
|
|
146
|
+
userId?: string;
|
|
147
|
+
/** Current IP address, compared against the stored value (binding policy). */
|
|
148
|
+
ipAddress?: string;
|
|
149
|
+
/** Current user agent, compared against the stored value (binding policy). */
|
|
150
|
+
userAgent?: string;
|
|
151
|
+
/** Current device id, compared against the stored value (binding policy). */
|
|
152
|
+
deviceId?: string;
|
|
153
|
+
};
|
|
154
|
+
/** Options for {@link SessionService.rotate}. */
|
|
155
|
+
export type RotateOptions = {
|
|
156
|
+
/**
|
|
157
|
+
* Client-supplied random nonce enabling retry-safe rotation. If the first
|
|
158
|
+
* rotation succeeded but the response was lost, retrying with the same
|
|
159
|
+
* nonce returns the already-created successor instead of a replay error.
|
|
160
|
+
*/
|
|
161
|
+
rotationNonce?: string;
|
|
162
|
+
/** Skip the pre-flight GET and let the Lua script be authoritative. */
|
|
163
|
+
userId?: string;
|
|
164
|
+
/** Optimistic concurrency: only rotate when the old record matches. */
|
|
165
|
+
expectedVersion?: number;
|
|
166
|
+
};
|
|
167
|
+
/** Options for {@link SessionService.update}. */
|
|
168
|
+
export type UpdateOptions = {
|
|
169
|
+
/**
|
|
170
|
+
* Optimistic concurrency: when set, the update only applies if the
|
|
171
|
+
* current record version matches. Otherwise throws SessionConcurrencyError.
|
|
172
|
+
*/
|
|
173
|
+
expectedVersion?: number;
|
|
174
|
+
/** When known, avoids the JTI lookup index round trip. */
|
|
175
|
+
userId?: string;
|
|
176
|
+
};
|
|
177
|
+
/** Options for {@link SessionService.list}. */
|
|
178
|
+
export type ListOptions = {
|
|
179
|
+
/** Maximum number of sessions to return (bounded pipeline). Default: 100. */
|
|
180
|
+
limit?: number;
|
|
181
|
+
/** Skip the first N sessions (oldest first). */
|
|
182
|
+
offset?: number;
|
|
183
|
+
/** Include consumed/revoked records in the result. Default: false. */
|
|
184
|
+
includeInactive?: boolean;
|
|
185
|
+
};
|
|
186
|
+
/** Advisory binding mismatch details (binding policy must be configured). */
|
|
187
|
+
export type BindingMismatch = {
|
|
188
|
+
ipAddress: boolean;
|
|
189
|
+
userAgent: boolean;
|
|
190
|
+
deviceId: boolean;
|
|
191
|
+
};
|
|
192
|
+
/** Schema version of the persisted envelope. */
|
|
193
|
+
export type SerializedSchemaVersion = 1 | 2;
|
|
194
|
+
/**
|
|
195
|
+
* Plain (unencrypted) persisted envelope.
|
|
196
|
+
* `v: 1` - plain JSON session record.
|
|
197
|
+
*/
|
|
198
|
+
export type PlainSessionEnvelope = {
|
|
199
|
+
v: 1;
|
|
200
|
+
s: SessionRecord;
|
|
201
|
+
};
|
|
202
|
+
/**
|
|
203
|
+
* Encrypted persisted envelope.
|
|
204
|
+
* `v: 2` - AES-256-GCM ciphertext plus a small plaintext header.
|
|
205
|
+
*
|
|
206
|
+
* The header carries only non-sensitive state used by Lua scripts
|
|
207
|
+
* (lifecycle status, concurrency version, timestamps, rotation nonce hash).
|
|
208
|
+
* The authenticated ciphertext is authoritative for validation: a header
|
|
209
|
+
* that disagrees with the decrypted record fails closed.
|
|
210
|
+
*/
|
|
211
|
+
export type EncryptedSessionEnvelope = {
|
|
212
|
+
v: 2;
|
|
213
|
+
e: 1;
|
|
214
|
+
/** Encryption key version. */
|
|
215
|
+
k: number;
|
|
216
|
+
/** Base64url initialization vector (12 bytes). */
|
|
217
|
+
i: string;
|
|
218
|
+
/** Base64url GCM auth tag (16 bytes). */
|
|
219
|
+
t: string;
|
|
220
|
+
/** Base64url ciphertext. */
|
|
221
|
+
c: string;
|
|
222
|
+
/** Plaintext lifecycle status mirror (script access). */
|
|
223
|
+
st: SessionStatus;
|
|
224
|
+
/** Plaintext concurrency version mirror (script access). */
|
|
225
|
+
ver: number;
|
|
226
|
+
/** Plaintext last activity mirror (script access). */
|
|
227
|
+
la: number;
|
|
228
|
+
/** Plaintext idle boundary mirror (script access). */
|
|
229
|
+
idle: number | null;
|
|
230
|
+
/** Plaintext absolute boundary mirror (script access). */
|
|
231
|
+
exp: number;
|
|
232
|
+
/** Plaintext rotation nonce hash mirror (script access). */
|
|
233
|
+
rn: string | null;
|
|
234
|
+
/** Plaintext rotated-to JTI mirror (script access). */
|
|
235
|
+
rj: string | null;
|
|
236
|
+
};
|
|
237
|
+
export type SessionEnvelope = PlainSessionEnvelope | EncryptedSessionEnvelope;
|
|
238
|
+
export type RevocationRecord = {
|
|
239
|
+
jti: string;
|
|
240
|
+
/** Unix seconds after which this revocation entry may be garbage collected. */
|
|
241
|
+
expiresAt: number;
|
|
242
|
+
reason?: "logout" | "logout-all" | "password-change" | "admin-revocation" | "reuse-detected" | (string & {});
|
|
243
|
+
};
|
|
244
|
+
/**
|
|
245
|
+
* Storage-agnostic type for tracking revoked token ids (jti).
|
|
246
|
+
* Implementations MUST auto-expire entries at/after `expiresAt` so the
|
|
247
|
+
* store doesn't grow unbounded (e.g. Redis TTL, or a sweep in-memory).
|
|
248
|
+
*/
|
|
249
|
+
export type RevocationStore = {
|
|
250
|
+
revoke(record: RevocationRecord): Promise<void>;
|
|
251
|
+
revokeMany(records: RevocationRecord[]): Promise<void>;
|
|
252
|
+
isRevoked(jti: string): Promise<boolean>;
|
|
253
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/* -------------------------------------------------------------------------- */
|
|
2
|
+
/* Session domain model. */
|
|
3
|
+
/* */
|
|
4
|
+
/* A session here represents a server-side authentication session (typically */
|
|
5
|
+
/* a refresh token or a long-lived browser session). It is NOT a JWT. */
|
|
6
|
+
/* */
|
|
7
|
+
/* Token model: */
|
|
8
|
+
/* raw token - 32 random bytes, base64url encoded. NEVER persisted, */
|
|
9
|
+
/* logged, or included in Redis keys/values. */
|
|
10
|
+
/* jti - SHA-256(token) in base64url. Used as the Redis key */
|
|
11
|
+
/* component and stored in records. A jti can only be */
|
|
12
|
+
/* reversed to the token by brute force, so persisting it */
|
|
13
|
+
/* is safe. */
|
|
14
|
+
/* key - the Redis key: {ns}:session:{userId}:session:{jti} */
|
|
15
|
+
/* -------------------------------------------------------------------------- */
|
|
16
|
+
export {};
|