@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
|
@@ -2,46 +2,40 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* SQL Server Proxy Adapter (HTTP)
|
|
4
4
|
*/
|
|
5
|
-
import { Env } from '../../config/env.js';
|
|
6
5
|
import { ErrorFactory } from '../../exceptions/ZintrustError.js';
|
|
7
6
|
import { AdaptersEnum } from '../../migrations/enum/index.js';
|
|
8
7
|
import { QueryBuilder } from '../QueryBuilder.js';
|
|
9
|
-
import { ensureSignedSettings, isRecord,
|
|
10
|
-
import {
|
|
8
|
+
import { ensureSignedSettings, isRecord, } from '../adapters/SqlProxyAdapterUtils.js';
|
|
9
|
+
import { SqlProxyHttpAdapterShared } from '../adapters/SqlProxyHttpAdapterShared.js';
|
|
10
|
+
import { createStatementPayload, getExecMetaWithLastRowId, } from '../adapters/SqlProxyRegistryMode.js';
|
|
11
11
|
const resolveProxyMode = () => {
|
|
12
|
-
return
|
|
13
|
-
};
|
|
14
|
-
const resolveBaseUrl = () => {
|
|
15
|
-
const explicit = Env.get('SQLSERVER_PROXY_URL', '').trim();
|
|
16
|
-
if (explicit !== '')
|
|
17
|
-
return explicit;
|
|
18
|
-
const host = Env.get('SQLSERVER_PROXY_HOST', '127.0.0.1');
|
|
19
|
-
const port = Env.getInt('SQLSERVER_PROXY_PORT', 8793);
|
|
20
|
-
return `http://${host}:${port}`;
|
|
12
|
+
return SqlProxyHttpAdapterShared.resolveProxyModeFromEnv('SQLSERVER_PROXY_MODE');
|
|
21
13
|
};
|
|
22
14
|
const buildProxySettings = () => {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
15
|
+
return SqlProxyHttpAdapterShared.buildProxySettingsFromEnv({
|
|
16
|
+
urlKey: 'SQLSERVER_PROXY_URL',
|
|
17
|
+
hostKey: 'SQLSERVER_PROXY_HOST',
|
|
18
|
+
portKey: 'SQLSERVER_PROXY_PORT',
|
|
19
|
+
defaultHost: '127.0.0.1',
|
|
20
|
+
defaultPort: 8793,
|
|
21
|
+
keyIdKey: 'SQLSERVER_PROXY_KEY_ID',
|
|
22
|
+
secretKey: 'SQLSERVER_PROXY_SECRET',
|
|
23
|
+
timeoutKey: 'SQLSERVER_PROXY_TIMEOUT_MS',
|
|
24
|
+
sharedTimeoutKey: 'ZT_PROXY_TIMEOUT_MS',
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
const buildSignedProxyConfig = (settings) => {
|
|
28
|
+
return SqlProxyHttpAdapterShared.buildStandardSignedProxyConfig({
|
|
29
|
+
settings,
|
|
30
|
+
label: 'SQL Server',
|
|
31
|
+
urlKey: 'SQLSERVER_PROXY_URL',
|
|
32
|
+
keyIdKey: 'SQLSERVER_PROXY_KEY_ID',
|
|
33
|
+
secretKey: 'SQLSERVER_PROXY_SECRET',
|
|
34
|
+
});
|
|
28
35
|
};
|
|
29
|
-
const buildSignedProxyConfig = (settings) => ({
|
|
30
|
-
settings,
|
|
31
|
-
missingUrlMessage: 'SQL Server proxy URL is missing (SQLSERVER_PROXY_URL)',
|
|
32
|
-
missingCredentialsMessage: 'SQL Server proxy signing credentials are missing (SQLSERVER_PROXY_KEY_ID / SQLSERVER_PROXY_SECRET)',
|
|
33
|
-
messages: {
|
|
34
|
-
unauthorized: 'SQL Server proxy unauthorized',
|
|
35
|
-
forbidden: 'SQL Server proxy forbidden',
|
|
36
|
-
rateLimited: 'SQL Server proxy rate limited',
|
|
37
|
-
rejected: 'SQL Server proxy rejected request',
|
|
38
|
-
error: 'SQL Server proxy error',
|
|
39
|
-
timedOut: 'SQL Server proxy request timed out',
|
|
40
|
-
},
|
|
41
|
-
});
|
|
42
36
|
const isQueryResponse = (value) => isRecord(value) && Array.isArray(value['rows']) && typeof value['rowCount'] === 'number';
|
|
43
37
|
const isQueryOneResponse = (value) => isRecord(value) && 'row' in value;
|
|
44
|
-
const requestProxy = async (
|
|
38
|
+
const requestProxy = async (signed, path, payload) => SqlProxyHttpAdapterShared.requestProxy(signed, path, payload);
|
|
45
39
|
const requireConnected = (state) => {
|
|
46
40
|
if (!state.connected)
|
|
47
41
|
throw ErrorFactory.createConnectionError('Database not connected');
|
|
@@ -61,8 +55,8 @@ const createQuery = (state) => async (sql, parameters) => {
|
|
|
61
55
|
requireConnected(state);
|
|
62
56
|
const mode = resolveProxyMode();
|
|
63
57
|
const out = mode === 'registry'
|
|
64
|
-
? await requestProxy(state.
|
|
65
|
-
: await requestProxy(state.
|
|
58
|
+
? await requestProxy(state.signed, '/zin/sqlserver/statement', await createStatementPayload(sql, parameters))
|
|
59
|
+
: await requestProxy(state.signed, '/zin/sqlserver/query', {
|
|
66
60
|
sql,
|
|
67
61
|
params: parameters,
|
|
68
62
|
});
|
|
@@ -72,13 +66,13 @@ const createQueryOne = (state) => async (sql, parameters) => {
|
|
|
72
66
|
requireConnected(state);
|
|
73
67
|
const mode = resolveProxyMode();
|
|
74
68
|
if (mode !== 'registry') {
|
|
75
|
-
const out = await requestProxy(state.
|
|
69
|
+
const out = await requestProxy(state.signed, '/zin/sqlserver/queryOne', {
|
|
76
70
|
sql,
|
|
77
71
|
params: parameters,
|
|
78
72
|
});
|
|
79
73
|
return out.row ?? null;
|
|
80
74
|
}
|
|
81
|
-
const out = await requestProxy(state.
|
|
75
|
+
const out = await requestProxy(state.signed, '/zin/sqlserver/statement', await createStatementPayload(sql, parameters));
|
|
82
76
|
if (isQueryOneResponse(out))
|
|
83
77
|
return out.row ?? null;
|
|
84
78
|
if (isQueryResponse(out))
|
|
@@ -123,7 +117,7 @@ const createAdapter = (state) => {
|
|
|
123
117
|
const queryOne = createQueryOne(state);
|
|
124
118
|
const adapter = {
|
|
125
119
|
async connect() {
|
|
126
|
-
ensureSignedSettings(
|
|
120
|
+
ensureSignedSettings(state.signed);
|
|
127
121
|
state.connected = true;
|
|
128
122
|
},
|
|
129
123
|
async disconnect() {
|
|
@@ -135,6 +129,28 @@ const createAdapter = (state) => {
|
|
|
135
129
|
ping: createPing(query),
|
|
136
130
|
transaction: createTransaction(state, () => adapter),
|
|
137
131
|
rawQuery: createRawQuery(query),
|
|
132
|
+
async ensureMigrationsTable() {
|
|
133
|
+
requireConnected(state);
|
|
134
|
+
try {
|
|
135
|
+
await query(`IF OBJECT_ID(N'migrations', N'U') IS NULL
|
|
136
|
+
BEGIN
|
|
137
|
+
CREATE TABLE migrations (
|
|
138
|
+
id INT IDENTITY(1,1) PRIMARY KEY,
|
|
139
|
+
name NVARCHAR(255) NOT NULL,
|
|
140
|
+
scope NVARCHAR(255) NOT NULL DEFAULT 'global',
|
|
141
|
+
service NVARCHAR(255) NOT NULL DEFAULT '',
|
|
142
|
+
batch INT NOT NULL,
|
|
143
|
+
status NVARCHAR(255) NOT NULL,
|
|
144
|
+
applied_at DATETIME2 NULL,
|
|
145
|
+
created_at DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(),
|
|
146
|
+
CONSTRAINT UQ_migrations_name_scope_service UNIQUE (name, scope, service)
|
|
147
|
+
);
|
|
148
|
+
END`, []);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
throw SqlProxyHttpAdapterShared.createProxyNotReachableCliError('SQL Server proxy', state.settings.baseUrl, error);
|
|
152
|
+
}
|
|
153
|
+
},
|
|
138
154
|
getType() {
|
|
139
155
|
return AdaptersEnum.sqlserver;
|
|
140
156
|
},
|
|
@@ -149,6 +165,7 @@ const createAdapter = (state) => {
|
|
|
149
165
|
};
|
|
150
166
|
export function createSqlServerProxyAdapter() {
|
|
151
167
|
const settings = buildProxySettings();
|
|
152
|
-
const
|
|
168
|
+
const signed = buildSignedProxyConfig(settings);
|
|
169
|
+
const state = { connected: false, inTransaction: false, settings, signed };
|
|
153
170
|
return createAdapter(state);
|
|
154
171
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MigrationStore.d.ts","sourceRoot":"","sources":["../../../../src/orm/migrations/MigrationStore.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"MigrationStore.d.ts","sourceRoot":"","sources":["../../../../src/orm/migrations/MigrationStore.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAI/C,OAAO,KAAK,EAAE,eAAe,EAAE,qBAAqB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AA0DhG,eAAO,MAAM,cAAc;oBACH,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;8BAkBzC,SAAS,UACN,cAAc,YACZ,MAAM,GACd,OAAO,CAAC,MAAM,CAAC;sBAiBZ,SAAS,SACN,cAAc,WACZ,MAAM,GACd,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;sBAyBlC,SAAS,UACL;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,cAAc,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAC9E,OAAO,CAAC,IAAI,CAAC;mBAuCV,SAAS,UACL;QACN,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,cAAc,CAAC;QACtB,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,qBAAqB,CAAC;QAC9B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KAC3B,GACA,OAAO,CAAC,IAAI,CAAC;kCAiBV,SAAS,UACL;QAAE,KAAK,EAAE,cAAc,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GACnE,OAAO,CAAC,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;8BAyB5C,SAAS,UACL;QAAE,KAAK,EAAE,cAAc,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GACjD,OAAO,CAAC,MAAM,EAAE,CAAC;qBAgBd,SAAS,UACL;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,cAAc,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAC/D,OAAO,CAAC,IAAI,CAAC;EAShB,CAAC"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Env } from '../../config/env.js';
|
|
1
2
|
import { ErrorFactory } from '../../exceptions/ZintrustError.js';
|
|
2
3
|
import { QueryBuilder } from '../QueryBuilder.js';
|
|
3
4
|
function nowIso() {
|
|
@@ -21,7 +22,19 @@ const hasMigrationsTableSupport = (adapter) => {
|
|
|
21
22
|
};
|
|
22
23
|
const requireMigrationsTableSupport = (adapter) => {
|
|
23
24
|
if (!hasMigrationsTableSupport(adapter)) {
|
|
24
|
-
|
|
25
|
+
const isSqlProxyEnabled = Env.getBool('USE_POSTGRES_PROXY', false) ||
|
|
26
|
+
Env.getBool('USE_MYSQL_PROXY', false) ||
|
|
27
|
+
Env.getBool('USE_SQLSERVER_PROXY', false) ||
|
|
28
|
+
Env.get('POSTGRES_PROXY_URL', '').trim() !== '' ||
|
|
29
|
+
Env.get('MYSQL_PROXY_URL', '').trim() !== '' ||
|
|
30
|
+
Env.get('SQLSERVER_PROXY_URL', '').trim() !== '';
|
|
31
|
+
const hint = isSqlProxyEnabled
|
|
32
|
+
? 'If you are using SQL proxy adapters, ensure the proxy stack is running (e.g. `zin cp up` or `docker compose -f docker-compose.proxy.yml up -d`).'
|
|
33
|
+
: undefined;
|
|
34
|
+
let message = 'Migrations tracking is not supported for this database adapter yet.';
|
|
35
|
+
if (hint)
|
|
36
|
+
message = `${message} ${hint}`;
|
|
37
|
+
throw ErrorFactory.createCliError(message);
|
|
25
38
|
}
|
|
26
39
|
return async () => {
|
|
27
40
|
await adapter.ensureMigrationsTable();
|
|
@@ -32,6 +45,14 @@ export const MigrationStore = Object.freeze({
|
|
|
32
45
|
assertDbSupportsMigrations(db);
|
|
33
46
|
const adapter = db.getAdapterInstance(false);
|
|
34
47
|
const ensure = requireMigrationsTableSupport(adapter);
|
|
48
|
+
// getAdapterInstance(false) returns a raw adapter without going through Database.query()
|
|
49
|
+
// which auto-connects; ensure we're connected before creating the migrations table.
|
|
50
|
+
if (typeof db.connect === 'function') {
|
|
51
|
+
await db.connect();
|
|
52
|
+
}
|
|
53
|
+
else if (typeof adapter.connect === 'function') {
|
|
54
|
+
await adapter.connect();
|
|
55
|
+
}
|
|
35
56
|
await ensure();
|
|
36
57
|
},
|
|
37
58
|
async getLastCompletedBatch(db, scope = 'global', service = '') {
|
package/src/routes/doc.js
CHANGED
|
@@ -16,7 +16,7 @@ export { MIME_TYPES_MAP } from './common.js';
|
|
|
16
16
|
*/
|
|
17
17
|
// Backward-compatible re-exports
|
|
18
18
|
export { findPackageRoot, findPackageRootAsync, getFrameworkPublicRoots, getFrameworkPublicRootsAsync, getPublicRoot, getPublicRootAsync, } from './publicRoot.js';
|
|
19
|
-
const PUBLIC_ROOT_CACHE_TTL_MS = 3000000000; //50 minutes,
|
|
19
|
+
const PUBLIC_ROOT_CACHE_TTL_MS = 3000000000; // 50 minutes, can be adjusted as needed
|
|
20
20
|
let cachedPublicRoot = null;
|
|
21
21
|
const getCachedPublicRootAsync = async () => {
|
|
22
22
|
const now = Date.now();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errorPages.d.ts","sourceRoot":"","sources":["../../../src/routes/errorPages.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAInD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"errorPages.d.ts","sourceRoot":"","sources":["../../../src/routes/errorPages.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAInD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAuKhD,eAAO,MAAM,mBAAmB,GAAI,SAAS,MAAM,EAAE,UAAU,SAAS,KAAG,OAkD1E,CAAC;AAEF,eAAO,MAAM,wBAAwB,GACnC,SAAS,MAAM,EACf,UAAU,SAAS,KAClB,OAAO,CAAC,OAAO,CA4CjB,CAAC;AAmBF,eAAO,MAAM,oBAAoB,GAAI,UAAU,SAAS,KAAG,OAE1D,CAAC;AAEF,eAAO,MAAM,yBAAyB,GAAU,UAAU,SAAS,KAAG,OAAO,CAAC,OAAO,CAKpF,CAAC;AAEF,eAAO,MAAM,wBAAwB,GAAI,QAAQ,OAAO,KAAG,IAO1D,CAAC;;uCAP+C,OAAO,KAAG,IAAI;mCAjIlB,MAAM,YAAY,SAAS,KAAG,OAAO;;AA0IlF,wBAAiE"}
|
package/src/routes/errorPages.js
CHANGED
|
@@ -10,10 +10,17 @@ import { Router } from './Router.js';
|
|
|
10
10
|
import { ErrorFactory } from '../exceptions/ZintrustError.js';
|
|
11
11
|
import * as fs from '../node-singletons/fs.js';
|
|
12
12
|
import * as path from '../node-singletons/path.js';
|
|
13
|
+
let cachedCandidateRoots = null;
|
|
13
14
|
const getCandidatePublicRoots = () => {
|
|
14
|
-
const
|
|
15
|
+
const cwd = process.cwd();
|
|
16
|
+
if (cachedCandidateRoots !== null && cachedCandidateRoots.cwd === cwd) {
|
|
17
|
+
return cachedCandidateRoots.roots;
|
|
18
|
+
}
|
|
19
|
+
const roots = [path.join(cwd, 'public'), ...getFrameworkPublicRoots()];
|
|
15
20
|
const unique = new Set(roots.map((root) => root.trim()).filter((root) => root !== ''));
|
|
16
|
-
|
|
21
|
+
const resolved = [...unique];
|
|
22
|
+
cachedCandidateRoots = { cwd, roots: resolved };
|
|
23
|
+
return resolved;
|
|
17
24
|
};
|
|
18
25
|
const pathExistsAsync = async (candidate) => {
|
|
19
26
|
try {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CsrfTokenManager.d.ts","sourceRoot":"","sources":["../../../src/security/CsrfTokenManager.ts"],"names":[],"mappings":"AACA;;;GAGG;AAQH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAErC,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAClD,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAClE,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClD,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC;IAC/D,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACxD,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAC3B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,aAAa,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,CAAC,OAAO,CAAC,EAAE,uBAAuB,GAAG,iBAAiB,CAAC;CAC9D;AAED,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE/C,MAAM,MAAM,uBAAuB,GAAG;IACpC,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;
|
|
1
|
+
{"version":3,"file":"CsrfTokenManager.d.ts","sourceRoot":"","sources":["../../../src/security/CsrfTokenManager.ts"],"names":[],"mappings":"AACA;;;GAGG;AAQH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAErC,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAClD,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAClE,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClD,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC;IAC/D,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACxD,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAC3B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,aAAa,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,CAAC,OAAO,CAAC,EAAE,uBAAuB,GAAG,iBAAiB,CAAC;CAC9D;AAED,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE/C,MAAM,MAAM,uBAAuB,GAAG;IACpC,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAyVF;;GAEG;AACH,eAAO,MAAM,gBAAgB,EAAE,oBAE7B,CAAC"}
|
|
@@ -121,12 +121,54 @@ const createRedisClientFactory = (options) => {
|
|
|
121
121
|
return redisClient;
|
|
122
122
|
};
|
|
123
123
|
};
|
|
124
|
+
const createRedisPrefixVersioner = (keyPrefix, getRedisClient) => {
|
|
125
|
+
const versionKey = `${keyPrefix}__v`;
|
|
126
|
+
let cachedVersion = null;
|
|
127
|
+
const getVersion = async () => {
|
|
128
|
+
if (cachedVersion !== null)
|
|
129
|
+
return cachedVersion;
|
|
130
|
+
const client = getRedisClient();
|
|
131
|
+
const raw = await client.get(versionKey);
|
|
132
|
+
const v = (raw ?? '1').trim();
|
|
133
|
+
cachedVersion = v === '' ? '1' : v;
|
|
134
|
+
return cachedVersion;
|
|
135
|
+
};
|
|
136
|
+
const getEffectivePrefix = async () => {
|
|
137
|
+
const v = await getVersion();
|
|
138
|
+
return `${keyPrefix}${v}:`;
|
|
139
|
+
};
|
|
140
|
+
const bumpPrefixVersion = async () => {
|
|
141
|
+
const client = getRedisClient();
|
|
142
|
+
// INCR creates the key if it does not exist.
|
|
143
|
+
const next = await client.incr(versionKey);
|
|
144
|
+
cachedVersion = String(next);
|
|
145
|
+
};
|
|
146
|
+
return {
|
|
147
|
+
getEffectivePrefix,
|
|
148
|
+
bumpPrefixVersion,
|
|
149
|
+
};
|
|
150
|
+
};
|
|
151
|
+
const scanRedisKeys = async (client, match) => {
|
|
152
|
+
const keys = [];
|
|
153
|
+
const stream = client.scanStream({ match, count: 200 });
|
|
154
|
+
return new Promise((resolve, reject) => {
|
|
155
|
+
stream.on('data', (resultKeys) => {
|
|
156
|
+
if (Array.isArray(resultKeys) && resultKeys.length) {
|
|
157
|
+
keys.push(...resultKeys);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
stream.on('end', () => resolve(keys));
|
|
161
|
+
stream.on('error', (err) => reject(err));
|
|
162
|
+
});
|
|
163
|
+
};
|
|
124
164
|
const createRedisTokenOperations = (keyPrefix, tokenTtl, getRedisClient) => {
|
|
125
|
-
const
|
|
165
|
+
const { getEffectivePrefix, bumpPrefixVersion } = createRedisPrefixVersioner(keyPrefix, getRedisClient);
|
|
166
|
+
const buildKey = (prefix, sessionId) => `${prefix}${sessionId}`;
|
|
126
167
|
const fetchTokenData = async (sessionId) => {
|
|
127
168
|
try {
|
|
128
169
|
const client = getRedisClient();
|
|
129
|
-
const
|
|
170
|
+
const prefix = await getEffectivePrefix();
|
|
171
|
+
const payload = await client.get(buildKey(prefix, sessionId));
|
|
130
172
|
if (payload === null || payload === '')
|
|
131
173
|
return null;
|
|
132
174
|
const parsed = JSON.parse(payload);
|
|
@@ -140,13 +182,14 @@ const createRedisTokenOperations = (keyPrefix, tokenTtl, getRedisClient) => {
|
|
|
140
182
|
const saveTokenData = async (data) => {
|
|
141
183
|
try {
|
|
142
184
|
const client = getRedisClient();
|
|
185
|
+
const prefix = await getEffectivePrefix();
|
|
143
186
|
const stored = {
|
|
144
187
|
token: data.token,
|
|
145
188
|
sessionId: data.sessionId,
|
|
146
189
|
createdAt: data.createdAt.getTime(),
|
|
147
190
|
expiresAt: data.expiresAt.getTime(),
|
|
148
191
|
};
|
|
149
|
-
await client.set(buildKey(data.sessionId), JSON.stringify(stored), 'PX', tokenTtl);
|
|
192
|
+
await client.set(buildKey(prefix, data.sessionId), JSON.stringify(stored), 'PX', tokenTtl);
|
|
150
193
|
}
|
|
151
194
|
catch (error) {
|
|
152
195
|
Logger.error('CSRF Redis save failed', error);
|
|
@@ -155,37 +198,30 @@ const createRedisTokenOperations = (keyPrefix, tokenTtl, getRedisClient) => {
|
|
|
155
198
|
const deleteToken = async (sessionId) => {
|
|
156
199
|
try {
|
|
157
200
|
const client = getRedisClient();
|
|
158
|
-
await
|
|
201
|
+
const prefix = await getEffectivePrefix();
|
|
202
|
+
await client.del(buildKey(prefix, sessionId));
|
|
159
203
|
}
|
|
160
204
|
catch (error) {
|
|
161
205
|
Logger.error('CSRF Redis delete failed', error);
|
|
162
206
|
}
|
|
163
207
|
};
|
|
164
|
-
const scanKeys = async () => {
|
|
208
|
+
const scanKeys = async (effectivePrefix) => {
|
|
165
209
|
const client = getRedisClient();
|
|
166
|
-
|
|
167
|
-
const stream = client.scanStream({ match: `${keyPrefix}*`, count: 200 });
|
|
168
|
-
return new Promise((resolve, reject) => {
|
|
169
|
-
stream.on('data', (resultKeys) => {
|
|
170
|
-
if (Array.isArray(resultKeys) && resultKeys.length) {
|
|
171
|
-
keys.push(...resultKeys);
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
stream.on('end', () => resolve(keys));
|
|
175
|
-
stream.on('error', (err) => reject(err));
|
|
176
|
-
});
|
|
210
|
+
return scanRedisKeys(client, `${effectivePrefix}*`);
|
|
177
211
|
};
|
|
178
212
|
return {
|
|
179
213
|
fetchTokenData,
|
|
180
214
|
saveTokenData,
|
|
181
215
|
deleteToken,
|
|
182
216
|
scanKeys,
|
|
217
|
+
getEffectivePrefix,
|
|
218
|
+
bumpPrefixVersion,
|
|
183
219
|
};
|
|
184
220
|
};
|
|
185
221
|
const createRedisManager = (tokenLength, tokenTtl, options) => {
|
|
186
222
|
const keyPrefix = options?.keyPrefix ?? RedisKeys.getCsrfPrefix();
|
|
187
223
|
const getRedisClient = createRedisClientFactory(options);
|
|
188
|
-
const { fetchTokenData, saveTokenData, deleteToken, scanKeys } = createRedisTokenOperations(keyPrefix, tokenTtl, getRedisClient);
|
|
224
|
+
const { fetchTokenData, saveTokenData, deleteToken, scanKeys, getEffectivePrefix, bumpPrefixVersion, } = createRedisTokenOperations(keyPrefix, tokenTtl, getRedisClient);
|
|
189
225
|
return {
|
|
190
226
|
async generateToken(sessionId) {
|
|
191
227
|
const token = randomBytes(tokenLength).toString('hex');
|
|
@@ -232,11 +268,8 @@ const createRedisManager = (tokenLength, tokenTtl, options) => {
|
|
|
232
268
|
},
|
|
233
269
|
async clear() {
|
|
234
270
|
try {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
return;
|
|
238
|
-
const client = getRedisClient();
|
|
239
|
-
await client.del(...keys);
|
|
271
|
+
// Logical clear: bump the version so future operations use a new prefix.
|
|
272
|
+
await bumpPrefixVersion();
|
|
240
273
|
}
|
|
241
274
|
catch (error) {
|
|
242
275
|
Logger.error('CSRF Redis clear failed', error);
|
|
@@ -244,7 +277,8 @@ const createRedisManager = (tokenLength, tokenTtl, options) => {
|
|
|
244
277
|
},
|
|
245
278
|
async getTokenCount() {
|
|
246
279
|
try {
|
|
247
|
-
const
|
|
280
|
+
const prefix = await getEffectivePrefix();
|
|
281
|
+
const keys = await scanKeys(prefix);
|
|
248
282
|
return keys.length;
|
|
249
283
|
}
|
|
250
284
|
catch (error) {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* JSON Web Token generation, verification, and claims management
|
|
4
4
|
* Uses native Node.js crypto module (zero external dependencies)
|
|
5
5
|
*/
|
|
6
|
+
import type { AuthorizationHeader } from './JwtSessions';
|
|
6
7
|
export type JwtAlgorithm = 'HS256' | 'HS512' | 'RS256';
|
|
7
8
|
export interface JwtPayload {
|
|
8
9
|
sub?: string;
|
|
@@ -33,7 +34,9 @@ export interface IJwtManager {
|
|
|
33
34
|
}
|
|
34
35
|
export interface JwtManagerType {
|
|
35
36
|
create(): IJwtManager;
|
|
36
|
-
signAccessToken: (payload: JwtPayload, expiresIn?: number) => string
|
|
37
|
+
signAccessToken: (payload: JwtPayload, expiresIn?: number) => Promise<string>;
|
|
38
|
+
logout: (authHeader: AuthorizationHeader) => Promise<void>;
|
|
39
|
+
logoutAll: (sub: string) => Promise<void>;
|
|
37
40
|
}
|
|
38
41
|
/**
|
|
39
42
|
* JwtManager namespace - sealed for immutability
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"JwtManager.d.ts","sourceRoot":"","sources":["../../../src/security/JwtManager.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"JwtManager.d.ts","sourceRoot":"","sources":["../../../src/security/JwtManager.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAGjE,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;AAEvD,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,UAAU;IACzB,SAAS,CAAC,EAAE,YAAY,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,WAAW;IAC1B,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACxD,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,MAAM,CAAC;IACxD,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,YAAY,GAAG,UAAU,CAAC;IAC5D,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CAAC;IAClC,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;IACjC,aAAa,IAAI,MAAM,CAAC;CACzB;AAQD,MAAM,WAAW,cAAc;IAC7B,MAAM,IAAI,WAAW,CAAC;IACtB,eAAe,EAAE,CAAC,OAAO,EAAE,UAAU,EAAE,SAAS,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9E,MAAM,EAAE,CAAC,UAAU,EAAE,mBAAmB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3C;AAyFD;;GAEG;AACH,eAAO,MAAM,UAAU,EAAE,cAKvB,CAAC"}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { securityConfig } from '../config/index.js';
|
|
7
7
|
import { ErrorFactory } from '../exceptions/ZintrustError.js';
|
|
8
8
|
import { createHmac, createSign, createVerify, randomBytes } from '../node-singletons/crypto.js';
|
|
9
|
+
import { JwtSessions } from './JwtSessions.js';
|
|
9
10
|
const createJwt = () => {
|
|
10
11
|
const algorithm = securityConfig.jwt.algorithm;
|
|
11
12
|
const secret = securityConfig.jwt.secret;
|
|
@@ -15,27 +16,38 @@ const createJwt = () => {
|
|
|
15
16
|
}
|
|
16
17
|
return jwt;
|
|
17
18
|
};
|
|
18
|
-
const
|
|
19
|
+
const logout = async (authHeader) => {
|
|
20
|
+
await JwtSessions.logout(authHeader);
|
|
21
|
+
};
|
|
22
|
+
const logoutAll = async (sub) => {
|
|
23
|
+
await JwtSessions.logoutAll(sub);
|
|
24
|
+
};
|
|
25
|
+
const signAccessToken = async (payload, expiresIn) => {
|
|
19
26
|
const algorithm = securityConfig.jwt.algorithm;
|
|
20
27
|
const jwt = createJwt();
|
|
28
|
+
let token;
|
|
21
29
|
// JwtManager currently supports HMAC secrets directly for HS algorithms.
|
|
22
30
|
// For other algorithms, verify will still reject mismatched tokens.
|
|
23
31
|
if (algorithm !== 'HS256' && algorithm !== 'HS512') {
|
|
24
|
-
|
|
32
|
+
token = jwt.sign(payload, {
|
|
33
|
+
algorithm,
|
|
34
|
+
issuer: securityConfig.jwt.issuer,
|
|
35
|
+
audience: securityConfig.jwt.audience,
|
|
36
|
+
jwtId: jwt.generateJwtId(),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
token = jwt.sign(payload, {
|
|
25
41
|
algorithm,
|
|
42
|
+
expiresIn: expiresIn ?? securityConfig.jwt.expiresIn,
|
|
26
43
|
issuer: securityConfig.jwt.issuer,
|
|
27
44
|
audience: securityConfig.jwt.audience,
|
|
45
|
+
subject: typeof payload.sub === 'string' ? payload.sub : undefined,
|
|
28
46
|
jwtId: jwt.generateJwtId(),
|
|
29
47
|
});
|
|
30
48
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
expiresIn: expiresIn ?? securityConfig.jwt.expiresIn,
|
|
34
|
-
issuer: securityConfig.jwt.issuer,
|
|
35
|
-
audience: securityConfig.jwt.audience,
|
|
36
|
-
subject: typeof payload.sub === 'string' ? payload.sub : undefined,
|
|
37
|
-
jwtId: jwt.generateJwtId(),
|
|
38
|
-
});
|
|
49
|
+
await JwtSessions.register(token);
|
|
50
|
+
return token;
|
|
39
51
|
};
|
|
40
52
|
/**
|
|
41
53
|
* Create a new JWT manager instance
|
|
@@ -77,6 +89,8 @@ const create = () => {
|
|
|
77
89
|
export const JwtManager = Object.freeze({
|
|
78
90
|
create,
|
|
79
91
|
signAccessToken,
|
|
92
|
+
logout,
|
|
93
|
+
logoutAll,
|
|
80
94
|
});
|
|
81
95
|
/**
|
|
82
96
|
* Sign JWT token
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type JwtSessionsDriverName = 'database' | 'memory' | 'redis' | 'kv' | 'kv-remote';
|
|
2
|
+
export type AuthorizationHeader = string | string[] | undefined;
|
|
3
|
+
export declare const JwtSessions: Readonly<{
|
|
4
|
+
register(token: string): Promise<void>;
|
|
5
|
+
isActive(token: string): Promise<boolean>;
|
|
6
|
+
logout(header: AuthorizationHeader): Promise<string | null>;
|
|
7
|
+
logoutAll(sub: string): Promise<void>;
|
|
8
|
+
getDriver(): JwtSessionsDriverName;
|
|
9
|
+
_resetForTests(): void;
|
|
10
|
+
}>;
|
|
11
|
+
export default JwtSessions;
|
|
12
|
+
//# sourceMappingURL=JwtSessions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"JwtSessions.d.ts","sourceRoot":"","sources":["../../../src/security/JwtSessions.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,qBAAqB,GAAG,UAAU,GAAG,QAAQ,GAAG,OAAO,GAAG,IAAI,GAAG,WAAW,CAAC;AAEzF,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC;AAqnBhE,eAAO,MAAM,WAAW;oBACA,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;oBAKtB,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;mBAM1B,mBAAmB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;mBAU5C,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;iBAQ9B,qBAAqB;sBAIhB,IAAI;EAItB,CAAC;AAEH,eAAe,WAAW,CAAC"}
|