@zintrust/core 0.1.46 → 0.1.49
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/README.md +1 -1
- package/app/Controllers/AuthController.d.ts.map +1 -1
- package/app/Controllers/AuthController.js +26 -4
- package/app/Middleware/index.js +5 -5
- package/app/Types/controller.d.ts +2 -0
- package/app/Types/controller.d.ts.map +1 -1
- package/app/Types/controller.js +1 -1
- package/config/storage.d.ts.map +1 -1
- package/config/storage.js +1 -1
- package/package.json +1 -1
- package/routes/api.js +13 -6
- package/src/cli/CLI.d.ts.map +1 -1
- package/src/cli/CLI.js +2 -0
- package/src/cli/commands/AddCommand.js +2 -2
- package/src/cli/commands/BulletproofKeyGenerateCommand.d.ts +10 -0
- package/src/cli/commands/BulletproofKeyGenerateCommand.d.ts.map +1 -0
- package/src/cli/commands/BulletproofKeyGenerateCommand.js +139 -0
- package/src/cli/commands/JwtDevCommand.d.ts.map +1 -1
- package/src/cli/commands/JwtDevCommand.js +51 -32
- package/src/cli/scaffolding/ControllerGenerator.d.ts +1 -1
- package/src/cli/scaffolding/ControllerGenerator.d.ts.map +1 -1
- package/src/cli/scaffolding/ControllerGenerator.js +8 -79
- package/src/config/SecretsManager.d.ts +0 -1
- package/src/config/SecretsManager.d.ts.map +1 -1
- package/src/config/SecretsManager.js +0 -1
- package/src/config/middleware.d.ts +1 -0
- package/src/config/middleware.d.ts.map +1 -1
- package/src/config/middleware.js +3 -0
- package/src/http/error-pages/ErrorPageRenderer.js +7 -1
- package/src/index.d.ts +1 -0
- package/src/index.d.ts.map +1 -1
- package/src/index.js +4 -3
- package/src/middleware/BulletproofAuthMiddleware.d.ts +92 -0
- package/src/middleware/BulletproofAuthMiddleware.d.ts.map +1 -0
- package/src/middleware/BulletproofAuthMiddleware.js +421 -0
- package/src/middleware/CsrfMiddleware.d.ts +0 -1
- package/src/middleware/CsrfMiddleware.d.ts.map +1 -1
- package/src/middleware/CsrfMiddleware.js +8 -1
- package/src/middleware/JwtAuthMiddleware.d.ts.map +1 -1
- package/src/middleware/JwtAuthMiddleware.js +11 -5
- package/src/orm/Database.d.ts.map +1 -1
- package/src/orm/Database.js +48 -39
- package/src/orm/QueryBuilder.d.ts.map +1 -1
- package/src/orm/QueryBuilder.js +0 -2
- package/src/orm/adapters/MySQLProxyAdapter.d.ts.map +1 -1
- package/src/orm/adapters/MySQLProxyAdapter.js +54 -35
- package/src/orm/adapters/PostgreSQLProxyAdapter.d.ts.map +1 -1
- package/src/orm/adapters/PostgreSQLProxyAdapter.js +126 -103
- package/src/orm/adapters/SqlProxyHttpAdapterShared.d.ts +30 -0
- package/src/orm/adapters/SqlProxyHttpAdapterShared.d.ts.map +1 -0
- package/src/orm/adapters/SqlProxyHttpAdapterShared.js +64 -0
- package/src/orm/adapters/SqlServerProxyAdapter.d.ts.map +1 -1
- package/src/orm/adapters/SqlServerProxyAdapter.js +54 -37
- package/src/orm/migrations/MigrationStore.d.ts.map +1 -1
- package/src/orm/migrations/MigrationStore.js +22 -1
- package/src/routes/doc.js +1 -1
- package/src/routes/errorPages.d.ts.map +1 -1
- package/src/routes/errorPages.js +9 -2
- package/src/security/CsrfTokenManager.d.ts.map +1 -1
- package/src/security/CsrfTokenManager.js +57 -23
- package/src/security/JwtManager.d.ts +4 -1
- package/src/security/JwtManager.d.ts.map +1 -1
- package/src/security/JwtManager.js +24 -10
- package/src/security/JwtSessions.d.ts +12 -0
- package/src/security/JwtSessions.d.ts.map +1 -0
- package/src/security/JwtSessions.js +556 -0
- package/src/security/NonceReplay.d.ts +24 -0
- package/src/security/NonceReplay.d.ts.map +1 -0
- package/src/security/NonceReplay.js +42 -0
- package/src/security/TokenRevocation.d.ts.map +1 -1
- package/src/security/TokenRevocation.js +1 -0
- package/src/tools/http/Http.d.ts +5 -0
- package/src/tools/http/Http.d.ts.map +1 -1
- package/src/tools/http/Http.js +25 -9
- package/src/tools/queue/QueueReliabilityOrchestrator.d.ts.map +1 -1
- package/src/tools/queue/QueueReliabilityOrchestrator.js +18 -6
- package/src/validation/Validator.d.ts.map +1 -1
- package/src/validation/Validator.js +4 -2
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
import { RemoteSignedJson } from '../common/RemoteSignedJson.js';
|
|
2
|
+
import { Cloudflare } from '../config/cloudflare.js';
|
|
3
|
+
import { Env } from '../config/env.js';
|
|
4
|
+
import { Logger } from '../config/logger.js';
|
|
5
|
+
import { securityConfig } from '../config/security.js';
|
|
6
|
+
import { createRedisConnection } from '../config/workers.js';
|
|
7
|
+
import { ErrorFactory } from '../exceptions/ZintrustError.js';
|
|
8
|
+
import { useDatabase } from '../orm/Database.js';
|
|
9
|
+
import { JwtManager } from './JwtManager.js';
|
|
10
|
+
const DEFAULTS = {
|
|
11
|
+
driver: 'database',
|
|
12
|
+
dbConnection: 'default',
|
|
13
|
+
dbTable: 'zintrust_jwt_revocations',
|
|
14
|
+
redisPrefix: 'zt:jwt:active:',
|
|
15
|
+
kvBinding: 'CACHE',
|
|
16
|
+
kvPrefix: 'zt:jwt:active:',
|
|
17
|
+
kvRemoteNamespace: '',
|
|
18
|
+
subIndexSuffix: ':sub:',
|
|
19
|
+
};
|
|
20
|
+
const defaultTtlMs = Math.max(securityConfig.jwt.expiresIn * 1000, 60_000);
|
|
21
|
+
const normalizeDriverName = (raw) => {
|
|
22
|
+
const value = typeof raw === 'string' ? raw.trim().toLowerCase() : '';
|
|
23
|
+
if (value === 'db' || value === 'database')
|
|
24
|
+
return 'database';
|
|
25
|
+
if (value === 'redis')
|
|
26
|
+
return 'redis';
|
|
27
|
+
if (value === 'kv')
|
|
28
|
+
return 'kv';
|
|
29
|
+
if (value === 'kv-remote' || value === 'kvremote')
|
|
30
|
+
return 'kv-remote';
|
|
31
|
+
if (value === 'memory' || value === 'mem')
|
|
32
|
+
return 'memory';
|
|
33
|
+
return DEFAULTS.driver;
|
|
34
|
+
};
|
|
35
|
+
const getHeaderValue = (value) => {
|
|
36
|
+
if (Array.isArray(value))
|
|
37
|
+
return typeof value[0] === 'string' ? value[0] : '';
|
|
38
|
+
return typeof value === 'string' ? value : '';
|
|
39
|
+
};
|
|
40
|
+
const getBearerToken = (authorizationHeader) => {
|
|
41
|
+
const header = getHeaderValue(authorizationHeader).trim();
|
|
42
|
+
if (header === '')
|
|
43
|
+
return null;
|
|
44
|
+
const [scheme, ...rest] = header.split(/\s+/);
|
|
45
|
+
if (typeof scheme !== 'string' || scheme.toLowerCase() !== 'bearer')
|
|
46
|
+
return null;
|
|
47
|
+
const token = rest.join(' ').trim();
|
|
48
|
+
if (token === '')
|
|
49
|
+
return null;
|
|
50
|
+
return token;
|
|
51
|
+
};
|
|
52
|
+
const resolveKey = (token) => {
|
|
53
|
+
let decoded = {};
|
|
54
|
+
try {
|
|
55
|
+
decoded = JwtManager.create().decode(token);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
decoded = {};
|
|
59
|
+
}
|
|
60
|
+
const expSeconds = typeof decoded['exp'] === 'number' ? decoded['exp'] : undefined;
|
|
61
|
+
const expiresAtMs = expSeconds !== undefined && Number.isFinite(expSeconds) && expSeconds > 0
|
|
62
|
+
? Math.floor(expSeconds * 1000)
|
|
63
|
+
: Date.now() + defaultTtlMs;
|
|
64
|
+
const jti = typeof decoded['jti'] === 'string' ? decoded['jti'].trim() : '';
|
|
65
|
+
const sub = typeof decoded['sub'] === 'string' ? decoded['sub'].trim() : '';
|
|
66
|
+
return {
|
|
67
|
+
id: jti === '' ? token : jti,
|
|
68
|
+
expiresAtMs,
|
|
69
|
+
sub: sub === '' ? undefined : sub,
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
const createMemoryStore = () => {
|
|
73
|
+
const active = new Map();
|
|
74
|
+
const subIndex = new Map();
|
|
75
|
+
const idToSub = new Map();
|
|
76
|
+
const indexDelete = (id) => {
|
|
77
|
+
const sub = idToSub.get(id);
|
|
78
|
+
if (sub === undefined)
|
|
79
|
+
return;
|
|
80
|
+
const set = subIndex.get(sub);
|
|
81
|
+
if (set !== undefined) {
|
|
82
|
+
set.delete(id);
|
|
83
|
+
if (set.size === 0) {
|
|
84
|
+
subIndex.delete(sub);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
idToSub.delete(id);
|
|
88
|
+
};
|
|
89
|
+
const indexAdd = (sub, id) => {
|
|
90
|
+
if (typeof sub !== 'string' || sub.trim() === '') {
|
|
91
|
+
indexDelete(id);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const key = sub.trim();
|
|
95
|
+
// If this id was previously indexed under another subject, remove it.
|
|
96
|
+
const previous = idToSub.get(id);
|
|
97
|
+
if (previous !== undefined && previous !== key) {
|
|
98
|
+
indexDelete(id);
|
|
99
|
+
}
|
|
100
|
+
const existing = subIndex.get(key) ?? new Set();
|
|
101
|
+
existing.add(id);
|
|
102
|
+
subIndex.set(key, existing);
|
|
103
|
+
idToSub.set(id, key);
|
|
104
|
+
};
|
|
105
|
+
const cleanupExpired = () => {
|
|
106
|
+
const now = Date.now();
|
|
107
|
+
for (const [id, expiresAtMs] of active.entries()) {
|
|
108
|
+
if (expiresAtMs <= now) {
|
|
109
|
+
active.delete(id);
|
|
110
|
+
indexDelete(id);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
return {
|
|
115
|
+
async upsertActive(key) {
|
|
116
|
+
cleanupExpired();
|
|
117
|
+
active.set(key.id, key.expiresAtMs);
|
|
118
|
+
indexAdd(key.sub, key.id);
|
|
119
|
+
await Promise.resolve();
|
|
120
|
+
},
|
|
121
|
+
async isActive(id) {
|
|
122
|
+
cleanupExpired();
|
|
123
|
+
const expiresAtMs = active.get(id);
|
|
124
|
+
if (expiresAtMs === undefined)
|
|
125
|
+
return false;
|
|
126
|
+
if (expiresAtMs <= Date.now()) {
|
|
127
|
+
active.delete(id);
|
|
128
|
+
indexDelete(id);
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
await Promise.resolve();
|
|
132
|
+
return true;
|
|
133
|
+
},
|
|
134
|
+
async deleteById(id) {
|
|
135
|
+
active.delete(id);
|
|
136
|
+
indexDelete(id);
|
|
137
|
+
await Promise.resolve();
|
|
138
|
+
},
|
|
139
|
+
async deleteAllForSub(sub) {
|
|
140
|
+
const key = sub.trim();
|
|
141
|
+
const ids = subIndex.get(key);
|
|
142
|
+
if (!ids)
|
|
143
|
+
return;
|
|
144
|
+
for (const id of ids.values()) {
|
|
145
|
+
active.delete(id);
|
|
146
|
+
idToSub.delete(id);
|
|
147
|
+
}
|
|
148
|
+
subIndex.delete(key);
|
|
149
|
+
await Promise.resolve();
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
const createDatabaseStore = (params) => {
|
|
154
|
+
let checkCount = 0;
|
|
155
|
+
const maybeCleanup = async () => {
|
|
156
|
+
checkCount += 1;
|
|
157
|
+
if (checkCount % 250 !== 0)
|
|
158
|
+
return;
|
|
159
|
+
try {
|
|
160
|
+
const db = useDatabase(undefined, params.connection);
|
|
161
|
+
await db.table(params.table).where('expires_at_ms', '<=', Date.now()).delete();
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
Logger.debug('JwtSessions database cleanup failed', {
|
|
165
|
+
error: error instanceof Error ? error.message : String(error),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
return {
|
|
170
|
+
async upsertActive(key) {
|
|
171
|
+
const db = useDatabase(undefined, params.connection);
|
|
172
|
+
// Require the new schema (kind column). Old rows should be kind=revoked.
|
|
173
|
+
const record = {
|
|
174
|
+
jti: key.id,
|
|
175
|
+
sub: key.sub ?? null,
|
|
176
|
+
user_id: key.sub ?? null,
|
|
177
|
+
expires_at_ms: key.expiresAtMs,
|
|
178
|
+
kind: 'active',
|
|
179
|
+
};
|
|
180
|
+
try {
|
|
181
|
+
await db.table(params.table).where('jti', '=', key.id).update(record);
|
|
182
|
+
const existing = await db.table(params.table).where('jti', '=', key.id).first();
|
|
183
|
+
if (existing === null) {
|
|
184
|
+
await db.table(params.table).insert(record);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
throw ErrorFactory.createConfigError(`JWT sessions database table '${params.table}' is missing required columns (run migrations)`, {
|
|
189
|
+
table: params.table,
|
|
190
|
+
error: error instanceof Error ? error.message : String(error),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
async isActive(id) {
|
|
195
|
+
await maybeCleanup();
|
|
196
|
+
const db = useDatabase(undefined, params.connection);
|
|
197
|
+
try {
|
|
198
|
+
const row = await db
|
|
199
|
+
.table(params.table)
|
|
200
|
+
.where('jti', '=', id)
|
|
201
|
+
.where('kind', '=', 'active')
|
|
202
|
+
.first();
|
|
203
|
+
if (row === null)
|
|
204
|
+
return false;
|
|
205
|
+
const expiresAtMs = Number(row['expires_at_ms']);
|
|
206
|
+
if (!Number.isFinite(expiresAtMs))
|
|
207
|
+
return true;
|
|
208
|
+
if (expiresAtMs <= Date.now()) {
|
|
209
|
+
await db.table(params.table).where('jti', '=', id).delete();
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
throw ErrorFactory.createConfigError(`JWT sessions database table '${params.table}' is missing required columns (run migrations)`, {
|
|
216
|
+
table: params.table,
|
|
217
|
+
error: error instanceof Error ? error.message : String(error),
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
async deleteById(id) {
|
|
222
|
+
const db = useDatabase(undefined, params.connection);
|
|
223
|
+
await db.table(params.table).where('jti', '=', id).delete();
|
|
224
|
+
},
|
|
225
|
+
async deleteAllForSub(sub) {
|
|
226
|
+
const db = useDatabase(undefined, params.connection);
|
|
227
|
+
await db.table(params.table).where('sub', '=', sub).where('kind', '=', 'active').delete();
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
};
|
|
231
|
+
const encodeSubIndexKey = (prefix, sub) => {
|
|
232
|
+
const trimmed = sub.trim();
|
|
233
|
+
return `${prefix}${DEFAULTS.subIndexSuffix}${encodeURIComponent(trimmed)}`;
|
|
234
|
+
};
|
|
235
|
+
const parseSubIndexValue = (raw) => {
|
|
236
|
+
if (!Array.isArray(raw))
|
|
237
|
+
return [];
|
|
238
|
+
return raw
|
|
239
|
+
.filter((v) => typeof v === 'string')
|
|
240
|
+
.map((s) => s.trim())
|
|
241
|
+
.filter((s) => s !== '');
|
|
242
|
+
};
|
|
243
|
+
const createRedisStore = (params) => {
|
|
244
|
+
const client = createRedisConnection({
|
|
245
|
+
host: Env.REDIS_HOST,
|
|
246
|
+
port: Env.REDIS_PORT,
|
|
247
|
+
password: Env.REDIS_PASSWORD,
|
|
248
|
+
db: Env.getInt('JWT_REVOCATION_REDIS_DB', Env.REDIS_DB),
|
|
249
|
+
});
|
|
250
|
+
const indexGet = async (sub) => {
|
|
251
|
+
const value = await client.get(encodeSubIndexKey(params.keyPrefix, sub));
|
|
252
|
+
if (value === null)
|
|
253
|
+
return [];
|
|
254
|
+
try {
|
|
255
|
+
return parseSubIndexValue(JSON.parse(value));
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return [];
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
const indexSet = async (sub, ids, ttlMs) => {
|
|
262
|
+
const ttl = Math.max(0, ttlMs);
|
|
263
|
+
if (ttl === 0)
|
|
264
|
+
return;
|
|
265
|
+
await client.set(encodeSubIndexKey(params.keyPrefix, sub), JSON.stringify(ids), 'PX', ttl);
|
|
266
|
+
};
|
|
267
|
+
return {
|
|
268
|
+
async upsertActive(key) {
|
|
269
|
+
const ttlMs = Math.max(0, key.expiresAtMs - Date.now());
|
|
270
|
+
if (ttlMs === 0)
|
|
271
|
+
return;
|
|
272
|
+
await client.set(`${params.keyPrefix}${key.id}`, '1', 'PX', ttlMs);
|
|
273
|
+
if (typeof key.sub === 'string' && key.sub.trim() !== '') {
|
|
274
|
+
const existing = await indexGet(key.sub);
|
|
275
|
+
const next = Array.from(new Set([...existing, key.id]));
|
|
276
|
+
await indexSet(key.sub, next, ttlMs);
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
async isActive(id) {
|
|
280
|
+
const value = await client.get(`${params.keyPrefix}${id}`);
|
|
281
|
+
return value !== null;
|
|
282
|
+
},
|
|
283
|
+
async deleteById(id) {
|
|
284
|
+
await client.del(`${params.keyPrefix}${id}`);
|
|
285
|
+
},
|
|
286
|
+
async deleteAllForSub(sub) {
|
|
287
|
+
const ids = await indexGet(sub);
|
|
288
|
+
if (ids.length > 0) {
|
|
289
|
+
await client.del(...ids.map((id) => `${params.keyPrefix}${id}`));
|
|
290
|
+
}
|
|
291
|
+
await client.del(encodeSubIndexKey(params.keyPrefix, sub));
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
};
|
|
295
|
+
const createKvStore = (params) => {
|
|
296
|
+
const getKvOrThrow = () => {
|
|
297
|
+
const kv = Cloudflare.getKVBinding(params.bindingName);
|
|
298
|
+
if (kv === null) {
|
|
299
|
+
throw ErrorFactory.createConfigError(`KV binding '${params.bindingName}' not found`, {
|
|
300
|
+
bindingName: params.bindingName,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
return kv;
|
|
304
|
+
};
|
|
305
|
+
const indexGet = async (sub) => {
|
|
306
|
+
const kv = getKvOrThrow();
|
|
307
|
+
const rawValue = await kv.get(encodeSubIndexKey(params.keyPrefix, sub));
|
|
308
|
+
if (typeof rawValue !== 'string')
|
|
309
|
+
return [];
|
|
310
|
+
const trimmed = rawValue.trim();
|
|
311
|
+
if (trimmed === '')
|
|
312
|
+
return [];
|
|
313
|
+
try {
|
|
314
|
+
return parseSubIndexValue(JSON.parse(trimmed));
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
return [];
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
const indexSet = async (sub, ids, ttlMs) => {
|
|
321
|
+
const ttlSeconds = Math.max(60, Math.ceil(ttlMs / 1000));
|
|
322
|
+
const kv = getKvOrThrow();
|
|
323
|
+
await kv.put(encodeSubIndexKey(params.keyPrefix, sub), JSON.stringify(ids), {
|
|
324
|
+
expirationTtl: ttlSeconds,
|
|
325
|
+
});
|
|
326
|
+
};
|
|
327
|
+
return {
|
|
328
|
+
async upsertActive(key) {
|
|
329
|
+
const ttlMs = Math.max(0, key.expiresAtMs - Date.now());
|
|
330
|
+
if (ttlMs === 0)
|
|
331
|
+
return;
|
|
332
|
+
const ttlSeconds = Math.max(60, Math.ceil(ttlMs / 1000));
|
|
333
|
+
const kv = getKvOrThrow();
|
|
334
|
+
await kv.put(`${params.keyPrefix}${key.id}`, '1', { expirationTtl: ttlSeconds });
|
|
335
|
+
if (typeof key.sub === 'string' && key.sub.trim() !== '') {
|
|
336
|
+
const existing = await indexGet(key.sub);
|
|
337
|
+
const next = Array.from(new Set([...existing, key.id]));
|
|
338
|
+
await indexSet(key.sub, next, ttlMs);
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
async isActive(id) {
|
|
342
|
+
const kv = getKvOrThrow();
|
|
343
|
+
const value = await kv.get(`${params.keyPrefix}${id}`);
|
|
344
|
+
return value !== null;
|
|
345
|
+
},
|
|
346
|
+
async deleteById(id) {
|
|
347
|
+
const kv = getKvOrThrow();
|
|
348
|
+
await kv.delete(`${params.keyPrefix}${id}`);
|
|
349
|
+
},
|
|
350
|
+
async deleteAllForSub(sub) {
|
|
351
|
+
const kv = getKvOrThrow();
|
|
352
|
+
const ids = await indexGet(sub);
|
|
353
|
+
for (const id of ids) {
|
|
354
|
+
// eslint-disable-next-line no-await-in-loop
|
|
355
|
+
await kv.delete(`${params.keyPrefix}${id}`);
|
|
356
|
+
}
|
|
357
|
+
await kv.delete(encodeSubIndexKey(params.keyPrefix, sub));
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
};
|
|
361
|
+
const kvRemoteGetProxySettings = () => ({
|
|
362
|
+
baseUrl: Env.get('KV_REMOTE_URL'),
|
|
363
|
+
keyId: Env.get('KV_REMOTE_KEY_ID'),
|
|
364
|
+
secret: Env.get('KV_REMOTE_SECRET', Env.APP_KEY),
|
|
365
|
+
timeoutMs: Env.getInt('ZT_PROXY_TIMEOUT_MS', Env.REQUEST_TIMEOUT),
|
|
366
|
+
});
|
|
367
|
+
const kvRemoteNormalizeNamespace = (value) => {
|
|
368
|
+
const trimmed = value.trim();
|
|
369
|
+
return trimmed === '' ? undefined : trimmed;
|
|
370
|
+
};
|
|
371
|
+
const kvRemoteCreateRemoteSettings = (proxy) => ({
|
|
372
|
+
baseUrl: proxy.baseUrl,
|
|
373
|
+
keyId: proxy.keyId,
|
|
374
|
+
secret: proxy.secret,
|
|
375
|
+
timeoutMs: proxy.timeoutMs,
|
|
376
|
+
signaturePathPrefixToStrip: undefined,
|
|
377
|
+
missingUrlMessage: 'KV remote proxy URL is missing (KV_REMOTE_URL)',
|
|
378
|
+
missingCredentialsMessage: 'KV remote signing credentials are missing (KV_REMOTE_KEY_ID / KV_REMOTE_SECRET)',
|
|
379
|
+
messages: {
|
|
380
|
+
unauthorized: 'KV remote proxy unauthorized',
|
|
381
|
+
forbidden: 'KV remote proxy forbidden',
|
|
382
|
+
rateLimited: 'KV remote proxy rate limited',
|
|
383
|
+
rejected: 'KV remote proxy rejected request',
|
|
384
|
+
error: 'KV remote proxy error',
|
|
385
|
+
timedOut: 'KV remote proxy request timed out',
|
|
386
|
+
},
|
|
387
|
+
});
|
|
388
|
+
const createKvRemoteOps = (ctx) => {
|
|
389
|
+
const getRemoteOrThrow = () => {
|
|
390
|
+
const proxy = ctx.getProxySettings();
|
|
391
|
+
if (proxy.baseUrl.trim() === '') {
|
|
392
|
+
throw ErrorFactory.createConfigError('KV remote proxy URL is missing (KV_REMOTE_URL)');
|
|
393
|
+
}
|
|
394
|
+
if (proxy.keyId.trim() === '' || proxy.secret.trim() === '') {
|
|
395
|
+
throw ErrorFactory.createConfigError('KV remote signing credentials are missing (KV_REMOTE_KEY_ID / KV_REMOTE_SECRET)');
|
|
396
|
+
}
|
|
397
|
+
return ctx.createRemoteSettings(proxy);
|
|
398
|
+
};
|
|
399
|
+
const remoteGetJson = async (key) => {
|
|
400
|
+
const remote = getRemoteOrThrow();
|
|
401
|
+
const out = await RemoteSignedJson.request(remote, '/zin/kv/get', {
|
|
402
|
+
namespace: ctx.normalizeNamespace(ctx.namespace),
|
|
403
|
+
key,
|
|
404
|
+
type: 'text',
|
|
405
|
+
});
|
|
406
|
+
return out.value;
|
|
407
|
+
};
|
|
408
|
+
const remotePutJson = async (key, value, ttlSeconds) => {
|
|
409
|
+
const remote = getRemoteOrThrow();
|
|
410
|
+
await RemoteSignedJson.request(remote, '/zin/kv/put', {
|
|
411
|
+
namespace: ctx.normalizeNamespace(ctx.namespace),
|
|
412
|
+
key,
|
|
413
|
+
value,
|
|
414
|
+
ttlSeconds,
|
|
415
|
+
});
|
|
416
|
+
};
|
|
417
|
+
const remoteDelete = async (key) => {
|
|
418
|
+
const remote = getRemoteOrThrow();
|
|
419
|
+
await RemoteSignedJson.request(remote, '/zin/kv/delete', {
|
|
420
|
+
namespace: ctx.normalizeNamespace(ctx.namespace),
|
|
421
|
+
key,
|
|
422
|
+
});
|
|
423
|
+
};
|
|
424
|
+
const indexGet = async (sub) => {
|
|
425
|
+
const raw = await remoteGetJson(encodeSubIndexKey(ctx.keyPrefix, sub));
|
|
426
|
+
if (typeof raw !== 'string')
|
|
427
|
+
return [];
|
|
428
|
+
try {
|
|
429
|
+
return parseSubIndexValue(JSON.parse(raw));
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
return [];
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
const indexSet = async (sub, ids, ttlMs) => {
|
|
436
|
+
const ttlSeconds = Math.max(60, Math.ceil(Math.max(0, ttlMs) / 1000));
|
|
437
|
+
await remotePutJson(encodeSubIndexKey(ctx.keyPrefix, sub), JSON.stringify(ids), ttlSeconds);
|
|
438
|
+
};
|
|
439
|
+
return { remoteGetJson, remotePutJson, remoteDelete, indexGet, indexSet };
|
|
440
|
+
};
|
|
441
|
+
const createKvRemoteStore = (params) => {
|
|
442
|
+
const ctx = {
|
|
443
|
+
keyPrefix: params.keyPrefix,
|
|
444
|
+
namespace: params.namespace,
|
|
445
|
+
getProxySettings: kvRemoteGetProxySettings,
|
|
446
|
+
createRemoteSettings: kvRemoteCreateRemoteSettings,
|
|
447
|
+
normalizeNamespace: kvRemoteNormalizeNamespace,
|
|
448
|
+
};
|
|
449
|
+
const ops = createKvRemoteOps(ctx);
|
|
450
|
+
return {
|
|
451
|
+
async upsertActive(key) {
|
|
452
|
+
const ttlMs = Math.max(0, key.expiresAtMs - Date.now());
|
|
453
|
+
if (ttlMs === 0)
|
|
454
|
+
return;
|
|
455
|
+
const ttlSeconds = Math.max(60, Math.ceil(ttlMs / 1000));
|
|
456
|
+
await ops.remotePutJson(`${ctx.keyPrefix}${key.id}`, '1', ttlSeconds);
|
|
457
|
+
if (typeof key.sub === 'string' && key.sub.trim() !== '') {
|
|
458
|
+
const existing = await ops.indexGet(key.sub);
|
|
459
|
+
const next = Array.from(new Set([...existing, key.id]));
|
|
460
|
+
await ops.indexSet(key.sub, next, ttlMs);
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
async isActive(id) {
|
|
464
|
+
const raw = await ops.remoteGetJson(`${ctx.keyPrefix}${id}`);
|
|
465
|
+
if (raw === null || raw === undefined)
|
|
466
|
+
return false;
|
|
467
|
+
if (typeof raw === 'string')
|
|
468
|
+
return raw.trim() !== '';
|
|
469
|
+
return true;
|
|
470
|
+
},
|
|
471
|
+
async deleteById(id) {
|
|
472
|
+
await ops.remoteDelete(`${ctx.keyPrefix}${id}`);
|
|
473
|
+
},
|
|
474
|
+
async deleteAllForSub(sub) {
|
|
475
|
+
const ids = await ops.indexGet(sub);
|
|
476
|
+
for (const id of ids) {
|
|
477
|
+
// eslint-disable-next-line no-await-in-loop
|
|
478
|
+
await ops.remoteDelete(`${ctx.keyPrefix}${id}`);
|
|
479
|
+
}
|
|
480
|
+
await ops.remoteDelete(encodeSubIndexKey(ctx.keyPrefix, sub));
|
|
481
|
+
},
|
|
482
|
+
};
|
|
483
|
+
};
|
|
484
|
+
let cachedStore = null;
|
|
485
|
+
let cachedDriver = null;
|
|
486
|
+
const resolveStore = () => {
|
|
487
|
+
const driver = normalizeDriverName(Env.get('JWT_SESSION_DRIVER', Env.get('JWT_REVOCATION_DRIVER', DEFAULTS.driver)));
|
|
488
|
+
if (cachedStore !== null && cachedDriver === driver) {
|
|
489
|
+
return { driver, store: cachedStore };
|
|
490
|
+
}
|
|
491
|
+
if (driver === 'memory') {
|
|
492
|
+
cachedStore = createMemoryStore();
|
|
493
|
+
cachedDriver = driver;
|
|
494
|
+
return { driver, store: cachedStore };
|
|
495
|
+
}
|
|
496
|
+
if (driver === 'database') {
|
|
497
|
+
const connection = Env.get('JWT_SESSION_DB_CONNECTION', Env.get('JWT_REVOCATION_DB_CONNECTION', DEFAULTS.dbConnection));
|
|
498
|
+
const table = Env.get('JWT_SESSION_DB_TABLE', Env.get('JWT_REVOCATION_DB_TABLE', DEFAULTS.dbTable));
|
|
499
|
+
cachedStore = createDatabaseStore({ connection, table });
|
|
500
|
+
cachedDriver = driver;
|
|
501
|
+
return { driver, store: cachedStore };
|
|
502
|
+
}
|
|
503
|
+
if (driver === 'redis') {
|
|
504
|
+
const keyPrefix = Env.get('JWT_SESSION_REDIS_PREFIX', Env.get('JWT_REVOCATION_REDIS_PREFIX', DEFAULTS.redisPrefix));
|
|
505
|
+
cachedStore = createRedisStore({ keyPrefix });
|
|
506
|
+
cachedDriver = driver;
|
|
507
|
+
return { driver, store: cachedStore };
|
|
508
|
+
}
|
|
509
|
+
if (driver === 'kv') {
|
|
510
|
+
const bindingName = Env.get('JWT_SESSION_KV_BINDING', Env.get('JWT_REVOCATION_KV_BINDING', DEFAULTS.kvBinding));
|
|
511
|
+
const keyPrefix = Env.get('JWT_SESSION_KV_PREFIX', DEFAULTS.kvPrefix);
|
|
512
|
+
cachedStore = createKvStore({ bindingName, keyPrefix });
|
|
513
|
+
cachedDriver = driver;
|
|
514
|
+
return { driver, store: cachedStore };
|
|
515
|
+
}
|
|
516
|
+
const namespace = Env.get('JWT_SESSION_KV_REMOTE_NAMESPACE', DEFAULTS.kvRemoteNamespace);
|
|
517
|
+
const keyPrefix = Env.get('JWT_SESSION_KV_REMOTE_PREFIX', DEFAULTS.kvPrefix);
|
|
518
|
+
cachedStore = createKvRemoteStore({ keyPrefix, namespace });
|
|
519
|
+
cachedDriver = driver;
|
|
520
|
+
return { driver, store: cachedStore };
|
|
521
|
+
};
|
|
522
|
+
export const JwtSessions = Object.freeze({
|
|
523
|
+
async register(token) {
|
|
524
|
+
const { store } = resolveStore();
|
|
525
|
+
await store.upsertActive(resolveKey(token));
|
|
526
|
+
},
|
|
527
|
+
async isActive(token) {
|
|
528
|
+
const { store } = resolveStore();
|
|
529
|
+
const key = resolveKey(token);
|
|
530
|
+
return store.isActive(key.id);
|
|
531
|
+
},
|
|
532
|
+
async logout(header) {
|
|
533
|
+
const token = getBearerToken(header);
|
|
534
|
+
if (token === null)
|
|
535
|
+
return null;
|
|
536
|
+
const { store } = resolveStore();
|
|
537
|
+
const key = resolveKey(token);
|
|
538
|
+
await store.deleteById(key.id);
|
|
539
|
+
return token;
|
|
540
|
+
},
|
|
541
|
+
async logoutAll(sub) {
|
|
542
|
+
const normalized = typeof sub === 'string' ? sub.trim() : '';
|
|
543
|
+
if (normalized === '')
|
|
544
|
+
return;
|
|
545
|
+
const { store } = resolveStore();
|
|
546
|
+
await store.deleteAllForSub(normalized);
|
|
547
|
+
},
|
|
548
|
+
getDriver() {
|
|
549
|
+
return resolveStore().driver;
|
|
550
|
+
},
|
|
551
|
+
_resetForTests() {
|
|
552
|
+
cachedStore = null;
|
|
553
|
+
cachedDriver = null;
|
|
554
|
+
},
|
|
555
|
+
});
|
|
556
|
+
export default JwtSessions;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type NonceReplayVerifier = (keyId: string, nonce: string, ttlMs: number) => Promise<boolean>;
|
|
2
|
+
export type MemoryNonceReplayOptions = {
|
|
3
|
+
/**
|
|
4
|
+
* Cleanup interval to prevent unbounded map growth.
|
|
5
|
+
* Default: 500 accepted nonces.
|
|
6
|
+
*/
|
|
7
|
+
cleanupEvery?: number;
|
|
8
|
+
/**
|
|
9
|
+
* Maximum nonce entries to keep before forcing a cleanup sweep.
|
|
10
|
+
* Default: 25_000.
|
|
11
|
+
*/
|
|
12
|
+
maxEntries?: number;
|
|
13
|
+
};
|
|
14
|
+
export declare const NonceReplay: Readonly<{
|
|
15
|
+
/**
|
|
16
|
+
* In-memory, best-effort replay protection.
|
|
17
|
+
*
|
|
18
|
+
* Works in both Node and Workers, but does not provide cross-instance guarantees.
|
|
19
|
+
* For strict production replay protection, supply your own verifier backed by Redis/KV.
|
|
20
|
+
*/
|
|
21
|
+
createMemoryVerifier(options?: MemoryNonceReplayOptions): NonceReplayVerifier;
|
|
22
|
+
}>;
|
|
23
|
+
export default NonceReplay;
|
|
24
|
+
//# sourceMappingURL=NonceReplay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NonceReplay.d.ts","sourceRoot":"","sources":["../../../src/security/NonceReplay.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,mBAAmB,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAEpG,MAAM,MAAM,wBAAwB,GAAG;IACrC;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAWF,eAAO,MAAM,WAAW;IACtB;;;;;OAKG;mCAC2B,wBAAwB,GAAQ,mBAAmB;EAmCjF,CAAC;AAEH,eAAe,WAAW,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const DEFAULTS = {
|
|
2
|
+
cleanupEvery: 500,
|
|
3
|
+
maxEntries: 25_000,
|
|
4
|
+
};
|
|
5
|
+
export const NonceReplay = Object.freeze({
|
|
6
|
+
/**
|
|
7
|
+
* In-memory, best-effort replay protection.
|
|
8
|
+
*
|
|
9
|
+
* Works in both Node and Workers, but does not provide cross-instance guarantees.
|
|
10
|
+
* For strict production replay protection, supply your own verifier backed by Redis/KV.
|
|
11
|
+
*/
|
|
12
|
+
createMemoryVerifier(options = {}) {
|
|
13
|
+
const cleanupEvery = options.cleanupEvery ?? DEFAULTS.cleanupEvery;
|
|
14
|
+
const maxEntries = options.maxEntries ?? DEFAULTS.maxEntries;
|
|
15
|
+
const store = new Map();
|
|
16
|
+
let accepted = 0;
|
|
17
|
+
const cleanup = (nowMs) => {
|
|
18
|
+
for (const [key, entry] of store.entries()) {
|
|
19
|
+
if (entry.expiresAtMs <= nowMs)
|
|
20
|
+
store.delete(key);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
return async (keyId, nonce, ttlMs) => {
|
|
24
|
+
const nowMs = Date.now();
|
|
25
|
+
const expiresAtMs = nowMs + Math.max(1, ttlMs);
|
|
26
|
+
const compositeKey = `${keyId}:${nonce}`;
|
|
27
|
+
const existing = store.get(compositeKey);
|
|
28
|
+
if (existing !== undefined && existing.expiresAtMs > nowMs) {
|
|
29
|
+
await Promise.resolve();
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
store.set(compositeKey, { expiresAtMs });
|
|
33
|
+
accepted += 1;
|
|
34
|
+
if (store.size > maxEntries || accepted % cleanupEvery === 0) {
|
|
35
|
+
cleanup(nowMs);
|
|
36
|
+
}
|
|
37
|
+
await Promise.resolve();
|
|
38
|
+
return true;
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
export default NonceReplay;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TokenRevocation.d.ts","sourceRoot":"","sources":["../../../src/security/TokenRevocation.ts"],"names":[],"mappings":"AAUA,KAAK,mBAAmB,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC;AAEzD,MAAM,MAAM,yBAAyB,GAAG,UAAU,GAAG,QAAQ,GAAG,OAAO,GAAG,IAAI,GAAG,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"TokenRevocation.d.ts","sourceRoot":"","sources":["../../../src/security/TokenRevocation.ts"],"names":[],"mappings":"AAUA,KAAK,mBAAmB,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC;AAEzD,MAAM,MAAM,yBAAyB,GAAG,UAAU,GAAG,QAAQ,GAAG,OAAO,GAAG,IAAI,GAAG,WAAW,CAAC;AAysB7F,eAAO,MAAM,eAAe;IAC1B;;;;OAIG;mBACkB,mBAAmB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAUjE;;OAEG;qBACoB,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAMhD;;OAEG;iBACU,yBAAyB;IAItC;;OAEG;sBACe,IAAI;EAItB,CAAC;AAEH,eAAe,eAAe,CAAC"}
|
package/src/tools/http/Http.d.ts
CHANGED
|
@@ -19,6 +19,11 @@ export interface IHttpRequest {
|
|
|
19
19
|
asJson(): IHttpRequest;
|
|
20
20
|
asForm(): IHttpRequest;
|
|
21
21
|
send(): Promise<IHttpResponse>;
|
|
22
|
+
sendRaw(): Promise<Response>;
|
|
23
|
+
sendStream(): Promise<{
|
|
24
|
+
response: Response;
|
|
25
|
+
stream: ReadableStream<Uint8Array> | null;
|
|
26
|
+
}>;
|
|
22
27
|
}
|
|
23
28
|
/**
|
|
24
29
|
* HTTP Client - Sealed namespace for making HTTP requests
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Http.d.ts","sourceRoot":"","sources":["../../../../src/tools/http/Http.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,OAAO,EAAsB,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAElF,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAE9D;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,YAAY,CAAC;IACtD,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,YAAY,CAAC;IAC3D,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,YAAY,CAAC;IACnE,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,YAAY,CAAC;IAChE,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,CAAC;IACtC,MAAM,IAAI,YAAY,CAAC;IACvB,MAAM,IAAI,YAAY,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,aAAa,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"Http.d.ts","sourceRoot":"","sources":["../../../../src/tools/http/Http.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,OAAO,EAAsB,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAElF,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAE9D;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,YAAY,CAAC;IACtD,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,YAAY,CAAC;IAC3D,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,YAAY,CAAC;IACnE,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,YAAY,CAAC;IAChE,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,CAAC;IACtC,MAAM,IAAI,YAAY,CAAC;IACvB,MAAM,IAAI,YAAY,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,aAAa,CAAC,CAAC;IAC/B,OAAO,IAAI,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC7B,UAAU,IAAI,OAAO,CAAC;QAAE,QAAQ,EAAE,QAAQ,CAAC;QAAC,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;CAC1F;AA6KD;;GAEG;AACH,eAAO,MAAM,UAAU;IACrB;;OAEG;aACM,MAAM,GAAG,YAAY;IAI9B;;OAEG;cACO,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,YAAY;IAQ/D;;OAEG;aACM,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,YAAY;IAQ9D;;OAEG;eACQ,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,YAAY;IAQhE;;OAEG;gBACS,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,YAAY;EAOjE,CAAC;AAEH,eAAe,UAAU,CAAC"}
|