@zintrust/core 0.1.14 → 0.1.16
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/package.json +1 -1
- package/src/boot/Application.d.ts.map +1 -1
- package/src/boot/Application.js +29 -61
- package/src/cli/CLI.d.ts.map +1 -1
- package/src/cli/CLI.js +6 -0
- package/src/cli/commands/BroadcastWorkCommand.d.ts +10 -0
- package/src/cli/commands/BroadcastWorkCommand.d.ts.map +1 -0
- package/src/cli/commands/BroadcastWorkCommand.js +16 -0
- package/src/cli/commands/NotificationWorkCommand.d.ts +10 -0
- package/src/cli/commands/NotificationWorkCommand.d.ts.map +1 -0
- package/src/cli/commands/NotificationWorkCommand.js +16 -0
- package/src/cli/commands/QueueCommand.d.ts +10 -0
- package/src/cli/commands/QueueCommand.d.ts.map +1 -0
- package/src/cli/commands/QueueCommand.js +63 -0
- package/src/cli/commands/QueueWorkCommandUtils.d.ts +10 -0
- package/src/cli/commands/QueueWorkCommandUtils.d.ts.map +1 -0
- package/src/cli/commands/QueueWorkCommandUtils.js +43 -0
- package/src/cli/commands/createKindWorkCommand.d.ts +9 -0
- package/src/cli/commands/createKindWorkCommand.d.ts.map +1 -0
- package/src/cli/commands/createKindWorkCommand.js +33 -0
- package/src/cli/commands/index.d.ts +3 -0
- package/src/cli/commands/index.d.ts.map +1 -1
- package/src/cli/commands/index.js +3 -0
- package/src/cli/scaffolding/ModelGenerator.d.ts.map +1 -1
- package/src/cli/scaffolding/ModelGenerator.js +1 -0
- package/src/cli/workers/QueueWorkRunner.d.ts +23 -0
- package/src/cli/workers/QueueWorkRunner.d.ts.map +1 -0
- package/src/cli/workers/QueueWorkRunner.js +142 -0
- package/src/collections/Collection.d.ts +30 -0
- package/src/collections/Collection.d.ts.map +1 -0
- package/src/collections/Collection.js +146 -0
- package/src/collections/index.d.ts +3 -0
- package/src/collections/index.d.ts.map +1 -0
- package/src/collections/index.js +1 -0
- package/src/config/broadcast.d.ts.map +1 -1
- package/src/config/broadcast.js +5 -3
- package/src/config/cache.d.ts.map +1 -1
- package/src/config/cache.js +12 -6
- package/src/config/database.d.ts.map +1 -1
- package/src/config/database.js +5 -3
- package/src/config/mail.d.ts.map +1 -1
- package/src/config/mail.js +21 -14
- package/src/config/notification.d.ts.map +1 -1
- package/src/config/notification.js +10 -5
- package/src/config/storage.d.ts.map +1 -1
- package/src/config/storage.js +5 -6
- package/src/events/EventDispatcher.d.ts +16 -0
- package/src/events/EventDispatcher.d.ts.map +1 -0
- package/src/events/EventDispatcher.js +90 -0
- package/src/events/index.d.ts +3 -0
- package/src/events/index.d.ts.map +1 -0
- package/src/events/index.js +1 -0
- package/src/features/Queue.js +1 -1
- package/src/http/Response.d.ts +2 -2
- package/src/http/Response.d.ts.map +1 -1
- package/src/index.d.ts +11 -0
- package/src/index.d.ts.map +1 -1
- package/src/index.js +11 -0
- package/src/middleware/CsrfMiddleware.d.ts.map +1 -1
- package/src/middleware/CsrfMiddleware.js +20 -25
- package/src/middleware/SessionMiddleware.d.ts +8 -0
- package/src/middleware/SessionMiddleware.d.ts.map +1 -0
- package/src/middleware/SessionMiddleware.js +15 -0
- package/src/orm/DatabaseRuntimeRegistration.d.ts.map +1 -1
- package/src/orm/DatabaseRuntimeRegistration.js +4 -2
- package/src/orm/Model.d.ts +15 -0
- package/src/orm/Model.d.ts.map +1 -1
- package/src/orm/Model.js +57 -8
- package/src/orm/QueryBuilder.d.ts +9 -1
- package/src/orm/QueryBuilder.d.ts.map +1 -1
- package/src/orm/QueryBuilder.js +54 -2
- package/src/scripts/TemplateSync.js +23 -1
- package/src/security/PasswordResetTokenBroker.d.ts +39 -0
- package/src/security/PasswordResetTokenBroker.d.ts.map +1 -0
- package/src/security/PasswordResetTokenBroker.js +131 -0
- package/src/session/SessionManager.d.ts +39 -0
- package/src/session/SessionManager.d.ts.map +1 -0
- package/src/session/SessionManager.js +149 -0
- package/src/session/index.d.ts +3 -0
- package/src/session/index.d.ts.map +1 -0
- package/src/session/index.js +1 -0
- package/src/templates/features/Queue.ts.tpl +4 -3
- package/src/templates/project/basic/config/FileLogWriter.ts.tpl +4 -3
- package/src/templates/project/basic/config/SecretsManager.ts.tpl +1 -1
- package/src/templates/project/basic/config/broadcast.ts.tpl +6 -4
- package/src/templates/project/basic/config/cache.ts.tpl +17 -5
- package/src/templates/project/basic/config/database.ts.tpl +6 -4
- package/src/templates/project/basic/config/features.ts.tpl +2 -2
- package/src/templates/project/basic/config/logger.ts.tpl +0 -2
- package/src/templates/project/basic/config/logging/HttpLogger.ts.tpl +1 -1
- package/src/templates/project/basic/config/logging/SlackLogger.ts.tpl +1 -1
- package/src/templates/project/basic/config/mail.ts.tpl +26 -16
- package/src/templates/project/basic/config/microservices.ts.tpl +1 -1
- package/src/templates/project/basic/config/middleware.ts.tpl +6 -9
- package/src/templates/project/basic/config/notification.ts.tpl +19 -7
- package/src/templates/project/basic/config/security.ts.tpl +1 -2
- package/src/templates/project/basic/config/storage.ts.tpl +8 -6
- package/src/templates/project/basic/config/type.ts.tpl +2 -2
- package/src/tools/broadcast/Broadcast.d.ts +8 -0
- package/src/tools/broadcast/Broadcast.d.ts.map +1 -1
- package/src/tools/broadcast/Broadcast.js +23 -0
- package/src/tools/broadcast/BroadcastRuntimeRegistration.d.ts.map +1 -1
- package/src/tools/broadcast/BroadcastRuntimeRegistration.js +7 -4
- package/src/tools/notification/Notification.d.ts +10 -0
- package/src/tools/notification/Notification.d.ts.map +1 -1
- package/src/tools/notification/Notification.js +21 -0
- package/src/tools/notification/NotificationRuntimeRegistration.d.ts.map +1 -1
- package/src/tools/notification/NotificationRuntimeRegistration.js +7 -4
- package/src/tools/queue/Queue.d.ts.map +1 -1
- package/src/tools/queue/Queue.js +4 -1
- package/src/tools/queue/QueueRuntimeRegistration.d.ts.map +1 -1
- package/src/tools/queue/QueueRuntimeRegistration.js +5 -8
- package/src/tools/storage/StorageRuntimeRegistration.d.ts.map +1 -1
- package/src/tools/storage/StorageRuntimeRegistration.js +8 -10
- package/src/workers/BroadcastWorker.d.ts +22 -0
- package/src/workers/BroadcastWorker.d.ts.map +1 -0
- package/src/workers/BroadcastWorker.js +24 -0
- package/src/workers/NotificationWorker.d.ts +22 -0
- package/src/workers/NotificationWorker.d.ts.map +1 -0
- package/src/workers/NotificationWorker.js +23 -0
- package/src/workers/createQueueWorker.d.ts +24 -0
- package/src/workers/createQueueWorker.d.ts.map +1 -0
- package/src/workers/createQueueWorker.js +114 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Password Reset Token Broker
|
|
3
|
+
*
|
|
4
|
+
* Framework-agnostic, storage-pluggable password reset token flow.
|
|
5
|
+
*
|
|
6
|
+
* - Generates high-entropy tokens for a given identifier (usually an email).
|
|
7
|
+
* - Stores only a SHA-256 hash of the token (one active token per identifier).
|
|
8
|
+
* - Supports verification and one-time consumption.
|
|
9
|
+
*/
|
|
10
|
+
import { ErrorFactory } from '../exceptions/ZintrustError.js';
|
|
11
|
+
import { createHash, randomBytes } from '../node-singletons/crypto.js';
|
|
12
|
+
const DEFAULT_TTL_MS = 30 * 60 * 1000;
|
|
13
|
+
const DEFAULT_TOKEN_BYTES = 32; // 256 bits
|
|
14
|
+
const createInMemoryStore = () => {
|
|
15
|
+
const map = new Map();
|
|
16
|
+
return {
|
|
17
|
+
set(record) {
|
|
18
|
+
map.set(record.identifier, record);
|
|
19
|
+
},
|
|
20
|
+
get(identifier) {
|
|
21
|
+
return map.get(identifier) ?? null;
|
|
22
|
+
},
|
|
23
|
+
delete(identifier) {
|
|
24
|
+
map.delete(identifier);
|
|
25
|
+
},
|
|
26
|
+
cleanup(now = new Date()) {
|
|
27
|
+
let removed = 0;
|
|
28
|
+
for (const [identifier, record] of map.entries()) {
|
|
29
|
+
if (now.getTime() > record.expiresAt.getTime()) {
|
|
30
|
+
map.delete(identifier);
|
|
31
|
+
removed++;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return removed;
|
|
35
|
+
},
|
|
36
|
+
clear() {
|
|
37
|
+
map.clear();
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
const create = (options = {}) => {
|
|
42
|
+
const store = options.store ?? createInMemoryStore();
|
|
43
|
+
const ttlMs = normalizeTtlMs(options.ttlMs ?? DEFAULT_TTL_MS);
|
|
44
|
+
const tokenBytes = normalizeTokenBytes(options.tokenBytes ?? DEFAULT_TOKEN_BYTES);
|
|
45
|
+
const now = options.now ?? (() => new Date());
|
|
46
|
+
return {
|
|
47
|
+
async createToken(identifier) {
|
|
48
|
+
const normalizedIdentifier = normalizeIdentifier(identifier);
|
|
49
|
+
const token = randomBytes(tokenBytes).toString('hex');
|
|
50
|
+
const tokenHash = sha256Hex(token);
|
|
51
|
+
const createdAt = now();
|
|
52
|
+
const expiresAt = new Date(createdAt.getTime() + ttlMs);
|
|
53
|
+
await store.set({ identifier: normalizedIdentifier, tokenHash, createdAt, expiresAt });
|
|
54
|
+
return token;
|
|
55
|
+
},
|
|
56
|
+
async verifyToken(identifier, token) {
|
|
57
|
+
const normalizedIdentifier = normalizeIdentifier(identifier);
|
|
58
|
+
const normalizedToken = normalizeToken(token);
|
|
59
|
+
const record = await store.get(normalizedIdentifier);
|
|
60
|
+
if (record === null)
|
|
61
|
+
return false;
|
|
62
|
+
if (isExpired(record, now())) {
|
|
63
|
+
await store.delete(normalizedIdentifier);
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
const computed = sha256Hex(normalizedToken);
|
|
67
|
+
return timingSafeEquals(record.tokenHash, computed);
|
|
68
|
+
},
|
|
69
|
+
async consumeToken(identifier, token) {
|
|
70
|
+
const normalizedIdentifier = normalizeIdentifier(identifier);
|
|
71
|
+
const ok = await this.verifyToken(normalizedIdentifier, token);
|
|
72
|
+
if (!ok)
|
|
73
|
+
return false;
|
|
74
|
+
await store.delete(normalizedIdentifier);
|
|
75
|
+
return true;
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
export const PasswordResetTokenBroker = Object.freeze({
|
|
80
|
+
create,
|
|
81
|
+
createInMemoryStore,
|
|
82
|
+
});
|
|
83
|
+
function normalizeIdentifier(identifier) {
|
|
84
|
+
if (typeof identifier !== 'string') {
|
|
85
|
+
throw ErrorFactory.createValidationError('Invalid identifier');
|
|
86
|
+
}
|
|
87
|
+
const trimmed = identifier.trim();
|
|
88
|
+
if (trimmed.length === 0) {
|
|
89
|
+
throw ErrorFactory.createValidationError('Invalid identifier');
|
|
90
|
+
}
|
|
91
|
+
return trimmed;
|
|
92
|
+
}
|
|
93
|
+
function normalizeToken(token) {
|
|
94
|
+
if (typeof token !== 'string') {
|
|
95
|
+
throw ErrorFactory.createValidationError('Invalid token');
|
|
96
|
+
}
|
|
97
|
+
const trimmed = token.trim();
|
|
98
|
+
if (trimmed.length === 0) {
|
|
99
|
+
throw ErrorFactory.createValidationError('Invalid token');
|
|
100
|
+
}
|
|
101
|
+
return trimmed;
|
|
102
|
+
}
|
|
103
|
+
function normalizeTtlMs(ttlMs) {
|
|
104
|
+
const value = Number.isFinite(ttlMs) ? Math.trunc(ttlMs) : 0;
|
|
105
|
+
if (value <= 0) {
|
|
106
|
+
throw ErrorFactory.createConfigError('Invalid password reset TTL', { ttlMs });
|
|
107
|
+
}
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
function normalizeTokenBytes(tokenBytes) {
|
|
111
|
+
const value = Number.isFinite(tokenBytes) ? Math.trunc(tokenBytes) : 0;
|
|
112
|
+
if (value <= 0) {
|
|
113
|
+
throw ErrorFactory.createConfigError('Invalid password reset token bytes', { tokenBytes });
|
|
114
|
+
}
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
function isExpired(record, now) {
|
|
118
|
+
return now.getTime() > record.expiresAt.getTime();
|
|
119
|
+
}
|
|
120
|
+
function sha256Hex(value) {
|
|
121
|
+
return createHash('sha256').update(value).digest('hex');
|
|
122
|
+
}
|
|
123
|
+
function timingSafeEquals(a, b) {
|
|
124
|
+
if (a.length !== b.length)
|
|
125
|
+
return false;
|
|
126
|
+
let result = 0;
|
|
127
|
+
for (let i = 0; i < a.length; i++) {
|
|
128
|
+
result |= (a.codePointAt(i) ?? 0) ^ (b.codePointAt(i) ?? 0);
|
|
129
|
+
}
|
|
130
|
+
return result === 0;
|
|
131
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type SessionData = Record<string, unknown>;
|
|
2
|
+
export interface ISession {
|
|
3
|
+
id: string;
|
|
4
|
+
get<T = unknown>(key: string): T | undefined;
|
|
5
|
+
set(key: string, value: unknown): void;
|
|
6
|
+
has(key: string): boolean;
|
|
7
|
+
forget(key: string): void;
|
|
8
|
+
all(): SessionData;
|
|
9
|
+
clear(): void;
|
|
10
|
+
}
|
|
11
|
+
export interface ISessionManager {
|
|
12
|
+
getIdFromCookieHeader(cookieHeader: string | undefined): string | undefined;
|
|
13
|
+
getIdFromRequest(req: {
|
|
14
|
+
getHeader: (name: string) => unknown;
|
|
15
|
+
sessionId?: unknown;
|
|
16
|
+
context?: Record<string, unknown>;
|
|
17
|
+
}): string | undefined;
|
|
18
|
+
ensureSessionId(req: {
|
|
19
|
+
getHeader: (name: string) => unknown;
|
|
20
|
+
sessionId?: unknown;
|
|
21
|
+
context: Record<string, unknown>;
|
|
22
|
+
}, res: {
|
|
23
|
+
getHeader: (name: string) => unknown;
|
|
24
|
+
setHeader: (name: string, value: string | string[]) => unknown;
|
|
25
|
+
}): Promise<string>;
|
|
26
|
+
get(sessionId: string): ISession;
|
|
27
|
+
destroy(sessionId: string): void;
|
|
28
|
+
cleanup(): number;
|
|
29
|
+
}
|
|
30
|
+
export interface SessionManagerOptions {
|
|
31
|
+
cookieName?: string;
|
|
32
|
+
headerName?: string;
|
|
33
|
+
ttlMs?: number;
|
|
34
|
+
}
|
|
35
|
+
export declare const SessionManager: Readonly<{
|
|
36
|
+
create(options?: SessionManagerOptions): ISessionManager;
|
|
37
|
+
}>;
|
|
38
|
+
export default SessionManager;
|
|
39
|
+
//# sourceMappingURL=SessionManager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SessionManager.d.ts","sourceRoot":"","sources":["../../../src/session/SessionManager.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAElD,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAAC;IAC7C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IACvC,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAC1B,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,GAAG,IAAI,WAAW,CAAC;IACnB,KAAK,IAAI,IAAI,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAAC;IAC5E,gBAAgB,CAAC,GAAG,EAAE;QACpB,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;QACrC,SAAS,CAAC,EAAE,OAAO,CAAC;QACpB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACnC,GAAG,MAAM,GAAG,SAAS,CAAC;IACvB,eAAe,CACb,GAAG,EAAE;QACH,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;QACrC,SAAS,CAAC,EAAE,OAAO,CAAC;QACpB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KAClC,EACD,GAAG,EAAE;QACH,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;QACrC,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,KAAK,OAAO,CAAC;KAChE,GACA,OAAO,CAAC,MAAM,CAAC,CAAC;IACnB,GAAG,CAAC,SAAS,EAAE,MAAM,GAAG,QAAQ,CAAC;IACjC,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,OAAO,IAAI,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,qBAAqB;IACpC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAyHD,eAAO,MAAM,cAAc;qBACT,qBAAqB,GAAQ,eAAe;EA2E5D,CAAC;AAEH,eAAe,cAAc,CAAC"}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { generateSecureJobId } from '../common/uuid.js';
|
|
2
|
+
const DEFAULT_OPTIONS = {
|
|
3
|
+
cookieName: 'ZIN_SESSION_ID',
|
|
4
|
+
headerName: 'x-session-id',
|
|
5
|
+
ttlMs: 7 * 24 * 60 * 60 * 1000,
|
|
6
|
+
};
|
|
7
|
+
function parseCookies(cookieHeader) {
|
|
8
|
+
const list = {};
|
|
9
|
+
if (cookieHeader.length === 0)
|
|
10
|
+
return list;
|
|
11
|
+
cookieHeader.split(';').forEach((cookie) => {
|
|
12
|
+
const parts = cookie.split('=');
|
|
13
|
+
const name = parts.shift()?.trim();
|
|
14
|
+
const value = parts.join('=');
|
|
15
|
+
if (name !== null && name !== undefined)
|
|
16
|
+
list[name] = decodeURIComponent(value);
|
|
17
|
+
});
|
|
18
|
+
return list;
|
|
19
|
+
}
|
|
20
|
+
function appendSetCookie(res, cookie) {
|
|
21
|
+
const existing = res.getHeader('Set-Cookie');
|
|
22
|
+
if (existing === undefined) {
|
|
23
|
+
res.setHeader('Set-Cookie', cookie);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (Array.isArray(existing)) {
|
|
27
|
+
const existingCookies = existing.map(String);
|
|
28
|
+
res.setHeader('Set-Cookie', [...existingCookies, cookie]);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (typeof existing === 'string') {
|
|
32
|
+
res.setHeader('Set-Cookie', [existing, cookie]);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
res.setHeader('Set-Cookie', cookie);
|
|
36
|
+
}
|
|
37
|
+
function buildSessionCookie(cookieName, sessionId) {
|
|
38
|
+
// Keep this minimal; callers can override behavior later.
|
|
39
|
+
// HttpOnly prevents JS access; SameSite=Lax is a reasonable default for app sessions.
|
|
40
|
+
return `${cookieName}=${encodeURIComponent(sessionId)}; Path=/; HttpOnly; SameSite=Lax`;
|
|
41
|
+
}
|
|
42
|
+
function createSessionApi(sessions, sessionId, ttlMs) {
|
|
43
|
+
const withoutKey = (data, key) => {
|
|
44
|
+
if (!Object.prototype.hasOwnProperty.call(data, key))
|
|
45
|
+
return data;
|
|
46
|
+
const record = data;
|
|
47
|
+
const { [key]: _removed, ...rest } = record;
|
|
48
|
+
return rest;
|
|
49
|
+
};
|
|
50
|
+
const ensureStored = () => {
|
|
51
|
+
const existing = sessions.get(sessionId);
|
|
52
|
+
const now = Date.now();
|
|
53
|
+
if (existing !== undefined && existing.expiresAt > now) {
|
|
54
|
+
return existing;
|
|
55
|
+
}
|
|
56
|
+
const created = { data: {}, expiresAt: now + ttlMs };
|
|
57
|
+
sessions.set(sessionId, created);
|
|
58
|
+
return created;
|
|
59
|
+
};
|
|
60
|
+
return {
|
|
61
|
+
id: sessionId,
|
|
62
|
+
get(key) {
|
|
63
|
+
return ensureStored().data[key];
|
|
64
|
+
},
|
|
65
|
+
set(key, value) {
|
|
66
|
+
const stored = ensureStored();
|
|
67
|
+
stored.data[key] = value;
|
|
68
|
+
stored.expiresAt = Date.now() + ttlMs;
|
|
69
|
+
},
|
|
70
|
+
has(key) {
|
|
71
|
+
return Object.prototype.hasOwnProperty.call(ensureStored().data, key);
|
|
72
|
+
},
|
|
73
|
+
forget(key) {
|
|
74
|
+
const stored = ensureStored();
|
|
75
|
+
stored.data = withoutKey(stored.data, key);
|
|
76
|
+
stored.expiresAt = Date.now() + ttlMs;
|
|
77
|
+
},
|
|
78
|
+
all() {
|
|
79
|
+
return { ...ensureStored().data };
|
|
80
|
+
},
|
|
81
|
+
clear() {
|
|
82
|
+
const stored = ensureStored();
|
|
83
|
+
stored.data = {};
|
|
84
|
+
stored.expiresAt = Date.now() + ttlMs;
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export const SessionManager = Object.freeze({
|
|
89
|
+
create(options = {}) {
|
|
90
|
+
const config = { ...DEFAULT_OPTIONS, ...options };
|
|
91
|
+
const sessions = new Map();
|
|
92
|
+
return {
|
|
93
|
+
getIdFromCookieHeader(cookieHeader) {
|
|
94
|
+
if (cookieHeader === undefined || cookieHeader.length === 0)
|
|
95
|
+
return undefined;
|
|
96
|
+
const cookies = parseCookies(cookieHeader);
|
|
97
|
+
return cookies[config.cookieName];
|
|
98
|
+
},
|
|
99
|
+
getIdFromRequest(req) {
|
|
100
|
+
const cookieHeader = req.getHeader('cookie');
|
|
101
|
+
if (typeof cookieHeader === 'string') {
|
|
102
|
+
const fromCookie = this.getIdFromCookieHeader(cookieHeader);
|
|
103
|
+
if (fromCookie !== undefined)
|
|
104
|
+
return fromCookie;
|
|
105
|
+
}
|
|
106
|
+
const fromHeader = req.getHeader(config.headerName);
|
|
107
|
+
if (typeof fromHeader === 'string' && fromHeader.length > 0)
|
|
108
|
+
return fromHeader;
|
|
109
|
+
if (typeof req.sessionId === 'string' && req.sessionId.length > 0)
|
|
110
|
+
return req.sessionId;
|
|
111
|
+
const fromContext = req.context?.['sessionId'];
|
|
112
|
+
if (typeof fromContext === 'string' && fromContext.length > 0)
|
|
113
|
+
return fromContext;
|
|
114
|
+
return undefined;
|
|
115
|
+
},
|
|
116
|
+
async ensureSessionId(req, res) {
|
|
117
|
+
const existing = this.getIdFromRequest(req);
|
|
118
|
+
const sessionId = existing ??
|
|
119
|
+
(await Promise.resolve(generateSecureJobId('SessionManager: secure crypto API not available to generate a session id')));
|
|
120
|
+
req.context['sessionId'] = sessionId;
|
|
121
|
+
// If the cookie is missing, set it.
|
|
122
|
+
const cookieHeader = req.getHeader('cookie');
|
|
123
|
+
const fromCookie = typeof cookieHeader === 'string' ? this.getIdFromCookieHeader(cookieHeader) : undefined;
|
|
124
|
+
if (fromCookie === undefined) {
|
|
125
|
+
appendSetCookie(res, buildSessionCookie(config.cookieName, sessionId));
|
|
126
|
+
}
|
|
127
|
+
return sessionId;
|
|
128
|
+
},
|
|
129
|
+
get(sessionId) {
|
|
130
|
+
return createSessionApi(sessions, sessionId, config.ttlMs);
|
|
131
|
+
},
|
|
132
|
+
destroy(sessionId) {
|
|
133
|
+
sessions.delete(sessionId);
|
|
134
|
+
},
|
|
135
|
+
cleanup() {
|
|
136
|
+
const now = Date.now();
|
|
137
|
+
let removed = 0;
|
|
138
|
+
for (const [id, stored] of sessions.entries()) {
|
|
139
|
+
if (stored.expiresAt <= now) {
|
|
140
|
+
sessions.delete(id);
|
|
141
|
+
removed++;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return removed;
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
export default SessionManager;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/session/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACzD,YAAY,EAAE,QAAQ,EAAE,eAAe,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SessionManager } from './SessionManager.js';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// TEMPLATE_START
|
|
2
2
|
|
|
3
|
-
import { generateSecureJobId
|
|
3
|
+
import { generateSecureJobId } from '@common/uuid';
|
|
4
|
+
import { Logger } from '@config/logger';
|
|
4
5
|
|
|
5
6
|
export interface QueueJob {
|
|
6
7
|
id: string;
|
|
@@ -19,7 +20,7 @@ export const Queue = Object.freeze({
|
|
|
19
20
|
* Add a job to the queue
|
|
20
21
|
*/
|
|
21
22
|
async add<T>(data: T): Promise<string> {
|
|
22
|
-
const id =
|
|
23
|
+
const id = generateSecureJobId();
|
|
23
24
|
const job: QueueJob = {
|
|
24
25
|
id,
|
|
25
26
|
data,
|
|
@@ -43,4 +44,4 @@ export const Queue = Object.freeze({
|
|
|
43
44
|
},
|
|
44
45
|
});
|
|
45
46
|
|
|
46
|
-
// TEMPLATE_END
|
|
47
|
+
// TEMPLATE_END
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* FileLogWriter (Node.js only)
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* Provides best-effort file logging with daily + size-based rotation.
|
|
5
|
+
* This module imports Node built-ins and should be loaded only in Node environments.
|
|
4
6
|
*/
|
|
5
7
|
|
|
6
8
|
import { ensureDirSafe } from '@zintrust/core';
|
|
9
|
+
import { Env } from './env';
|
|
7
10
|
import * as fs from 'node:fs';
|
|
8
11
|
import * as path from 'node:path';
|
|
9
12
|
|
|
10
|
-
import { Env } from './env';
|
|
11
|
-
|
|
12
13
|
const getCwdSafe = (): string => {
|
|
13
14
|
try {
|
|
14
15
|
if (typeof process === 'undefined' || typeof process.cwd !== 'function') return '';
|
|
@@ -24,13 +24,15 @@ const hasOwn = (obj: Record<string, unknown>, key: string): boolean => {
|
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
const getDefaultBroadcaster = (drivers: BroadcastDrivers): string => {
|
|
27
|
-
const
|
|
27
|
+
const envSelectedRaw = Env.get('BROADCAST_CONNECTION', Env.get('BROADCAST_DRIVER', 'inmemory'));
|
|
28
|
+
const value = normalizeDriverName(envSelectedRaw ?? 'inmemory');
|
|
28
29
|
|
|
29
|
-
if (value.length > 0 && hasOwn(drivers, value))
|
|
30
|
-
|
|
30
|
+
if (value.length > 0 && hasOwn(drivers, value)) return value;
|
|
31
|
+
|
|
32
|
+
if (envSelectedRaw.trim().length > 0) {
|
|
33
|
+
throw ErrorFactory.createConfigError(`Broadcast driver not configured: ${value}`);
|
|
31
34
|
}
|
|
32
35
|
|
|
33
|
-
// Backwards-compatible default.
|
|
34
36
|
return hasOwn(drivers, 'inmemory') ? 'inmemory' : (Object.keys(drivers)[0] ?? 'inmemory');
|
|
35
37
|
};
|
|
36
38
|
|
|
@@ -23,18 +23,30 @@ const getCacheDriver = (config: CacheConfigInput, name?: string): CacheDriverCon
|
|
|
23
23
|
throw ErrorFactory.createConfigError(`Cache store not configured: ${storeName}`);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
if (Object.keys(config.drivers ?? {}).length === 0) {
|
|
27
|
+
throw ErrorFactory.createConfigError('No cache stores are configured');
|
|
28
|
+
}
|
|
29
29
|
|
|
30
|
-
throw ErrorFactory.createConfigError(
|
|
30
|
+
throw ErrorFactory.createConfigError(
|
|
31
|
+
`Cache default store not configured: ${storeName || '<empty>'}`
|
|
32
|
+
);
|
|
31
33
|
};
|
|
32
34
|
|
|
33
35
|
const cacheConfigObj = {
|
|
34
36
|
/**
|
|
35
37
|
* Default cache driver
|
|
36
38
|
*/
|
|
37
|
-
default:
|
|
39
|
+
default: (() => {
|
|
40
|
+
const envConnection = Env.get('CACHE_CONNECTION', '').trim();
|
|
41
|
+
|
|
42
|
+
const envDriver =
|
|
43
|
+
typeof (Env as unknown as { CACHE_DRIVER?: unknown }).CACHE_DRIVER === 'string'
|
|
44
|
+
? String((Env as unknown as { CACHE_DRIVER?: unknown }).CACHE_DRIVER)
|
|
45
|
+
: Env.get('CACHE_DRIVER', 'memory');
|
|
46
|
+
|
|
47
|
+
const selected = envConnection.length > 0 ? envConnection : String(envDriver ?? 'memory');
|
|
48
|
+
return selected.trim().toLowerCase();
|
|
49
|
+
})(),
|
|
38
50
|
|
|
39
51
|
/**
|
|
40
52
|
* Cache drivers
|
|
@@ -13,13 +13,15 @@ const hasOwn = (obj: Record<string, unknown>, key: string): boolean => {
|
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
const getDefaultConnection = (connections: DatabaseConnections): string => {
|
|
16
|
-
const
|
|
16
|
+
const envSelectedRaw = Env.get('DB_CONNECTION', '');
|
|
17
|
+
const value = String(envSelectedRaw ?? '').trim();
|
|
17
18
|
|
|
18
|
-
if (value.length > 0 && hasOwn(connections, value))
|
|
19
|
-
|
|
19
|
+
if (value.length > 0 && hasOwn(connections, value)) return value;
|
|
20
|
+
|
|
21
|
+
if (envSelectedRaw.trim().length > 0) {
|
|
22
|
+
throw ErrorFactory.createConfigError(`Database connection not configured: ${value}`);
|
|
20
23
|
}
|
|
21
24
|
|
|
22
|
-
// Backwards-compatible default.
|
|
23
25
|
return hasOwn(connections, 'sqlite') ? 'sqlite' : (Object.keys(connections)[0] ?? 'sqlite');
|
|
24
26
|
};
|
|
25
27
|
|
|
@@ -4,28 +4,38 @@
|
|
|
4
4
|
* Sealed namespace for immutability
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { Env } from '
|
|
8
|
-
import type { MailConfigInput, MailDriverConfig } from '
|
|
7
|
+
import { Env } from './env';
|
|
8
|
+
import type { MailConfigInput, MailDriverConfig } from './type';
|
|
9
9
|
import { ErrorFactory } from '@zintrust/core';
|
|
10
10
|
|
|
11
|
+
const isMailDriverConfig = (value: unknown): value is MailDriverConfig => {
|
|
12
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
13
|
+
if (!('driver' in value)) return false;
|
|
14
|
+
|
|
15
|
+
const driver = (value as { driver?: unknown }).driver;
|
|
16
|
+
return typeof driver === 'string' && driver.trim().length > 0;
|
|
17
|
+
};
|
|
18
|
+
|
|
11
19
|
const getMailDriver = (config: MailConfigInput, name?: string): MailDriverConfig => {
|
|
12
|
-
const
|
|
20
|
+
const drivers = config.drivers as Record<string, unknown>;
|
|
21
|
+
const envSelectedRaw = Env.get('MAIL_CONNECTION', Env.get('MAIL_DRIVER', '')).trim();
|
|
22
|
+
const selected = (
|
|
23
|
+
name ??
|
|
24
|
+
(envSelectedRaw.length > 0 ? envSelectedRaw : undefined) ??
|
|
25
|
+
config.default
|
|
26
|
+
)
|
|
27
|
+
.toString()
|
|
28
|
+
.trim();
|
|
29
|
+
|
|
13
30
|
if (selected.length === 0) {
|
|
14
|
-
const disabled =
|
|
15
|
-
if (disabled
|
|
31
|
+
const disabled = drivers['disabled'];
|
|
32
|
+
if (isMailDriverConfig(disabled)) return disabled;
|
|
16
33
|
throw ErrorFactory.createConfigError('Mail driver not configured: disabled');
|
|
17
34
|
}
|
|
18
35
|
|
|
19
|
-
if (Object.hasOwn(
|
|
20
|
-
const resolved =
|
|
21
|
-
if (resolved
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Backward-compatible fallback: if the default is misconfigured, treat mail as disabled.
|
|
25
|
-
if (name === undefined) {
|
|
26
|
-
const disabled = config.drivers['disabled'];
|
|
27
|
-
if (disabled !== undefined) return disabled;
|
|
28
|
-
throw ErrorFactory.createConfigError('Mail driver not configured: disabled');
|
|
36
|
+
if (Object.hasOwn(drivers, selected)) {
|
|
37
|
+
const resolved = drivers[selected];
|
|
38
|
+
if (isMailDriverConfig(resolved)) return resolved;
|
|
29
39
|
}
|
|
30
40
|
|
|
31
41
|
throw ErrorFactory.createConfigError(`Mail driver not configured: ${selected}`);
|
|
@@ -35,7 +45,7 @@ const mailConfigObj = {
|
|
|
35
45
|
/**
|
|
36
46
|
* Default mail driver
|
|
37
47
|
*/
|
|
38
|
-
default: Env.get('MAIL_DRIVER', 'disabled').trim().toLowerCase()
|
|
48
|
+
default: Env.get('MAIL_CONNECTION', Env.get('MAIL_DRIVER', 'disabled')).trim().toLowerCase(),
|
|
39
49
|
|
|
40
50
|
/**
|
|
41
51
|
* Default "From" identity
|
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
CsrfMiddleware,
|
|
3
|
-
ErrorHandlerMiddleware,
|
|
4
|
-
LoggingMiddleware,
|
|
5
|
-
RateLimiter,
|
|
6
|
-
SecurityMiddleware,
|
|
7
|
-
type Middleware,
|
|
8
|
-
} from '@zintrust/core';
|
|
9
|
-
|
|
10
1
|
import { MiddlewareConfigType } from './type';
|
|
2
|
+
import { CsrfMiddleware } from '@zintrust/core';
|
|
3
|
+
import { ErrorHandlerMiddleware } from '@zintrust/core';
|
|
4
|
+
import { LoggingMiddleware } from '@zintrust/core';
|
|
5
|
+
import type { Middleware } from '@zintrust/core';
|
|
6
|
+
import { RateLimiter } from '@zintrust/core';
|
|
7
|
+
import { SecurityMiddleware } from '@zintrust/core';
|
|
11
8
|
|
|
12
9
|
const shared = Object.freeze({
|
|
13
10
|
log: LoggingMiddleware.create(),
|
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
* Driver selection must be dynamic (tests may mutate process.env).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { Env } from '
|
|
8
|
+
import { Env } from './env';
|
|
9
9
|
import type {
|
|
10
10
|
KnownNotificationDriverConfig,
|
|
11
11
|
NotificationConfigInput,
|
|
12
12
|
NotificationDrivers,
|
|
13
13
|
NotificationProviders,
|
|
14
|
-
} from '
|
|
14
|
+
} from './type';
|
|
15
15
|
import { ErrorFactory } from '@zintrust/core';
|
|
16
16
|
|
|
17
17
|
const normalizeName = (value: string): string => value.trim().toLowerCase();
|
|
@@ -21,8 +21,18 @@ const hasOwn = (obj: Record<string, unknown>, key: string): boolean => {
|
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
const getDefaultChannel = (drivers: NotificationDrivers): string => {
|
|
24
|
-
const
|
|
24
|
+
const envSelectedRaw = Env.get(
|
|
25
|
+
'NOTIFICATION_CONNECTION',
|
|
26
|
+
Env.get('NOTIFICATION_DRIVER', 'console')
|
|
27
|
+
);
|
|
28
|
+
const value = normalizeName(envSelectedRaw ?? 'console');
|
|
29
|
+
|
|
25
30
|
if (value.length > 0 && hasOwn(drivers, value)) return value;
|
|
31
|
+
|
|
32
|
+
if (envSelectedRaw.trim().length > 0) {
|
|
33
|
+
throw ErrorFactory.createConfigError(`Notification channel not configured: ${value}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
26
36
|
return hasOwn(drivers, 'console') ? 'console' : (Object.keys(drivers)[0] ?? 'console');
|
|
27
37
|
};
|
|
28
38
|
|
|
@@ -43,14 +53,16 @@ const getNotificationDriver = (
|
|
|
43
53
|
if (resolved !== undefined) return resolved;
|
|
44
54
|
}
|
|
45
55
|
|
|
56
|
+
if (Object.keys(config.drivers ?? {}).length === 0) {
|
|
57
|
+
throw ErrorFactory.createConfigError('No notification channels are configured');
|
|
58
|
+
}
|
|
59
|
+
|
|
46
60
|
if (isExplicitSelection) {
|
|
47
61
|
throw ErrorFactory.createConfigError(`Notification channel not configured: ${channelName}`);
|
|
48
62
|
}
|
|
49
63
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
throw ErrorFactory.createConfigError('No notification channels are configured');
|
|
64
|
+
// Default selection is strict: if `default` points at an unconfigured channel, throw.
|
|
65
|
+
throw ErrorFactory.createConfigError(`Notification channel not configured: ${channelName}`);
|
|
54
66
|
};
|
|
55
67
|
|
|
56
68
|
const getBaseProviders = (): NotificationProviders => {
|