@zintrust/core 0.1.16 → 0.1.18
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 +2 -2
- package/package.json +1 -1
- package/public/index.html +1 -1
- package/src/cli/PromptHelper.js +1 -1
- package/src/cli/commands/NewCommand.js +1 -1
- package/src/cli/config/ConfigSchema.js +1 -1
- package/src/cli/scaffolding/ProjectScaffolder.d.ts.map +1 -1
- package/src/cli/scaffolding/ProjectScaffolder.js +90 -22
- package/src/config/env.d.ts +2 -0
- package/src/config/env.d.ts.map +1 -1
- package/src/config/env.js +4 -0
- package/src/config/index.d.ts +3 -0
- package/src/config/index.d.ts.map +1 -1
- package/src/config/security.d.ts +4 -1
- package/src/config/security.d.ts.map +1 -1
- package/src/config/security.js +9 -1
- package/src/features/Queue.d.ts +1 -1
- package/src/features/Queue.d.ts.map +1 -1
- package/src/features/Queue.js +1 -1
- package/src/index.d.ts +1 -0
- package/src/index.d.ts.map +1 -1
- package/src/index.js +1 -0
- package/src/middleware/RateLimiter.d.ts +35 -0
- package/src/middleware/RateLimiter.d.ts.map +1 -1
- package/src/middleware/RateLimiter.js +187 -15
- package/src/node-singletons/crypto.d.ts +1 -1
- package/src/node-singletons/crypto.d.ts.map +1 -1
- package/src/node-singletons/crypto.js +1 -1
- package/src/security/EncryptedEnvelope.d.ts +77 -0
- package/src/security/EncryptedEnvelope.d.ts.map +1 -0
- package/src/security/EncryptedEnvelope.js +256 -0
- package/src/security/StartupSecretValidation.d.ts.map +1 -1
- package/src/security/StartupSecretValidation.js +72 -0
- package/src/templates/features/Queue.ts.tpl +1 -1
- package/src/templates/project/basic/config/env.ts.tpl +5 -0
- package/src/templates/project/basic/config/security.ts.tpl +11 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ cd my-app
|
|
|
16
16
|
zin add db:sqlite
|
|
17
17
|
|
|
18
18
|
# Start development
|
|
19
|
-
|
|
19
|
+
zin start
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
Your API is now running at `http://localhost:7777`
|
|
@@ -169,7 +169,7 @@ export function registerRoutes(app: Application): void {
|
|
|
169
169
|
### 3. Run Your API
|
|
170
170
|
|
|
171
171
|
```bash
|
|
172
|
-
|
|
172
|
+
zin start
|
|
173
173
|
```
|
|
174
174
|
|
|
175
175
|
Test it:
|
package/package.json
CHANGED
package/public/index.html
CHANGED
package/src/cli/PromptHelper.js
CHANGED
|
@@ -54,7 +54,7 @@ const getProjectDefaults = (name, options) => {
|
|
|
54
54
|
const database = getStringOption(options, 'database', 'sqlite');
|
|
55
55
|
const portRaw = getStringOption(options, 'port', '7777');
|
|
56
56
|
const portParsed = Number.parseInt(portRaw, 10);
|
|
57
|
-
const port = Number.isFinite(portParsed) && portParsed > 0 ? portParsed :
|
|
57
|
+
const port = Number.isFinite(portParsed) && portParsed > 0 ? portParsed : 7777;
|
|
58
58
|
const author = getStringOption(options, 'author', '');
|
|
59
59
|
const description = getStringOption(options, 'description', `A new Zintrust project: ${name}`);
|
|
60
60
|
const interactive = getBooleanOption(options, 'interactive', true);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProjectScaffolder.d.ts","sourceRoot":"","sources":["../../../../src/cli/scaffolding/ProjectScaffolder.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAQH,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,cAAc,GAAG,sBAAsB,CAAC;AAEpD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,KAAK,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,cAAc,CAAC,OAAO,EAAE,sBAAsB,GAAG,IAAI,CAAC;IACtD,YAAY,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,eAAe,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAAC;IACpE,cAAc,IAAI,MAAM,CAAC;IACzB,sBAAsB,IAAI,OAAO,CAAC;IAClC,iBAAiB,IAAI,MAAM,CAAC;IAC5B,WAAW,CAAC,OAAO,CAAC,EAAE,sBAAsB,GAAG,MAAM,CAAC;IACtD,gBAAgB,IAAI,OAAO,CAAC;IAC5B,aAAa,IAAI,OAAO,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAAC;CAC3E;
|
|
1
|
+
{"version":3,"file":"ProjectScaffolder.d.ts","sourceRoot":"","sources":["../../../../src/cli/scaffolding/ProjectScaffolder.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAQH,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,cAAc,GAAG,sBAAsB,CAAC;AAEpD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,KAAK,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,cAAc,CAAC,OAAO,EAAE,sBAAsB,GAAG,IAAI,CAAC;IACtD,YAAY,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,eAAe,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAAC;IACpE,cAAc,IAAI,MAAM,CAAC;IACzB,sBAAsB,IAAI,OAAO,CAAC;IAClC,iBAAiB,IAAI,MAAM,CAAC;IAC5B,WAAW,CAAC,OAAO,CAAC,EAAE,sBAAsB,GAAG,MAAM,CAAC;IACtD,gBAAgB,IAAI,OAAO,CAAC;IAC5B,aAAa,IAAI,OAAO,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAAC;CAC3E;AAsfD,wBAAgB,qBAAqB,IAAI,MAAM,EAAE,CAEhD;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAsBrE;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG;IAChE,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB,CAsBA;AA0ID;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,GAAE,MAAsB,GAAG,kBAAkB,CAsB/F;AAED,wBAAsB,eAAe,CACnC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,qBAAqB,CAAC,CAEhC;AAED;;GAEG;AACH,eAAO,MAAM,iBAAiB;;;;;;EAM5B,CAAC"}
|
|
@@ -87,7 +87,7 @@ const createProjectConfigFile = (projectPath, variables) => {
|
|
|
87
87
|
connection: variables['database'] ?? 'sqlite',
|
|
88
88
|
},
|
|
89
89
|
server: {
|
|
90
|
-
port: variables['port'] ??
|
|
90
|
+
port: variables['port'] ?? 7777,
|
|
91
91
|
},
|
|
92
92
|
};
|
|
93
93
|
fs.writeFileSync(fullPath, JSON.stringify(config, null, 2));
|
|
@@ -97,18 +97,96 @@ const createProjectConfigFile = (projectPath, variables) => {
|
|
|
97
97
|
return false;
|
|
98
98
|
}
|
|
99
99
|
};
|
|
100
|
+
const stripEnvInlineComment = (value) => {
|
|
101
|
+
let inSingle = false;
|
|
102
|
+
let inDouble = false;
|
|
103
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
104
|
+
const ch = value[i];
|
|
105
|
+
if (ch === "'" && !inDouble)
|
|
106
|
+
inSingle = !inSingle;
|
|
107
|
+
if (ch === '"' && !inSingle)
|
|
108
|
+
inDouble = !inDouble;
|
|
109
|
+
if (!inSingle && !inDouble && ch === '#') {
|
|
110
|
+
const prev = value[i - 1];
|
|
111
|
+
if (prev === undefined || prev === ' ' || prev === '\t') {
|
|
112
|
+
return value.slice(0, i).trimEnd();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return value;
|
|
117
|
+
};
|
|
118
|
+
const backfillEnvDefaults = (envPath, defaults) => {
|
|
119
|
+
const raw = fs.readFileSync(envPath, 'utf8');
|
|
120
|
+
const lines = raw.split(/\r?\n/);
|
|
121
|
+
const seen = new Set();
|
|
122
|
+
const filled = new Set();
|
|
123
|
+
const out = lines.map((line) => {
|
|
124
|
+
const trimmed = line.trim();
|
|
125
|
+
if (trimmed === '' || trimmed.startsWith('#'))
|
|
126
|
+
return line;
|
|
127
|
+
const withoutExport = trimmed.startsWith('export ') ? trimmed.slice('export '.length) : trimmed;
|
|
128
|
+
const eq = withoutExport.indexOf('=');
|
|
129
|
+
if (eq <= 0)
|
|
130
|
+
return line;
|
|
131
|
+
const key = withoutExport.slice(0, eq).trim();
|
|
132
|
+
if (key === '')
|
|
133
|
+
return line;
|
|
134
|
+
if (!Object.hasOwn(defaults, key))
|
|
135
|
+
return line;
|
|
136
|
+
if (seen.has(key))
|
|
137
|
+
return line;
|
|
138
|
+
seen.add(key);
|
|
139
|
+
const rhs = withoutExport.slice(eq + 1);
|
|
140
|
+
const withoutComment = stripEnvInlineComment(rhs);
|
|
141
|
+
const value = withoutComment.trim();
|
|
142
|
+
if (value !== '')
|
|
143
|
+
return line;
|
|
144
|
+
filled.add(key);
|
|
145
|
+
return `${key}=${defaults[key]}`;
|
|
146
|
+
});
|
|
147
|
+
const missingKeys = Object.keys(defaults).filter((k) => !seen.has(k));
|
|
148
|
+
if (missingKeys.length > 0) {
|
|
149
|
+
out.push(...missingKeys.map((k) => `${k}=${defaults[k]}`));
|
|
150
|
+
}
|
|
151
|
+
// Avoid rewriting if nothing changed.
|
|
152
|
+
if (filled.size === 0 && missingKeys.length === 0)
|
|
153
|
+
return;
|
|
154
|
+
fs.writeFileSync(envPath, out.join('\n') + (out.at(-1) === '' ? '' : '\n'));
|
|
155
|
+
};
|
|
156
|
+
const buildDatabaseEnvLines = (database) => {
|
|
157
|
+
if (database === 'postgresql' || database === 'postgres') {
|
|
158
|
+
return [
|
|
159
|
+
'DB_HOST=localhost',
|
|
160
|
+
'DB_PORT=5432',
|
|
161
|
+
'DB_DATABASE=zintrust',
|
|
162
|
+
'DB_USERNAME=postgres',
|
|
163
|
+
'DB_PASSWORD=',
|
|
164
|
+
];
|
|
165
|
+
}
|
|
166
|
+
if (database === 'sqlite') {
|
|
167
|
+
// Provide both DB_DATABASE (used by the framework) and DB_PATH (common alias)
|
|
168
|
+
return ['DB_DATABASE=./database.sqlite', 'DB_PATH=./database.sqlite'];
|
|
169
|
+
}
|
|
170
|
+
return [];
|
|
171
|
+
};
|
|
100
172
|
const createEnvFile = (projectPath, variables) => {
|
|
101
173
|
try {
|
|
102
174
|
if (!fs.existsSync(projectPath)) {
|
|
103
175
|
fs.mkdirSync(projectPath, { recursive: true });
|
|
104
176
|
}
|
|
105
177
|
const fullPath = path.join(projectPath, '.env');
|
|
106
|
-
// If
|
|
178
|
+
// If an .env already exists (e.g., from a template), do not overwrite user values.
|
|
179
|
+
// But we *do* backfill safe defaults for common bootstrap keys when missing/blank.
|
|
107
180
|
if (fs.existsSync(fullPath)) {
|
|
181
|
+
backfillEnvDefaults(fullPath, {
|
|
182
|
+
HOST: 'localhost',
|
|
183
|
+
PORT: String(Number(variables['port'] ?? 7777)),
|
|
184
|
+
LOG_LEVEL: 'debug',
|
|
185
|
+
});
|
|
108
186
|
return true;
|
|
109
187
|
}
|
|
110
188
|
const name = typeof variables['projectName'] === 'string' ? variables['projectName'] : 'zintrust-app';
|
|
111
|
-
const port = Number(variables['port'] ??
|
|
189
|
+
const port = Number(variables['port'] ?? 7777);
|
|
112
190
|
const database = typeof variables['database'] === 'string' ? variables['database'] : 'sqlite';
|
|
113
191
|
// Generate a secure APP_KEY (32 bytes = 256-bit, base64 encoded)
|
|
114
192
|
const appKeyBytes = randomBytes(32);
|
|
@@ -125,22 +203,7 @@ const createEnvFile = (projectPath, variables) => {
|
|
|
125
203
|
`APP_KEY=${appKey}`,
|
|
126
204
|
`DB_CONNECTION=${database}`,
|
|
127
205
|
];
|
|
128
|
-
const dbLines = (
|
|
129
|
-
if (database === 'postgresql' || database === 'postgres') {
|
|
130
|
-
return [
|
|
131
|
-
'DB_HOST=localhost',
|
|
132
|
-
'DB_PORT=5432',
|
|
133
|
-
'DB_DATABASE=zintrust',
|
|
134
|
-
'DB_USERNAME=postgres',
|
|
135
|
-
'DB_PASSWORD=',
|
|
136
|
-
];
|
|
137
|
-
}
|
|
138
|
-
if (database === 'sqlite') {
|
|
139
|
-
// Provide both DB_DATABASE (used by the framework) and DB_PATH (common alias)
|
|
140
|
-
return ['DB_DATABASE=./database.sqlite', 'DB_PATH=./database.sqlite'];
|
|
141
|
-
}
|
|
142
|
-
return [];
|
|
143
|
-
})();
|
|
206
|
+
const dbLines = buildDatabaseEnvLines(database);
|
|
144
207
|
const placeholderLines = [
|
|
145
208
|
'',
|
|
146
209
|
'# Logging',
|
|
@@ -152,7 +215,8 @@ const createEnvFile = (projectPath, variables) => {
|
|
|
152
215
|
'JWT_SECRET=',
|
|
153
216
|
'JWT_EXPIRES_IN=1h',
|
|
154
217
|
'CSRF_SECRET=',
|
|
155
|
-
'
|
|
218
|
+
'ENCRYPTION_CIPHER=aes-256-cbc',
|
|
219
|
+
'APP_PREVIOUS_KEYS=',
|
|
156
220
|
'',
|
|
157
221
|
'# Cache / Queue',
|
|
158
222
|
'CACHE_DRIVER=memory',
|
|
@@ -244,11 +308,15 @@ const loadTemplateFiles = (templateDir) => {
|
|
|
244
308
|
if (relPath === 'template.json')
|
|
245
309
|
return false;
|
|
246
310
|
const normalized = normalizeRelPath(relPath);
|
|
311
|
+
// Project `.env` is generated by createEnvFile() so it can set defaults and create a secure APP_KEY.
|
|
312
|
+
// Some templates ship `.env.tpl` (which would become `.env`), but that file is intentionally ignored.
|
|
313
|
+
const outputRel = normalizeRelPath(getOutputRelPath(relPath));
|
|
314
|
+
if (outputRel === '.env')
|
|
315
|
+
return false;
|
|
247
316
|
if (!normalized.startsWith('config/'))
|
|
248
317
|
return true;
|
|
249
318
|
// Starter apps should only ship app-level config modules.
|
|
250
319
|
// Core/framework config internals (e.g. config/logging/*) remain core-owned.
|
|
251
|
-
const outputRel = normalizeRelPath(getOutputRelPath(relPath));
|
|
252
320
|
return allowedConfigFiles.has(outputRel);
|
|
253
321
|
};
|
|
254
322
|
const readUtf8FileOrUndefined = (absPath) => {
|
|
@@ -423,7 +491,7 @@ const prepareContext = (state, options) => {
|
|
|
423
491
|
projectSlug: options.name,
|
|
424
492
|
author: options.author ?? 'Your Name',
|
|
425
493
|
description: options.description ?? '',
|
|
426
|
-
port: options.port ??
|
|
494
|
+
port: options.port ?? 7777,
|
|
427
495
|
database: options.database ?? 'sqlite',
|
|
428
496
|
template: state.templateName,
|
|
429
497
|
migrationTimestamp,
|
package/src/config/env.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export declare const Env: Readonly<{
|
|
|
14
14
|
HOST: string;
|
|
15
15
|
APP_NAME: string;
|
|
16
16
|
APP_KEY: string;
|
|
17
|
+
APP_PREVIOUS_KEYS: string;
|
|
17
18
|
DB_CONNECTION: string;
|
|
18
19
|
DB_HOST: string;
|
|
19
20
|
DB_PORT: number;
|
|
@@ -55,6 +56,7 @@ export declare const Env: Readonly<{
|
|
|
55
56
|
ENABLE_MICROSERVICES: boolean;
|
|
56
57
|
TOKEN_TTL: number;
|
|
57
58
|
TOKEN_LENGTH: number;
|
|
59
|
+
ENCRYPTION_CIPHER: string;
|
|
58
60
|
ENVIRONMENT: string;
|
|
59
61
|
REQUEST_TIMEOUT: number;
|
|
60
62
|
MAX_BODY_SIZE: number;
|
package/src/config/env.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../../src/config/env.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AA+CH,eAAO,MAAM,GAAG;eA/BE,MAAM,iBAAiB,MAAM,KAAG,MAAM;kBAMnC,MAAM,iBAAiB,MAAM,KAAG,MAAM;mBASrC,MAAM,iBAAiB,OAAO,KAAG,OAAO
|
|
1
|
+
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../../src/config/env.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AA+CH,eAAO,MAAM,GAAG;eA/BE,MAAM,iBAAiB,MAAM,KAAG,MAAM;kBAMnC,MAAM,iBAAiB,MAAM,KAAG,MAAM;mBASrC,MAAM,iBAAiB,OAAO,KAAG,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;eAqGP,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO;;;;;;;;;EAkCxF,CAAC"}
|
package/src/config/env.js
CHANGED
|
@@ -60,6 +60,8 @@ export const Env = Object.freeze({
|
|
|
60
60
|
HOST: get('HOST', 'localhost'),
|
|
61
61
|
APP_NAME: get('APP_NAME', 'ZinTrust'),
|
|
62
62
|
APP_KEY: get('APP_KEY', ''),
|
|
63
|
+
// Optional key rotation support (comma-separated or JSON array of keys)
|
|
64
|
+
APP_PREVIOUS_KEYS: get('APP_PREVIOUS_KEYS', ''),
|
|
63
65
|
// Database
|
|
64
66
|
DB_CONNECTION: get('DB_CONNECTION', 'sqlite'),
|
|
65
67
|
DB_HOST: get('DB_HOST', 'localhost'),
|
|
@@ -110,6 +112,8 @@ export const Env = Object.freeze({
|
|
|
110
112
|
ENABLE_MICROSERVICES: getBool('ENABLE_MICROSERVICES', false),
|
|
111
113
|
TOKEN_TTL: getInt('TOKEN_TTL', 3600000),
|
|
112
114
|
TOKEN_LENGTH: getInt('TOKEN_LENGTH', 32),
|
|
115
|
+
// Encryption interop
|
|
116
|
+
ENCRYPTION_CIPHER: get('ENCRYPTION_CIPHER', ''),
|
|
113
117
|
// Deployment
|
|
114
118
|
ENVIRONMENT: get('ENVIRONMENT', 'development'),
|
|
115
119
|
REQUEST_TIMEOUT: getInt('REQUEST_TIMEOUT', 30000),
|
package/src/config/index.d.ts
CHANGED
|
@@ -139,6 +139,9 @@ export declare const config: Readonly<{
|
|
|
139
139
|
readonly cookieSameSite: "strict" | "lax" | "none";
|
|
140
140
|
};
|
|
141
141
|
readonly encryption: {
|
|
142
|
+
readonly cipher: string;
|
|
143
|
+
readonly appKey: string;
|
|
144
|
+
readonly appPreviousKeys: string;
|
|
142
145
|
readonly algorithm: string;
|
|
143
146
|
readonly key: string;
|
|
144
147
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/config/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAaH,OAAO,EAAE,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAC/D,OAAO,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,eAAe,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACvE,OAAO,EAAE,mBAAmB,EAAE,KAAK,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AACtF,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAE,KAAK,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AACnF,OAAO,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,eAAe,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEpE;;;GAGG;AACH,eAAO,MAAM,MAAM
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/config/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAaH,OAAO,EAAE,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAC/D,OAAO,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,eAAe,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACvE,OAAO,EAAE,mBAAmB,EAAE,KAAK,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AACtF,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAE,KAAK,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AACnF,OAAO,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,eAAe,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEpE;;;GAGG;AACH,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAWR,CAAC;AAEZ,MAAM,MAAM,MAAM,GAAG,OAAO,MAAM,CAAC"}
|
package/src/config/security.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Security keys can be configured per domain:
|
|
10
10
|
* - APP_KEY: Default encryption key for all operations (auto-generated)
|
|
11
11
|
* - API_KEY_SECRET: Optional API key authentication (if API_KEY_ENABLED=true)
|
|
12
|
-
* -
|
|
12
|
+
* - ENCRYPTION_CIPHER: Cipher for encrypted envelope interoperability
|
|
13
13
|
* - JWT_SECRET: JWT token signing key
|
|
14
14
|
*
|
|
15
15
|
* Developers can use a single APP_KEY or configure separate keys for different
|
|
@@ -44,6 +44,9 @@ export declare const securityConfig: Readonly<{
|
|
|
44
44
|
* Encryption
|
|
45
45
|
*/
|
|
46
46
|
readonly encryption: {
|
|
47
|
+
readonly cipher: string;
|
|
48
|
+
readonly appKey: string;
|
|
49
|
+
readonly appPreviousKeys: string;
|
|
47
50
|
readonly algorithm: string;
|
|
48
51
|
readonly key: string;
|
|
49
52
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"security.d.ts","sourceRoot":"","sources":["../../../src/config/security.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;
|
|
1
|
+
{"version":3,"file":"security.d.ts","sourceRoot":"","sources":["../../../src/config/security.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAyJH,eAAO,MAAM,cAAc;IAhIzB;;OAEG;;;yBAGa,MAAM;4BAQ4B,OAAO,GAAG,OAAO,GAAG,OAAO;;;;;;IAO7E;;OAEG;;;;;;;;iCAQ6D,QAAQ,GAAG,KAAK,GAAG,MAAM;;IAGzF;;OAEG;;;;;;;;IAgBH;;OAEG;;;;;;IAOH;;OAEG;;;;;;;;;;IAWH;;OAEG;;;;;;;IAQH;;OAEG;;;;;IAMH;;OAEG;;;;;;;;;;IAWH;;OAEG;;;;;;;2BAOmD,QAAQ,GAAG,KAAK,GAAG,MAAM;;IAG/E;;OAEG;;;;;;;;EAUyD,CAAC"}
|
package/src/config/security.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Security keys can be configured per domain:
|
|
10
10
|
* - APP_KEY: Default encryption key for all operations (auto-generated)
|
|
11
11
|
* - API_KEY_SECRET: Optional API key authentication (if API_KEY_ENABLED=true)
|
|
12
|
-
* -
|
|
12
|
+
* - ENCRYPTION_CIPHER: Cipher for encrypted envelope interoperability
|
|
13
13
|
* - JWT_SECRET: JWT token signing key
|
|
14
14
|
*
|
|
15
15
|
* Developers can use a single APP_KEY or configure separate keys for different
|
|
@@ -70,6 +70,14 @@ const securityConfigObj = {
|
|
|
70
70
|
* Encryption
|
|
71
71
|
*/
|
|
72
72
|
encryption: {
|
|
73
|
+
// Required for framework-compatible encrypted payloads.
|
|
74
|
+
// Supported values: aes-256-cbc | aes-256-gcm (case-insensitive)
|
|
75
|
+
cipher: Env.get('ENCRYPTION_CIPHER', ''),
|
|
76
|
+
// Primary key used for encryption interoperability (framework-compatible envelopes).
|
|
77
|
+
// APP_KEY supports both `base64:...` and raw base64.
|
|
78
|
+
appKey: Env.get('APP_KEY', ''),
|
|
79
|
+
appPreviousKeys: Env.get('APP_PREVIOUS_KEYS', ''),
|
|
80
|
+
// Back-compat fields (not used by EncryptedEnvelope)
|
|
73
81
|
algorithm: Env.get('ENCRYPTION_ALGORITHM', 'aes-256-cbc'),
|
|
74
82
|
key: Env.get('ENCRYPTION_KEY', 'your-encryption-key'),
|
|
75
83
|
},
|
package/src/features/Queue.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Queue.d.ts","sourceRoot":"","sources":["../../../src/features/Queue.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,OAAO,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,eAAO,MAAM,KAAK;UACJ,QAAQ,EAAE;IAEtB;;OAEG;
|
|
1
|
+
{"version":3,"file":"Queue.d.ts","sourceRoot":"","sources":["../../../src/features/Queue.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,OAAO,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,eAAO,MAAM,KAAK;UACJ,QAAQ,EAAE;IAEtB;;OAEG;QACC,CAAC,QAAQ,CAAC,GAAG,MAAM;IAcvB;;OAEG;qBACoB,CAAC,GAAG,EAAE,QAAQ,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;EAMvE,CAAC"}
|
package/src/features/Queue.js
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -98,6 +98,7 @@ export { Schema, Validator } from './validation/Validator';
|
|
|
98
98
|
export type { ISchema, SchemaType } from './validation/Validator';
|
|
99
99
|
export { CsrfTokenManager } from './security/CsrfTokenManager';
|
|
100
100
|
export type { CsrfTokenData, CsrfTokenManagerType, ICsrfTokenManager, } from './security/CsrfTokenManager';
|
|
101
|
+
export { EncryptedEnvelope } from './security/EncryptedEnvelope';
|
|
101
102
|
export { Encryptor } from './security/Encryptor';
|
|
102
103
|
export { Hash } from './security/Hash';
|
|
103
104
|
export { JwtManager } from './security/JwtManager';
|
package/src/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,QAAA,MAAM,mBAAmB;;EAAc,CAAC;AACxC,QAAA,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAAW,CAAC;AAClC,QAAA,MAAM,qBAAqB;;;;;;;;;;;EAAgB,CAAC;AAE5C,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,YAAY,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,YAAY,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAC5E,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAC9D,YAAY,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AACpE,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACpE,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAClE,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACrE,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACnE,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,YAAY,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAAE,mBAAmB,IAAI,WAAW,EAAE,CAAC;AAG9C,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AACvE,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,YAAY,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAG/C,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjE,OAAO,EAAE,gBAAgB,IAAI,QAAQ,EAAE,CAAC;AAGxC,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC1D,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGrE,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGpE,YAAY,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAGxE,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,YAAY,EACV,WAAW,EACX,cAAc,EACd,SAAS,EACT,aAAa,EACb,aAAa,GACd,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAC9D,YAAY,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAC1D,YAAY,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAGjE,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAC9D,YAAY,EACV,aAAa,EACb,oBAAoB,EACpB,iBAAiB,GAClB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,YAAY,EACV,WAAW,EACX,YAAY,EACZ,cAAc,EACd,UAAU,EACV,UAAU,GACX,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,wBAAwB,EAAE,MAAM,oCAAoC,CAAC;AAC9E,YAAY,EACV,yBAAyB,EACzB,wBAAwB,EACxB,+BAA+B,EAC/B,4BAA4B,EAC5B,wBAAwB,GACzB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,EAAE,qBAAqB,IAAI,aAAa,EAAE,CAAC;AAGlD,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAGzD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAGzF,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACzD,YAAY,EACV,QAAQ,EACR,eAAe,EACf,WAAW,EACX,qBAAqB,GACtB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAClC,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAExC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,YAAY,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD,OAAO,EAAE,+BAA+B,EAAE,MAAM,iCAAiC,CAAC;AAGlF,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAEjE,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,YAAY,EAAE,cAAc,IAAI,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAChF,OAAO,EAAE,kCAAkC,EAAE,MAAM,kCAAkC,CAAC;AAEtF,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,YAAY,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAEjE,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,YAAY,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAEzD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAC/D,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAErE,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAElD,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,YAAY,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,YAAY,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAErD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,YAAY,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAErD,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC5F,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAEhD,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,YAAY,EAAE,gBAAgB,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAGjF,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAGxE,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAClC,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEhE,OAAO,EAAE,oBAAoB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACtE,YAAY,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAG1E,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,EAAE,+BAA+B,EAAE,MAAM,uCAAuC,CAAC;AAExF,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,YAAY,EAAE,UAAU,IAAI,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAEzE,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,YAAY,EACV,cAAc,EACd,WAAW,IAAI,mBAAmB,EAClC,cAAc,IAAI,sBAAsB,EACxC,WAAW,IAAI,mBAAmB,EAClC,UAAU,IAAI,kBAAkB,GACjC,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,YAAY,EACV,aAAa,EACb,WAAW,IAAI,cAAc,EAC7B,UAAU,IAAI,aAAa,GAC5B,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AACrE,OAAO,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AAClE,OAAO,EAAE,OAAO,EAAE,MAAM,oCAAoC,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAChE,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AAGpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAGpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAGnE,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AACvE,OAAO,EAAE,qCAAqC,EAAE,MAAM,+CAA+C,CAAC;AAGtG,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAGlE,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAC/C,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,EAAE,qBAAqB,EAAE,MAAM,sCAAsC,CAAC;AAE7E,OAAO,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AACrD,YAAY,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAE1D,OAAO,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AACrD,YAAY,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAE1D,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AACvD,YAAY,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAG5D,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AACxD,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC"}
|
package/src/index.js
CHANGED
|
@@ -50,6 +50,7 @@ export { ValidationError } from './validation/ValidationError.js';
|
|
|
50
50
|
export { Schema, Validator } from './validation/Validator.js';
|
|
51
51
|
// Security
|
|
52
52
|
export { CsrfTokenManager } from './security/CsrfTokenManager.js';
|
|
53
|
+
export { EncryptedEnvelope } from './security/EncryptedEnvelope.js';
|
|
53
54
|
export { Encryptor } from './security/Encryptor.js';
|
|
54
55
|
export { Hash } from './security/Hash.js';
|
|
55
56
|
export { JwtManager } from './security/JwtManager.js';
|
|
@@ -12,8 +12,43 @@ export interface RateLimitOptions {
|
|
|
12
12
|
statusCode?: number;
|
|
13
13
|
headers?: boolean;
|
|
14
14
|
keyGenerator?: (req: IRequest) => string;
|
|
15
|
+
/**
|
|
16
|
+
* Optional store selection for this middleware instance.
|
|
17
|
+
* - 'memory' uses an in-process Map (default)
|
|
18
|
+
* - 'redis' uses Cache.store('redis')
|
|
19
|
+
* - 'kv' uses Cache.store('kv')
|
|
20
|
+
* - 'db' uses Cache.store('mongodb')
|
|
21
|
+
*/
|
|
22
|
+
store?: RateLimitStoreName;
|
|
15
23
|
}
|
|
24
|
+
export type RateLimitStoreName = 'memory' | 'redis' | 'kv' | 'db';
|
|
16
25
|
export declare const RateLimiter: Readonly<{
|
|
26
|
+
/**
|
|
27
|
+
* Configure the store used by the programmatic API (attempt/tooManyAttempts/till/clear).
|
|
28
|
+
* Defaults to 'memory'.
|
|
29
|
+
*/
|
|
30
|
+
configure(config?: {
|
|
31
|
+
store?: RateLimitStoreName;
|
|
32
|
+
}): void;
|
|
33
|
+
/**
|
|
34
|
+
* Attempt to perform an action.
|
|
35
|
+
*
|
|
36
|
+
* Returns true if allowed (and records the hit), false if rate limited.
|
|
37
|
+
*/
|
|
38
|
+
attempt(key: string, maxAttempts: number, decaySeconds: number): Promise<boolean>;
|
|
39
|
+
/**
|
|
40
|
+
* Check if the key is currently rate limited.
|
|
41
|
+
*/
|
|
42
|
+
tooManyAttempts(key: string, maxAttempts: number): Promise<boolean>;
|
|
43
|
+
/**
|
|
44
|
+
* Seconds until the key is available again.
|
|
45
|
+
* Returns 0 if not rate limited.
|
|
46
|
+
*/
|
|
47
|
+
till(key: string): Promise<number>;
|
|
48
|
+
/**
|
|
49
|
+
* Clear rate limit state for a key.
|
|
50
|
+
*/
|
|
51
|
+
clear(key: string): Promise<void>;
|
|
17
52
|
/**
|
|
18
53
|
* Create rate limiter middleware
|
|
19
54
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RateLimiter.d.ts","sourceRoot":"","sources":["../../../src/middleware/RateLimiter.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"RateLimiter.d.ts","sourceRoot":"","sources":["../../../src/middleware/RateLimiter.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAEzC,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAEzD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,QAAQ,KAAK,MAAM,CAAC;IAEzC;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,kBAAkB,CAAC;CAC5B;AAED,MAAM,MAAM,kBAAkB,GAAG,QAAQ,GAAG,OAAO,GAAG,IAAI,GAAG,IAAI,CAAC;AAyIlE,eAAO,MAAM,WAAW;IACtB;;;OAGG;uBACgB;QAAE,KAAK,CAAC,EAAE,kBAAkB,CAAA;KAAE,GAAG,IAAI;IAKxD;;;;OAIG;iBACgB,MAAM,eAAe,MAAM,gBAAgB,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAYvF;;OAEG;yBACwB,MAAM,eAAe,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAQzE;;;OAGG;cACa,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAQxC;;OAEG;eACc,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKvC;;OAEG;qBACa,gBAAgB,GAAqB,UAAU;EAgF/D,CAAC"}
|
|
@@ -3,7 +3,104 @@
|
|
|
3
3
|
* Token bucket implementation for request rate limiting
|
|
4
4
|
* Zero-dependency implementation
|
|
5
5
|
*/
|
|
6
|
+
import { Cache } from '../cache/Cache.js';
|
|
6
7
|
import { Logger } from '../config/logger.js';
|
|
8
|
+
const createMemoryStore = () => {
|
|
9
|
+
const entries = new Map();
|
|
10
|
+
let nextCleanupAt = Date.now() + 60_000;
|
|
11
|
+
const cleanupExpired = (now) => {
|
|
12
|
+
if (now < nextCleanupAt)
|
|
13
|
+
return;
|
|
14
|
+
for (const [k, state] of entries.entries()) {
|
|
15
|
+
if (now > state.resetTime)
|
|
16
|
+
entries.delete(k);
|
|
17
|
+
}
|
|
18
|
+
nextCleanupAt = now + 60_000;
|
|
19
|
+
};
|
|
20
|
+
return Object.freeze({
|
|
21
|
+
async get(key) {
|
|
22
|
+
await Promise.resolve();
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
cleanupExpired(now);
|
|
25
|
+
const state = entries.get(key);
|
|
26
|
+
if (!state)
|
|
27
|
+
return null;
|
|
28
|
+
if (now > state.resetTime) {
|
|
29
|
+
entries.delete(key);
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return { ...state };
|
|
33
|
+
},
|
|
34
|
+
async set(key, value) {
|
|
35
|
+
await Promise.resolve();
|
|
36
|
+
const now = Date.now();
|
|
37
|
+
cleanupExpired(now);
|
|
38
|
+
entries.set(key, { ...value });
|
|
39
|
+
},
|
|
40
|
+
async delete(key) {
|
|
41
|
+
await Promise.resolve();
|
|
42
|
+
entries.delete(key);
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
const createCacheStore = (storeName) => {
|
|
47
|
+
const store = Cache.store(storeName);
|
|
48
|
+
return Object.freeze({
|
|
49
|
+
async get(key) {
|
|
50
|
+
return store.get(key);
|
|
51
|
+
},
|
|
52
|
+
async set(key, value, ttlSeconds) {
|
|
53
|
+
await store.set(key, value, ttlSeconds);
|
|
54
|
+
},
|
|
55
|
+
async delete(key) {
|
|
56
|
+
await store.delete(key);
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
const normalizeStoreName = (name) => {
|
|
61
|
+
const raw = String(name ?? '')
|
|
62
|
+
.trim()
|
|
63
|
+
.toLowerCase();
|
|
64
|
+
if (raw === 'redis')
|
|
65
|
+
return 'redis';
|
|
66
|
+
if (raw === 'kv')
|
|
67
|
+
return 'kv';
|
|
68
|
+
if (raw === 'db' || raw === 'database' || raw === 'mongo' || raw === 'mongodb')
|
|
69
|
+
return 'db';
|
|
70
|
+
return 'memory';
|
|
71
|
+
};
|
|
72
|
+
const resolveStore = (name) => {
|
|
73
|
+
const selected = normalizeStoreName(name ?? process.env['RATE_LIMIT_STORE'] ?? process.env['RATE_LIMIT_DRIVER'] ?? 'memory');
|
|
74
|
+
if (selected === 'redis')
|
|
75
|
+
return { storeName: 'redis', store: createCacheStore('redis') };
|
|
76
|
+
if (selected === 'kv')
|
|
77
|
+
return { storeName: 'kv', store: createCacheStore('kv') };
|
|
78
|
+
if (selected === 'db')
|
|
79
|
+
return { storeName: 'db', store: createCacheStore('mongodb') };
|
|
80
|
+
return { storeName: 'memory', store: createMemoryStore() };
|
|
81
|
+
};
|
|
82
|
+
let serviceStoreSelection = normalizeStoreName(process.env['RATE_LIMIT_STORE'] ?? process.env['RATE_LIMIT_DRIVER'] ?? 'memory');
|
|
83
|
+
let serviceStore = resolveStore(serviceStoreSelection).store;
|
|
84
|
+
const prefixKey = (purpose, key) => {
|
|
85
|
+
const prefix = (process.env['RATE_LIMIT_KEY_PREFIX'] ?? 'zintrust:ratelimit:').toString().trim();
|
|
86
|
+
return `${prefix}${purpose}:${key}`;
|
|
87
|
+
};
|
|
88
|
+
const consume = async (params) => {
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
const ttlSeconds = Math.max(1, Math.ceil(params.windowMs / 1000));
|
|
91
|
+
const existing = await params.store.get(params.key);
|
|
92
|
+
const state = existing === null || now > existing.resetTime
|
|
93
|
+
? { count: 0, resetTime: now + params.windowMs }
|
|
94
|
+
: existing;
|
|
95
|
+
const nextCount = state.count + 1;
|
|
96
|
+
const nextState = { count: nextCount, resetTime: state.resetTime };
|
|
97
|
+
await params.store.set(params.key, nextState, ttlSeconds);
|
|
98
|
+
return {
|
|
99
|
+
count: nextCount,
|
|
100
|
+
resetTime: nextState.resetTime,
|
|
101
|
+
allowed: nextCount <= params.max,
|
|
102
|
+
};
|
|
103
|
+
};
|
|
7
104
|
const DEFAULT_OPTIONS = {
|
|
8
105
|
windowMs: 60 * 1000, // 1 minute
|
|
9
106
|
max: 100, // 100 requests per minute
|
|
@@ -15,11 +112,68 @@ const DEFAULT_OPTIONS = {
|
|
|
15
112
|
},
|
|
16
113
|
};
|
|
17
114
|
export const RateLimiter = Object.freeze({
|
|
115
|
+
/**
|
|
116
|
+
* Configure the store used by the programmatic API (attempt/tooManyAttempts/till/clear).
|
|
117
|
+
* Defaults to 'memory'.
|
|
118
|
+
*/
|
|
119
|
+
configure(config) {
|
|
120
|
+
serviceStoreSelection = normalizeStoreName(config?.store);
|
|
121
|
+
serviceStore = resolveStore(serviceStoreSelection).store;
|
|
122
|
+
},
|
|
123
|
+
/**
|
|
124
|
+
* Attempt to perform an action.
|
|
125
|
+
*
|
|
126
|
+
* Returns true if allowed (and records the hit), false if rate limited.
|
|
127
|
+
*/
|
|
128
|
+
async attempt(key, maxAttempts, decaySeconds) {
|
|
129
|
+
const windowMs = Math.max(1, Math.floor(decaySeconds * 1000));
|
|
130
|
+
const namespacedKey = prefixKey('service', key);
|
|
131
|
+
const out = await consume({
|
|
132
|
+
store: serviceStore,
|
|
133
|
+
key: namespacedKey,
|
|
134
|
+
max: maxAttempts,
|
|
135
|
+
windowMs,
|
|
136
|
+
});
|
|
137
|
+
return out.allowed;
|
|
138
|
+
},
|
|
139
|
+
/**
|
|
140
|
+
* Check if the key is currently rate limited.
|
|
141
|
+
*/
|
|
142
|
+
async tooManyAttempts(key, maxAttempts) {
|
|
143
|
+
const now = Date.now();
|
|
144
|
+
const namespacedKey = prefixKey('service', key);
|
|
145
|
+
const state = await serviceStore.get(namespacedKey);
|
|
146
|
+
if (!state || now > state.resetTime)
|
|
147
|
+
return false;
|
|
148
|
+
return state.count >= maxAttempts;
|
|
149
|
+
},
|
|
150
|
+
/**
|
|
151
|
+
* Seconds until the key is available again.
|
|
152
|
+
* Returns 0 if not rate limited.
|
|
153
|
+
*/
|
|
154
|
+
async till(key) {
|
|
155
|
+
const now = Date.now();
|
|
156
|
+
const namespacedKey = prefixKey('service', key);
|
|
157
|
+
const state = await serviceStore.get(namespacedKey);
|
|
158
|
+
if (!state || now > state.resetTime)
|
|
159
|
+
return 0;
|
|
160
|
+
return Math.max(0, Math.ceil((state.resetTime - now) / 1000));
|
|
161
|
+
},
|
|
162
|
+
/**
|
|
163
|
+
* Clear rate limit state for a key.
|
|
164
|
+
*/
|
|
165
|
+
async clear(key) {
|
|
166
|
+
const namespacedKey = prefixKey('service', key);
|
|
167
|
+
await serviceStore.delete(namespacedKey);
|
|
168
|
+
},
|
|
18
169
|
/**
|
|
19
170
|
* Create rate limiter middleware
|
|
20
171
|
*/
|
|
21
172
|
create(options = DEFAULT_OPTIONS) {
|
|
22
173
|
const config = { ...DEFAULT_OPTIONS, ...options };
|
|
174
|
+
const { storeName, store } = resolveStore(config.store);
|
|
175
|
+
const useMemoryInstanceStore = storeName === 'memory';
|
|
176
|
+
// Middleware store is per-instance (matches prior behavior).
|
|
23
177
|
const clients = new Map();
|
|
24
178
|
// Cleanup to prevent unbounded growth.
|
|
25
179
|
// Done lazily (on requests) to avoid background timers in serverless/test environments.
|
|
@@ -27,29 +181,47 @@ export const RateLimiter = Object.freeze({
|
|
|
27
181
|
const cleanupExpiredClients = (now) => {
|
|
28
182
|
if (now < nextCleanupAt)
|
|
29
183
|
return;
|
|
30
|
-
for (const [
|
|
184
|
+
for (const [k, state] of clients.entries()) {
|
|
31
185
|
if (now > state.resetTime) {
|
|
32
|
-
clients.delete(
|
|
186
|
+
clients.delete(k);
|
|
33
187
|
}
|
|
34
188
|
}
|
|
35
189
|
nextCleanupAt = now + config.windowMs;
|
|
36
190
|
};
|
|
37
|
-
|
|
38
|
-
const key = config.keyGenerator ? config.keyGenerator(req) : 'unknown';
|
|
39
|
-
const now = Date.now();
|
|
40
|
-
cleanupExpiredClients(now);
|
|
191
|
+
const getOrInitClient = (key, now) => {
|
|
41
192
|
let client = clients.get(key);
|
|
42
|
-
// Initialize or reset if window expired
|
|
43
193
|
if (!client || now > client.resetTime) {
|
|
44
|
-
client = {
|
|
45
|
-
count: 0,
|
|
46
|
-
resetTime: now + config.windowMs,
|
|
47
|
-
};
|
|
194
|
+
client = { count: 0, resetTime: now + config.windowMs };
|
|
48
195
|
clients.set(key, client);
|
|
49
196
|
}
|
|
50
|
-
client
|
|
51
|
-
|
|
52
|
-
|
|
197
|
+
return client;
|
|
198
|
+
};
|
|
199
|
+
return async (req, res, next) => {
|
|
200
|
+
const key = config.keyGenerator ? config.keyGenerator(req) : 'unknown';
|
|
201
|
+
const now = Date.now();
|
|
202
|
+
let count;
|
|
203
|
+
let resetAt;
|
|
204
|
+
if (useMemoryInstanceStore) {
|
|
205
|
+
cleanupExpiredClients(now);
|
|
206
|
+
const client = getOrInitClient(key, now);
|
|
207
|
+
client.count++;
|
|
208
|
+
count = client.count;
|
|
209
|
+
resetAt = client.resetTime;
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
// Include limiter config to avoid collisions between different middleware instances.
|
|
213
|
+
const middlewareKey = prefixKey('middleware', `${config.max}:${config.windowMs}:${key}`);
|
|
214
|
+
const out = await consume({
|
|
215
|
+
store,
|
|
216
|
+
key: middlewareKey,
|
|
217
|
+
max: config.max,
|
|
218
|
+
windowMs: config.windowMs,
|
|
219
|
+
});
|
|
220
|
+
count = out.count;
|
|
221
|
+
resetAt = out.resetTime;
|
|
222
|
+
}
|
|
223
|
+
const remaining = Math.max(0, config.max - count);
|
|
224
|
+
const resetTime = Math.ceil((resetAt - now) / 1000);
|
|
53
225
|
// Set headers
|
|
54
226
|
if (config.headers ?? false) {
|
|
55
227
|
res.setHeader('X-RateLimit-Limit', config.max.toString());
|
|
@@ -57,7 +229,7 @@ export const RateLimiter = Object.freeze({
|
|
|
57
229
|
res.setHeader('X-RateLimit-Reset', resetTime.toString());
|
|
58
230
|
}
|
|
59
231
|
// Check limit
|
|
60
|
-
if (
|
|
232
|
+
if (count > config.max) {
|
|
61
233
|
Logger.warn(`Rate limit exceeded for IP: ${key}`);
|
|
62
234
|
res.setStatus(config.statusCode ?? 429);
|
|
63
235
|
res.json({
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
* Safe to import in both API and CLI code
|
|
4
4
|
* Exported from node:crypto built-in
|
|
5
5
|
*/
|
|
6
|
-
export { createHash, createHmac, createSign, createVerify, generateKeyPairSync, pbkdf2Sync, randomBytes, randomInt, } from 'node:crypto';
|
|
6
|
+
export { createCipheriv, createDecipheriv, createHash, createHmac, createSign, createVerify, generateKeyPairSync, pbkdf2Sync, randomBytes, randomInt, timingSafeEqual, } from 'node:crypto';
|
|
7
7
|
//# sourceMappingURL=crypto.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../../src/node-singletons/crypto.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,UAAU,EACV,UAAU,EACV,UAAU,EACV,YAAY,EACZ,mBAAmB,EACnB,UAAU,EACV,WAAW,EACX,SAAS,
|
|
1
|
+
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../../src/node-singletons/crypto.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,UAAU,EACV,UAAU,EACV,UAAU,EACV,YAAY,EACZ,mBAAmB,EACnB,UAAU,EACV,WAAW,EACX,SAAS,EACT,eAAe,GAChB,MAAM,aAAa,CAAC"}
|
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
* Safe to import in both API and CLI code
|
|
4
4
|
* Exported from node:crypto built-in
|
|
5
5
|
*/
|
|
6
|
-
export { createHash, createHmac, createSign, createVerify, generateKeyPairSync, pbkdf2Sync, randomBytes, randomInt, } from 'node:crypto';
|
|
6
|
+
export { createCipheriv, createDecipheriv, createHash, createHmac, createSign, createVerify, generateKeyPairSync, pbkdf2Sync, randomBytes, randomInt, timingSafeEqual, } from 'node:crypto';
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
export type EncryptedEnvelopeCipher = 'aes-256-cbc' | 'aes-256-gcm';
|
|
13
|
+
export type EncryptedEnvelopeCipherInput = EncryptedEnvelopeCipher | 'AES-256-CBC' | 'AES-256-GCM';
|
|
14
|
+
export type EncryptedEnvelopePayload = {
|
|
15
|
+
iv: string;
|
|
16
|
+
value: string;
|
|
17
|
+
mac?: string;
|
|
18
|
+
tag?: string;
|
|
19
|
+
};
|
|
20
|
+
export type EncryptedEnvelopeSerializer<T> = {
|
|
21
|
+
serialize: (value: T) => string;
|
|
22
|
+
deserialize: (value: string) => T;
|
|
23
|
+
};
|
|
24
|
+
export type EncryptedEnvelopeKeyring = {
|
|
25
|
+
primaryKey: Uint8Array;
|
|
26
|
+
previousKeys: Uint8Array[];
|
|
27
|
+
};
|
|
28
|
+
export type EncryptedEnvelopeEnv = {
|
|
29
|
+
APP_KEY?: string;
|
|
30
|
+
APP_PREVIOUS_KEYS?: string;
|
|
31
|
+
ENCRYPTION_CIPHER?: string;
|
|
32
|
+
};
|
|
33
|
+
export declare const EncryptedEnvelope: Readonly<{
|
|
34
|
+
normalizeCipher: (cipher: EncryptedEnvelopeCipherInput) => EncryptedEnvelopeCipher;
|
|
35
|
+
/**
|
|
36
|
+
* Build a keyring from environment variables.
|
|
37
|
+
* - Uses APP_KEY
|
|
38
|
+
* - Supports APP_PREVIOUS_KEYS (comma-separated or JSON array)
|
|
39
|
+
*/
|
|
40
|
+
keyringFromEnv(env?: EncryptedEnvelopeEnv): EncryptedEnvelopeKeyring;
|
|
41
|
+
/**
|
|
42
|
+
* Encrypt a UTF-8 string and return a framework-compatible base64(JSON) envelope.
|
|
43
|
+
*/
|
|
44
|
+
encryptString(plaintext: string, options: {
|
|
45
|
+
cipher: EncryptedEnvelopeCipherInput;
|
|
46
|
+
key: string;
|
|
47
|
+
}): string;
|
|
48
|
+
/**
|
|
49
|
+
* Decrypt a framework-compatible base64(JSON) envelope to a UTF-8 string.
|
|
50
|
+
* Tries the primary key first, then previous keys.
|
|
51
|
+
*/
|
|
52
|
+
decryptString(encrypted: string, options: {
|
|
53
|
+
cipher: EncryptedEnvelopeCipherInput;
|
|
54
|
+
key: string;
|
|
55
|
+
previousKeys?: string[];
|
|
56
|
+
}): string;
|
|
57
|
+
/**
|
|
58
|
+
* Encrypt arbitrary values using a caller-provided serializer.
|
|
59
|
+
* This supports encrypted payloads for frameworks that store serialized values.
|
|
60
|
+
*/
|
|
61
|
+
encrypt<T>(value: T, options: {
|
|
62
|
+
cipher: EncryptedEnvelopeCipherInput;
|
|
63
|
+
key: string;
|
|
64
|
+
serializer: EncryptedEnvelopeSerializer<T>;
|
|
65
|
+
}): string;
|
|
66
|
+
/**
|
|
67
|
+
* Decrypt into an arbitrary value using a caller-provided serializer.
|
|
68
|
+
*/
|
|
69
|
+
decrypt<T>(encrypted: string, options: {
|
|
70
|
+
cipher: EncryptedEnvelopeCipherInput;
|
|
71
|
+
key: string;
|
|
72
|
+
previousKeys?: string[];
|
|
73
|
+
serializer: EncryptedEnvelopeSerializer<T>;
|
|
74
|
+
}): T;
|
|
75
|
+
}>;
|
|
76
|
+
export default EncryptedEnvelope;
|
|
77
|
+
//# sourceMappingURL=EncryptedEnvelope.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EncryptedEnvelope.d.ts","sourceRoot":"","sources":["../../../src/security/EncryptedEnvelope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAYH,MAAM,MAAM,uBAAuB,GAAG,aAAa,GAAG,aAAa,CAAC;AACpE,MAAM,MAAM,4BAA4B,GAAG,uBAAuB,GAAG,aAAa,GAAG,aAAa,CAAC;AAEnG,MAAM,MAAM,wBAAwB,GAAG;IACrC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,2BAA2B,CAAC,CAAC,IAAI;IAC3C,SAAS,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,CAAC;IAChC,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,CAAC,CAAC;CACnC,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IACrC,UAAU,EAAE,UAAU,CAAC;IACvB,YAAY,EAAE,UAAU,EAAE,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AA6LF,eAAO,MAAM,iBAAiB;8BA3LG,4BAA4B,KAAG,uBAAuB;IA8LrF;;;;OAIG;yBAEI,oBAAoB,GACxB,wBAAwB;IAe3B;;OAEG;6BAEU,MAAM,WACR;QAAE,MAAM,EAAE,4BAA4B,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAC7D,MAAM;IAgCT;;;OAGG;6BAEU,MAAM,WACR;QAAE,MAAM,EAAE,4BAA4B,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,GACtF,MAAM;IA0BT;;;OAGG;YACK,CAAC,SACA,CAAC,WACC;QACP,MAAM,EAAE,4BAA4B,CAAC;QACrC,GAAG,EAAE,MAAM,CAAC;QACZ,UAAU,EAAE,2BAA2B,CAAC,CAAC,CAAC,CAAC;KAC5C,GACA,MAAM;IAQT;;OAEG;YACK,CAAC,aACI,MAAM,WACR;QACP,MAAM,EAAE,4BAA4B,CAAC;QACrC,GAAG,EAAE,MAAM,CAAC;QACZ,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;QACxB,UAAU,EAAE,2BAA2B,CAAC,CAAC,CAAC,CAAC;KAC5C,GACA,CAAC;EASJ,CAAC;AAEH,eAAe,iBAAiB,CAAC"}
|
|
@@ -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;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StartupSecretValidation.d.ts","sourceRoot":"","sources":["../../../src/security/StartupSecretValidation.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
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() {
|
|
@@ -64,6 +64,8 @@ export const Env = Object.freeze({
|
|
|
64
64
|
HOST: get('HOST', 'localhost'),
|
|
65
65
|
APP_NAME: get('APP_NAME', 'ZinTrust'),
|
|
66
66
|
APP_KEY: get('APP_KEY', ''),
|
|
67
|
+
// Optional key rotation support (comma-separated or JSON array of keys)
|
|
68
|
+
APP_PREVIOUS_KEYS: get('APP_PREVIOUS_KEYS', ''),
|
|
67
69
|
|
|
68
70
|
// Database
|
|
69
71
|
DB_CONNECTION: get('DB_CONNECTION', 'sqlite'),
|
|
@@ -124,6 +126,9 @@ export const Env = Object.freeze({
|
|
|
124
126
|
TOKEN_TTL: getInt('TOKEN_TTL', 3600000),
|
|
125
127
|
TOKEN_LENGTH: getInt('TOKEN_LENGTH', 32),
|
|
126
128
|
|
|
129
|
+
// Encryption interop
|
|
130
|
+
ENCRYPTION_CIPHER: get('ENCRYPTION_CIPHER', ''),
|
|
131
|
+
|
|
127
132
|
// Deployment
|
|
128
133
|
ENVIRONMENT: get('ENVIRONMENT', 'development'),
|
|
129
134
|
REQUEST_TIMEOUT: getInt('REQUEST_TIMEOUT', 30000),
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Security keys can be configured per domain:
|
|
10
10
|
* - APP_KEY: Default encryption key for all operations (auto-generated)
|
|
11
11
|
* - API_KEY_SECRET: Optional API key authentication (if API_KEY_ENABLED=true)
|
|
12
|
-
* -
|
|
12
|
+
* - ENCRYPTION_CIPHER: Cipher for encrypted envelope interoperability
|
|
13
13
|
* - JWT_SECRET: JWT token signing key
|
|
14
14
|
*
|
|
15
15
|
* Developers can use a single APP_KEY or configure separate keys for different
|
|
@@ -76,6 +76,16 @@ const securityConfigObj = {
|
|
|
76
76
|
* Encryption
|
|
77
77
|
*/
|
|
78
78
|
encryption: {
|
|
79
|
+
// Required for framework-compatible encrypted payloads.
|
|
80
|
+
// Supported values: aes-256-cbc | aes-256-gcm (case-insensitive)
|
|
81
|
+
cipher: Env.get('ENCRYPTION_CIPHER', ''),
|
|
82
|
+
|
|
83
|
+
// Primary key used for encryption interoperability (framework-compatible envelopes).
|
|
84
|
+
// APP_KEY supports both `base64:...` and raw base64.
|
|
85
|
+
appKey: Env.get('APP_KEY', ''),
|
|
86
|
+
appPreviousKeys: Env.get('APP_PREVIOUS_KEYS', ''),
|
|
87
|
+
|
|
88
|
+
// Back-compat fields (not used by EncryptedEnvelope)
|
|
79
89
|
algorithm: Env.get('ENCRYPTION_ALGORITHM', 'aes-256-cbc'),
|
|
80
90
|
key: Env.get('ENCRYPTION_KEY', 'your-encryption-key'),
|
|
81
91
|
},
|