@zintrust/core 0.1.15 → 0.1.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/public/index.html +1 -1
  4. package/src/cli/CLI.d.ts.map +1 -1
  5. package/src/cli/CLI.js +6 -0
  6. package/src/cli/commands/BroadcastWorkCommand.d.ts +10 -0
  7. package/src/cli/commands/BroadcastWorkCommand.d.ts.map +1 -0
  8. package/src/cli/commands/BroadcastWorkCommand.js +16 -0
  9. package/src/cli/commands/NotificationWorkCommand.d.ts +10 -0
  10. package/src/cli/commands/NotificationWorkCommand.d.ts.map +1 -0
  11. package/src/cli/commands/NotificationWorkCommand.js +16 -0
  12. package/src/cli/commands/QueueCommand.d.ts +10 -0
  13. package/src/cli/commands/QueueCommand.d.ts.map +1 -0
  14. package/src/cli/commands/QueueCommand.js +63 -0
  15. package/src/cli/commands/QueueWorkCommandUtils.d.ts +10 -0
  16. package/src/cli/commands/QueueWorkCommandUtils.d.ts.map +1 -0
  17. package/src/cli/commands/QueueWorkCommandUtils.js +43 -0
  18. package/src/cli/commands/createKindWorkCommand.d.ts +9 -0
  19. package/src/cli/commands/createKindWorkCommand.d.ts.map +1 -0
  20. package/src/cli/commands/createKindWorkCommand.js +33 -0
  21. package/src/cli/commands/index.d.ts +3 -0
  22. package/src/cli/commands/index.d.ts.map +1 -1
  23. package/src/cli/commands/index.js +3 -0
  24. package/src/cli/scaffolding/ModelGenerator.d.ts.map +1 -1
  25. package/src/cli/scaffolding/ModelGenerator.js +1 -0
  26. package/src/cli/scaffolding/ProjectScaffolder.d.ts.map +1 -1
  27. package/src/cli/scaffolding/ProjectScaffolder.js +2 -1
  28. package/src/cli/workers/QueueWorkRunner.d.ts +23 -0
  29. package/src/cli/workers/QueueWorkRunner.d.ts.map +1 -0
  30. package/src/cli/workers/QueueWorkRunner.js +142 -0
  31. package/src/collections/Collection.d.ts +30 -0
  32. package/src/collections/Collection.d.ts.map +1 -0
  33. package/src/collections/Collection.js +146 -0
  34. package/src/collections/index.d.ts +3 -0
  35. package/src/collections/index.d.ts.map +1 -0
  36. package/src/collections/index.js +1 -0
  37. package/src/config/env.d.ts +2 -0
  38. package/src/config/env.d.ts.map +1 -1
  39. package/src/config/env.js +4 -0
  40. package/src/config/index.d.ts +3 -0
  41. package/src/config/index.d.ts.map +1 -1
  42. package/src/config/security.d.ts +4 -1
  43. package/src/config/security.d.ts.map +1 -1
  44. package/src/config/security.js +9 -1
  45. package/src/events/EventDispatcher.d.ts +16 -0
  46. package/src/events/EventDispatcher.d.ts.map +1 -0
  47. package/src/events/EventDispatcher.js +90 -0
  48. package/src/events/index.d.ts +3 -0
  49. package/src/events/index.d.ts.map +1 -0
  50. package/src/events/index.js +1 -0
  51. package/src/features/Queue.d.ts +1 -1
  52. package/src/features/Queue.d.ts.map +1 -1
  53. package/src/features/Queue.js +2 -2
  54. package/src/http/Response.d.ts +2 -2
  55. package/src/http/Response.d.ts.map +1 -1
  56. package/src/index.d.ts +12 -0
  57. package/src/index.d.ts.map +1 -1
  58. package/src/index.js +12 -0
  59. package/src/middleware/CsrfMiddleware.d.ts.map +1 -1
  60. package/src/middleware/CsrfMiddleware.js +20 -25
  61. package/src/middleware/SessionMiddleware.d.ts +8 -0
  62. package/src/middleware/SessionMiddleware.d.ts.map +1 -0
  63. package/src/middleware/SessionMiddleware.js +15 -0
  64. package/src/node-singletons/crypto.d.ts +1 -1
  65. package/src/node-singletons/crypto.d.ts.map +1 -1
  66. package/src/node-singletons/crypto.js +1 -1
  67. package/src/orm/Model.d.ts +15 -0
  68. package/src/orm/Model.d.ts.map +1 -1
  69. package/src/orm/Model.js +57 -8
  70. package/src/orm/QueryBuilder.d.ts +9 -1
  71. package/src/orm/QueryBuilder.d.ts.map +1 -1
  72. package/src/orm/QueryBuilder.js +54 -2
  73. package/src/scripts/TemplateSync.js +23 -1
  74. package/src/security/EncryptedEnvelope.d.ts +77 -0
  75. package/src/security/EncryptedEnvelope.d.ts.map +1 -0
  76. package/src/security/EncryptedEnvelope.js +256 -0
  77. package/src/security/PasswordResetTokenBroker.d.ts +39 -0
  78. package/src/security/PasswordResetTokenBroker.d.ts.map +1 -0
  79. package/src/security/PasswordResetTokenBroker.js +131 -0
  80. package/src/security/StartupSecretValidation.d.ts.map +1 -1
  81. package/src/security/StartupSecretValidation.js +72 -0
  82. package/src/session/SessionManager.d.ts +39 -0
  83. package/src/session/SessionManager.d.ts.map +1 -0
  84. package/src/session/SessionManager.js +149 -0
  85. package/src/session/index.d.ts +3 -0
  86. package/src/session/index.d.ts.map +1 -0
  87. package/src/session/index.js +1 -0
  88. package/src/templates/features/Queue.ts.tpl +5 -4
  89. package/src/templates/project/basic/config/FileLogWriter.ts.tpl +4 -3
  90. package/src/templates/project/basic/config/SecretsManager.ts.tpl +1 -1
  91. package/src/templates/project/basic/config/broadcast.ts.tpl +2 -2
  92. package/src/templates/project/basic/config/cache.ts.tpl +2 -2
  93. package/src/templates/project/basic/config/database.ts.tpl +2 -2
  94. package/src/templates/project/basic/config/env.ts.tpl +5 -0
  95. package/src/templates/project/basic/config/features.ts.tpl +2 -2
  96. package/src/templates/project/basic/config/logger.ts.tpl +0 -2
  97. package/src/templates/project/basic/config/logging/HttpLogger.ts.tpl +1 -1
  98. package/src/templates/project/basic/config/logging/SlackLogger.ts.tpl +1 -1
  99. package/src/templates/project/basic/config/mail.ts.tpl +2 -2
  100. package/src/templates/project/basic/config/microservices.ts.tpl +1 -1
  101. package/src/templates/project/basic/config/middleware.ts.tpl +6 -9
  102. package/src/templates/project/basic/config/notification.ts.tpl +2 -2
  103. package/src/templates/project/basic/config/security.ts.tpl +12 -3
  104. package/src/templates/project/basic/config/storage.ts.tpl +2 -2
  105. package/src/templates/project/basic/config/type.ts.tpl +2 -2
  106. package/src/tools/broadcast/Broadcast.d.ts +8 -0
  107. package/src/tools/broadcast/Broadcast.d.ts.map +1 -1
  108. package/src/tools/broadcast/Broadcast.js +23 -0
  109. package/src/tools/notification/Notification.d.ts +10 -0
  110. package/src/tools/notification/Notification.d.ts.map +1 -1
  111. package/src/tools/notification/Notification.js +21 -0
  112. package/src/workers/BroadcastWorker.d.ts +22 -0
  113. package/src/workers/BroadcastWorker.d.ts.map +1 -0
  114. package/src/workers/BroadcastWorker.js +24 -0
  115. package/src/workers/NotificationWorker.d.ts +22 -0
  116. package/src/workers/NotificationWorker.d.ts.map +1 -0
  117. package/src/workers/NotificationWorker.js +23 -0
  118. package/src/workers/createQueueWorker.d.ts +24 -0
  119. package/src/workers/createQueueWorker.d.ts.map +1 -0
  120. package/src/workers/createQueueWorker.js +114 -0
@@ -0,0 +1,256 @@
1
+ /**
2
+ * EncryptedEnvelope
3
+ *
4
+ * Framework-compatible encrypted payload envelope (PHP-style envelope).
5
+ *
6
+ * Format: base64(JSON({ iv, value, mac, tag }))
7
+ * - iv: base64
8
+ * - value: base64 ciphertext
9
+ * - mac: hex string (AES-CBC envelopes)
10
+ * - tag: base64 auth tag (AES-GCM envelopes)
11
+ */
12
+ import { Env } from '../config/env.js';
13
+ import { ErrorFactory } from '../exceptions/ZintrustError.js';
14
+ import { createCipheriv, createDecipheriv, createHmac, randomBytes, timingSafeEqual, } from '../node-singletons/crypto.js';
15
+ const normalizeCipher = (cipher) => {
16
+ const normalized = cipher.toLowerCase();
17
+ if (normalized === 'aes-256-cbc')
18
+ return 'aes-256-cbc';
19
+ if (normalized === 'aes-256-gcm')
20
+ return 'aes-256-gcm';
21
+ throw ErrorFactory.createValidationError('Unsupported ENCRYPTION_CIPHER', {
22
+ cipher,
23
+ supported: ['aes-256-cbc', 'aes-256-gcm'],
24
+ });
25
+ };
26
+ const normalizeBase64ForCompare = (value) => {
27
+ const trimmed = value.trim();
28
+ // Remove base64 padding without regex (avoids any regex backtracking concerns).
29
+ let end = trimmed.length;
30
+ while (end > 0 && trimmed.codePointAt(end - 1) === 61) {
31
+ end -= 1;
32
+ }
33
+ return trimmed.slice(0, end);
34
+ };
35
+ const decodeBase64 = (input, label) => {
36
+ const trimmed = input.trim();
37
+ if (trimmed.length === 0) {
38
+ throw ErrorFactory.createValidationError(`Invalid base64 for ${label}`);
39
+ }
40
+ // Note: Buffer.from(..., 'base64') does not reliably throw on invalid input.
41
+ // Validate by re-encoding and comparing (ignoring padding).
42
+ const decoded = Buffer.from(trimmed, 'base64');
43
+ if (decoded.length === 0) {
44
+ throw ErrorFactory.createValidationError(`Invalid base64 for ${label}`);
45
+ }
46
+ const roundTrip = decoded.toString('base64');
47
+ if (normalizeBase64ForCompare(roundTrip) !== normalizeBase64ForCompare(trimmed)) {
48
+ throw ErrorFactory.createValidationError(`Invalid base64 for ${label}`);
49
+ }
50
+ return decoded;
51
+ };
52
+ const CIPHER_KEY_BYTES = Object.freeze({
53
+ 'aes-256-cbc': 32,
54
+ 'aes-256-gcm': 32,
55
+ });
56
+ const expectedKeyBytesForCipher = (cipher) => CIPHER_KEY_BYTES[cipher];
57
+ const parseKey = (key, cipher) => {
58
+ const trimmed = key.trim();
59
+ if (trimmed.length === 0) {
60
+ throw ErrorFactory.createValidationError('Missing APP_KEY');
61
+ }
62
+ const raw = trimmed.startsWith('base64:') ? trimmed.slice('base64:'.length) : trimmed;
63
+ const bytes = decodeBase64(raw, 'APP_KEY');
64
+ const expectedBytes = expectedKeyBytesForCipher(cipher);
65
+ if (bytes.length !== expectedBytes) {
66
+ throw ErrorFactory.createValidationError('Invalid APP_KEY length for cipher', {
67
+ cipher,
68
+ expectedBytes,
69
+ actualBytes: bytes.length,
70
+ });
71
+ }
72
+ return new Uint8Array(bytes);
73
+ };
74
+ const parsePreviousKeys = (raw, cipher) => {
75
+ const value = (raw ?? '').trim();
76
+ if (value.length === 0)
77
+ return [];
78
+ const items = (() => {
79
+ if (value.startsWith('[')) {
80
+ try {
81
+ const parsed = JSON.parse(value);
82
+ if (!Array.isArray(parsed))
83
+ return [];
84
+ return parsed.filter((v) => typeof v === 'string');
85
+ }
86
+ catch {
87
+ return [];
88
+ }
89
+ }
90
+ return value
91
+ .split(',')
92
+ .map((s) => s.trim())
93
+ .filter(Boolean);
94
+ })();
95
+ return items.map((k) => parseKey(k, cipher));
96
+ };
97
+ const parsePayload = (payload) => {
98
+ const decoded = Buffer.from(payload, 'base64').toString('utf8');
99
+ let parsed;
100
+ try {
101
+ parsed = JSON.parse(decoded);
102
+ }
103
+ catch {
104
+ throw ErrorFactory.createValidationError('Invalid encrypted envelope payload (not JSON)');
105
+ }
106
+ if (typeof parsed !== 'object' || parsed === null) {
107
+ throw ErrorFactory.createValidationError('Invalid encrypted envelope payload (not an object)');
108
+ }
109
+ const record = parsed;
110
+ const iv = typeof record['iv'] === 'string' ? record['iv'] : '';
111
+ const value = typeof record['value'] === 'string' ? record['value'] : '';
112
+ const mac = typeof record['mac'] === 'string' ? record['mac'] : undefined;
113
+ const tag = typeof record['tag'] === 'string' ? record['tag'] : undefined;
114
+ if (iv.length === 0 || value.length === 0) {
115
+ throw ErrorFactory.createValidationError('Invalid encrypted envelope payload (missing iv/value)');
116
+ }
117
+ return { iv, value, mac, tag };
118
+ };
119
+ const computeMacHex = (key, ivBase64, valueBase64) => {
120
+ // Envelope MAC: mac = HMAC-SHA256(iv + value, key), where iv/value are base64 strings.
121
+ return createHmac('sha256', Buffer.from(key))
122
+ .update(ivBase64 + valueBase64, 'utf8')
123
+ .digest('hex');
124
+ };
125
+ const timingSafeEqualsHex = (a, b) => {
126
+ const aBuf = Buffer.from(a, 'utf8');
127
+ const bBuf = Buffer.from(b, 'utf8');
128
+ if (aBuf.length !== bBuf.length)
129
+ return false;
130
+ return timingSafeEqual(aBuf, bBuf);
131
+ };
132
+ const ivLengthFor = (cipher) => {
133
+ // OpenSSL defaults for these ciphers.
134
+ if (cipher === 'aes-256-gcm')
135
+ return 12;
136
+ return 16;
137
+ };
138
+ const decryptWithKey = (payload, cipher, key) => {
139
+ const iv = decodeBase64(payload.iv, 'iv');
140
+ const ciphertext = decodeBase64(payload.value, 'value');
141
+ if (cipher === 'aes-256-cbc') {
142
+ const expected = payload.mac ?? '';
143
+ if (expected.length === 0) {
144
+ throw ErrorFactory.createValidationError('Missing mac for aes-256-cbc envelope');
145
+ }
146
+ const actual = computeMacHex(key, payload.iv, payload.value);
147
+ if (!timingSafeEqualsHex(actual, expected)) {
148
+ throw ErrorFactory.createSecurityError('Invalid MAC');
149
+ }
150
+ const decipher = createDecipheriv('aes-256-cbc', Buffer.from(key), iv);
151
+ const plain = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
152
+ return plain.toString('utf8');
153
+ }
154
+ const tagB64 = (payload.tag ?? '').trim();
155
+ if (tagB64.length === 0) {
156
+ throw ErrorFactory.createValidationError('Missing tag for aes-256-gcm envelope');
157
+ }
158
+ const tag = decodeBase64(tagB64, 'tag');
159
+ const decipher = createDecipheriv('aes-256-gcm', Buffer.from(key), iv);
160
+ decipher.setAuthTag(tag);
161
+ const plain = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
162
+ return plain.toString('utf8');
163
+ };
164
+ export const EncryptedEnvelope = Object.freeze({
165
+ normalizeCipher,
166
+ /**
167
+ * Build a keyring from environment variables.
168
+ * - Uses APP_KEY
169
+ * - Supports APP_PREVIOUS_KEYS (comma-separated or JSON array)
170
+ */
171
+ keyringFromEnv(env = Env) {
172
+ const cipherRaw = (env.ENCRYPTION_CIPHER ?? '').trim();
173
+ if (cipherRaw.length === 0) {
174
+ throw ErrorFactory.createConfigError('ENCRYPTION_CIPHER must be set', {
175
+ key: 'ENCRYPTION_CIPHER',
176
+ });
177
+ }
178
+ const cipher = normalizeCipher(cipherRaw);
179
+ const primaryKey = parseKey(env.APP_KEY ?? '', cipher);
180
+ const previousKeys = parsePreviousKeys(env.APP_PREVIOUS_KEYS, cipher);
181
+ return { primaryKey, previousKeys };
182
+ },
183
+ /**
184
+ * Encrypt a UTF-8 string and return a framework-compatible base64(JSON) envelope.
185
+ */
186
+ encryptString(plaintext, options) {
187
+ const cipher = normalizeCipher(options.cipher);
188
+ const key = parseKey(options.key, cipher);
189
+ const iv = randomBytes(ivLengthFor(cipher));
190
+ if (cipher === 'aes-256-cbc') {
191
+ const c = createCipheriv('aes-256-cbc', Buffer.from(key), iv);
192
+ const ciphertext = Buffer.concat([c.update(Buffer.from(plaintext, 'utf8')), c.final()]);
193
+ const ivB64 = iv.toString('base64');
194
+ const valueB64 = ciphertext.toString('base64');
195
+ const mac = computeMacHex(key, ivB64, valueB64);
196
+ const envelope = { iv: ivB64, value: valueB64, mac, tag: '' };
197
+ return Buffer.from(JSON.stringify(envelope), 'utf8').toString('base64');
198
+ }
199
+ const c = createCipheriv('aes-256-gcm', Buffer.from(key), iv);
200
+ const ciphertext = Buffer.concat([c.update(Buffer.from(plaintext, 'utf8')), c.final()]);
201
+ const tag = c.getAuthTag();
202
+ const envelope = {
203
+ iv: iv.toString('base64'),
204
+ value: ciphertext.toString('base64'),
205
+ mac: '',
206
+ tag: tag.toString('base64'),
207
+ };
208
+ return Buffer.from(JSON.stringify(envelope), 'utf8').toString('base64');
209
+ },
210
+ /**
211
+ * Decrypt a framework-compatible base64(JSON) envelope to a UTF-8 string.
212
+ * Tries the primary key first, then previous keys.
213
+ */
214
+ decryptString(encrypted, options) {
215
+ const cipher = normalizeCipher(options.cipher);
216
+ const primaryKey = parseKey(options.key, cipher);
217
+ const previous = (options.previousKeys ?? []).map((k) => parseKey(k, cipher));
218
+ const payload = parsePayload(encrypted);
219
+ const keys = [primaryKey, ...previous];
220
+ let lastError;
221
+ for (const key of keys) {
222
+ try {
223
+ return decryptWithKey(payload, cipher, key);
224
+ }
225
+ catch (error) {
226
+ lastError = error;
227
+ }
228
+ }
229
+ throw ErrorFactory.createSecurityError('Unable to decrypt encrypted envelope with provided keyring', {
230
+ cause: lastError instanceof Error ? lastError.message : String(lastError),
231
+ });
232
+ },
233
+ /**
234
+ * Encrypt arbitrary values using a caller-provided serializer.
235
+ * This supports encrypted payloads for frameworks that store serialized values.
236
+ */
237
+ encrypt(value, options) {
238
+ const serialized = options.serializer.serialize(value);
239
+ return EncryptedEnvelope.encryptString(serialized, {
240
+ cipher: options.cipher,
241
+ key: options.key,
242
+ });
243
+ },
244
+ /**
245
+ * Decrypt into an arbitrary value using a caller-provided serializer.
246
+ */
247
+ decrypt(encrypted, options) {
248
+ const serialized = EncryptedEnvelope.decryptString(encrypted, {
249
+ cipher: options.cipher,
250
+ key: options.key,
251
+ previousKeys: options.previousKeys,
252
+ });
253
+ return options.serializer.deserialize(serialized);
254
+ },
255
+ });
256
+ export default EncryptedEnvelope;
@@ -0,0 +1,39 @@
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
+ export interface PasswordResetTokenRecord {
11
+ identifier: string;
12
+ tokenHash: string;
13
+ createdAt: Date;
14
+ expiresAt: Date;
15
+ }
16
+ export interface IPasswordResetTokenStore {
17
+ set(record: PasswordResetTokenRecord): void | Promise<void>;
18
+ get(identifier: string): PasswordResetTokenRecord | null | Promise<PasswordResetTokenRecord | null>;
19
+ delete(identifier: string): void | Promise<void>;
20
+ cleanup?(now?: Date): number | Promise<number>;
21
+ clear?(): void | Promise<void>;
22
+ }
23
+ export interface IPasswordResetTokenBroker {
24
+ createToken(identifier: string): Promise<string>;
25
+ verifyToken(identifier: string, token: string): Promise<boolean>;
26
+ consumeToken(identifier: string, token: string): Promise<boolean>;
27
+ }
28
+ export interface PasswordResetTokenBrokerOptions {
29
+ store?: IPasswordResetTokenStore;
30
+ ttlMs?: number;
31
+ tokenBytes?: number;
32
+ now?: () => Date;
33
+ }
34
+ export interface PasswordResetTokenBrokerType {
35
+ create(options?: PasswordResetTokenBrokerOptions): IPasswordResetTokenBroker;
36
+ createInMemoryStore(): IPasswordResetTokenStore;
37
+ }
38
+ export declare const PasswordResetTokenBroker: PasswordResetTokenBrokerType;
39
+ //# sourceMappingURL=PasswordResetTokenBroker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PasswordResetTokenBroker.d.ts","sourceRoot":"","sources":["../../../src/security/PasswordResetTokenBroker.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,MAAM,WAAW,wBAAwB;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,wBAAwB;IACvC,GAAG,CAAC,MAAM,EAAE,wBAAwB,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,GAAG,CACD,UAAU,EAAE,MAAM,GACjB,wBAAwB,GAAG,IAAI,GAAG,OAAO,CAAC,wBAAwB,GAAG,IAAI,CAAC,CAAC;IAC9E,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjD,OAAO,CAAC,CAAC,GAAG,CAAC,EAAE,IAAI,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/C,KAAK,CAAC,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,yBAAyB;IACxC,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACjD,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACjE,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACnE;AAED,MAAM,WAAW,+BAA+B;IAC9C,KAAK,CAAC,EAAE,wBAAwB,CAAC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,MAAM,WAAW,4BAA4B;IAC3C,MAAM,CAAC,OAAO,CAAC,EAAE,+BAA+B,GAAG,yBAAyB,CAAC;IAC7E,mBAAmB,IAAI,wBAAwB,CAAC;CACjD;AAmFD,eAAO,MAAM,wBAAwB,EAAE,4BAGrC,CAAC"}
@@ -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
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"StartupSecretValidation.d.ts","sourceRoot":"","sources":["../../../src/security/StartupSecretValidation.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAOH,MAAM,MAAM,4BAA4B,GAAG;IACzC,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,6BAA6B,GAAG;IAC1C,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,4BAA4B,EAAE,CAAC;CACxC,CAAC;AA6BF,eAAO,MAAM,uBAAuB;gBACtB,6BAA6B;mBAe1B,IAAI;EAQnB,CAAC;AAEH,eAAe,uBAAuB,CAAC"}
1
+ {"version":3,"file":"StartupSecretValidation.d.ts","sourceRoot":"","sources":["../../../src/security/StartupSecretValidation.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAQH,MAAM,MAAM,4BAA4B,GAAG;IACzC,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,6BAA6B,GAAG;IAC1C,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,4BAA4B,EAAE,CAAC;CACxC,CAAC;AA2GF,eAAO,MAAM,uBAAuB;gBACtB,6BAA6B;mBAiB1B,IAAI;EAQnB,CAAC;AAEH,eAAe,uBAAuB,CAAC"}
@@ -5,6 +5,7 @@
5
5
  * fails fast and predictably.
6
6
  */
7
7
  import { appConfig } from '../config/app.js';
8
+ import { Env } from '../config/env.js';
8
9
  import { securityConfig } from '../config/security.js';
9
10
  import { startupConfig } from '../config/startup.js';
10
11
  import { ErrorFactory } from '../exceptions/ZintrustError.js';
@@ -34,6 +35,76 @@ const validateJwtSecret = () => {
34
35
  return { key: 'JWT_SECRET', message };
35
36
  }
36
37
  };
38
+ const normalizeCipher = (raw) => {
39
+ const value = raw.trim().toLowerCase();
40
+ if (value === 'aes-256-cbc')
41
+ return 'aes-256-cbc';
42
+ if (value === 'aes-256-gcm')
43
+ return 'aes-256-gcm';
44
+ return null;
45
+ };
46
+ const parseBase64KeyBytes = (rawKey) => {
47
+ const base64 = rawKey.startsWith('base64:') ? rawKey.slice('base64:'.length) : rawKey;
48
+ const decoded = Buffer.from(base64, 'base64');
49
+ if (decoded.length === 0) {
50
+ return null;
51
+ }
52
+ return decoded.length;
53
+ };
54
+ const validateEncryptionInterop = () => {
55
+ const errors = [];
56
+ const cipherRaw = (Env.ENCRYPTION_CIPHER ?? '').trim();
57
+ if (cipherRaw.length === 0) {
58
+ errors.push({
59
+ key: 'ENCRYPTION_CIPHER',
60
+ message: 'ENCRYPTION_CIPHER must be set (supported: aes-256-cbc, aes-256-gcm)',
61
+ });
62
+ return errors;
63
+ }
64
+ const cipher = normalizeCipher(cipherRaw);
65
+ if (cipher === null) {
66
+ errors.push({
67
+ key: 'ENCRYPTION_CIPHER',
68
+ message: 'Unsupported ENCRYPTION_CIPHER (supported: aes-256-cbc, aes-256-gcm)',
69
+ });
70
+ }
71
+ const appKey = (Env.APP_KEY ?? '').trim();
72
+ if (appKey.length === 0) {
73
+ errors.push({ key: 'APP_KEY', message: 'APP_KEY must be set for encryption interoperability' });
74
+ return errors;
75
+ }
76
+ const bytes = parseBase64KeyBytes(appKey);
77
+ if (bytes === null) {
78
+ errors.push({
79
+ key: 'APP_KEY',
80
+ message: 'APP_KEY must be valid base64 (supports base64:... prefix)',
81
+ });
82
+ return errors;
83
+ }
84
+ // Current supported ciphers are aes-256-*, so require 32-byte keys.
85
+ if (bytes !== 32) {
86
+ errors.push({
87
+ key: 'APP_KEY',
88
+ message: `APP_KEY must decode to 32 bytes for ${cipherRaw}`,
89
+ });
90
+ }
91
+ const prev = (Env.APP_PREVIOUS_KEYS ?? '').trim();
92
+ if (prev.length > 0 && prev.startsWith('[')) {
93
+ try {
94
+ const parsed = JSON.parse(prev);
95
+ if (!Array.isArray(parsed) || !parsed.every((v) => typeof v === 'string')) {
96
+ errors.push({
97
+ key: 'APP_PREVIOUS_KEYS',
98
+ message: 'APP_PREVIOUS_KEYS JSON must be an array of strings',
99
+ });
100
+ }
101
+ }
102
+ catch {
103
+ errors.push({ key: 'APP_PREVIOUS_KEYS', message: 'APP_PREVIOUS_KEYS must be valid JSON' });
104
+ }
105
+ }
106
+ return errors;
107
+ };
37
108
  export const StartupSecretValidation = Object.freeze({
38
109
  validate() {
39
110
  if (!startupConfig.validateSecrets)
@@ -47,6 +118,7 @@ export const StartupSecretValidation = Object.freeze({
47
118
  const apiKeyError = validateApiKeySecret();
48
119
  if (apiKeyError !== null)
49
120
  errors.push(apiKeyError);
121
+ errors.push(...validateEncryptionInterop());
50
122
  return { valid: errors.length === 0, errors };
51
123
  },
52
124
  assertValid() {
@@ -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"}