@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.
Files changed (36) 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/PromptHelper.js +1 -1
  5. package/src/cli/commands/NewCommand.js +1 -1
  6. package/src/cli/config/ConfigSchema.js +1 -1
  7. package/src/cli/scaffolding/ProjectScaffolder.d.ts.map +1 -1
  8. package/src/cli/scaffolding/ProjectScaffolder.js +90 -22
  9. package/src/config/env.d.ts +2 -0
  10. package/src/config/env.d.ts.map +1 -1
  11. package/src/config/env.js +4 -0
  12. package/src/config/index.d.ts +3 -0
  13. package/src/config/index.d.ts.map +1 -1
  14. package/src/config/security.d.ts +4 -1
  15. package/src/config/security.d.ts.map +1 -1
  16. package/src/config/security.js +9 -1
  17. package/src/features/Queue.d.ts +1 -1
  18. package/src/features/Queue.d.ts.map +1 -1
  19. package/src/features/Queue.js +1 -1
  20. package/src/index.d.ts +1 -0
  21. package/src/index.d.ts.map +1 -1
  22. package/src/index.js +1 -0
  23. package/src/middleware/RateLimiter.d.ts +35 -0
  24. package/src/middleware/RateLimiter.d.ts.map +1 -1
  25. package/src/middleware/RateLimiter.js +187 -15
  26. package/src/node-singletons/crypto.d.ts +1 -1
  27. package/src/node-singletons/crypto.d.ts.map +1 -1
  28. package/src/node-singletons/crypto.js +1 -1
  29. package/src/security/EncryptedEnvelope.d.ts +77 -0
  30. package/src/security/EncryptedEnvelope.d.ts.map +1 -0
  31. package/src/security/EncryptedEnvelope.js +256 -0
  32. package/src/security/StartupSecretValidation.d.ts.map +1 -1
  33. package/src/security/StartupSecretValidation.js +72 -0
  34. package/src/templates/features/Queue.ts.tpl +1 -1
  35. package/src/templates/project/basic/config/env.ts.tpl +5 -0
  36. 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
- npm run dev
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
- npm run dev
172
+ zin start
173
173
  ```
174
174
 
175
175
  Test it:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zintrust/core",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "Production-grade TypeScript backend framework for JavaScript",
5
5
  "homepage": "https://zintrust.com",
6
6
  "repository": {
package/public/index.html CHANGED
@@ -366,7 +366,7 @@
366
366
  >
367
367
  <a
368
368
  class="hover:text-slate-900 dark:hover:text-white"
369
- href="https://linkedin.com/company/zintrust"
369
+ href="https://linkedin.com/company/zintrustjs"
370
370
  target="_blank"
371
371
  >LinkedIn</a
372
372
  >
@@ -63,7 +63,7 @@ export const PromptHelper = Object.freeze({
63
63
  /**
64
64
  * Ask for port number
65
65
  */
66
- async port(defaultPort = 3000, interactive = true) {
66
+ async port(defaultPort = 7777, interactive = true) {
67
67
  if (!interactive) {
68
68
  return defaultPort;
69
69
  }
@@ -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 : 3000;
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);
@@ -18,7 +18,7 @@ export const DEFAULT_CONFIG = Object.freeze({
18
18
  logging: false,
19
19
  },
20
20
  server: {
21
- port: 3000,
21
+ port: 7777,
22
22
  host: '0.0.0.0',
23
23
  environment: 'development',
24
24
  debug: false,
@@ -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;AAwaD,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"}
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'] ?? 3000,
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 the template already produced an .env, do not overwrite it here.
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'] ?? 3000);
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
- 'ENCRYPTION_KEY=',
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 ?? 3000,
494
+ port: options.port ?? 7777,
427
495
  database: options.database ?? 'sqlite',
428
496
  template: state.templateName,
429
497
  migrationTimestamp,
@@ -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;
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;eAgGP,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO;;;;;;;;;EAkCxF,CAAC"}
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),
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAWR,CAAC;AAEZ,MAAM,MAAM,MAAM,GAAG,OAAO,MAAM,CAAC"}
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"}
@@ -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
- * - ENCRYPTION_KEY: Optional separate encryption key (overrides APP_KEY if set)
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;AA+IH,eAAO,MAAM,cAAc;IAtHzB;;OAEG;;;yBAGa,MAAM;4BAQ4B,OAAO,GAAG,OAAO,GAAG,OAAO;;;;;;IAO7E;;OAEG;;;;;;;;iCAQ6D,QAAQ,GAAG,KAAK,GAAG,MAAM;;IAGzF;;OAEG;;;;;IAMH;;OAEG;;;;;;IAOH;;OAEG;;;;;;;;;;IAWH;;OAEG;;;;;;;IAQH;;OAEG;;;;;IAMH;;OAEG;;;;;;;;;;IAWH;;OAEG;;;;;;;2BAOmD,QAAQ,GAAG,KAAK,GAAG,MAAM;;IAG/E;;OAEG;;;;;;;;EAUyD,CAAC"}
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"}
@@ -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
- * - ENCRYPTION_KEY: Optional separate encryption key (overrides APP_KEY if set)
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
  },
@@ -12,7 +12,7 @@ export declare const Queue: Readonly<{
12
12
  /**
13
13
  * Add a job to the queue
14
14
  */
15
- add<T>(data: T): Promise<string>;
15
+ add<T>(data: T): string;
16
16
  /**
17
17
  * Process jobs (Placeholder)
18
18
  */
@@ -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;QACO,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;IActC;;OAEG;qBACoB,CAAC,GAAG,EAAE,QAAQ,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;EAMvE,CAAC"}
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"}
@@ -10,7 +10,7 @@ export const Queue = Object.freeze({
10
10
  /**
11
11
  * Add a job to the queue
12
12
  */
13
- async add(data) {
13
+ add(data) {
14
14
  const id = generateSecureJobId();
15
15
  const job = {
16
16
  id,
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';
@@ -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":"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"}
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;AAGH,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;CAC1C;AAoBD,eAAO,MAAM,WAAW;IACtB;;OAEG;qBACa,gBAAgB,GAAqB,UAAU;EA4D/D,CAAC"}
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 [key, state] of clients.entries()) {
184
+ for (const [k, state] of clients.entries()) {
31
185
  if (now > state.resetTime) {
32
- clients.delete(key);
186
+ clients.delete(k);
33
187
  }
34
188
  }
35
189
  nextCleanupAt = now + config.windowMs;
36
190
  };
37
- return async (req, res, next) => {
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.count++;
51
- const remaining = Math.max(0, config.max - client.count);
52
- const resetTime = Math.ceil((client.resetTime - now) / 1000);
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 (client.count > config.max) {
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,GACV,MAAM,aAAa,CAAC"}
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;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() {
@@ -19,7 +19,7 @@ export const Queue = Object.freeze({
19
19
  /**
20
20
  * Add a job to the queue
21
21
  */
22
- async add<T>(data: T): Promise<string> {
22
+ add<T>(data: T): string {
23
23
  const id = generateSecureJobId();
24
24
  const job: QueueJob = {
25
25
  id,
@@ -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
- * - ENCRYPTION_KEY: Optional separate encryption key (overrides APP_KEY if set)
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
  },