@zintrust/core 0.1.2 → 0.1.4

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 (46) hide show
  1. package/README.md +2 -2
  2. package/bin/zintrust.d.ts.map +1 -1
  3. package/bin/zintrust.js +59 -37
  4. package/package.json +15 -1
  5. package/src/cli/BaseCommand.js +1 -1
  6. package/src/cli/CLI.d.ts.map +1 -1
  7. package/src/cli/CLI.js +4 -3
  8. package/src/cli/commands/NewCommand.d.ts +4 -3
  9. package/src/cli/commands/NewCommand.d.ts.map +1 -1
  10. package/src/cli/commands/NewCommand.js +117 -31
  11. package/src/cli/scaffolding/ProjectScaffolder.d.ts.map +1 -1
  12. package/src/cli/scaffolding/ProjectScaffolder.js +15 -1
  13. package/src/index.d.ts +7 -2
  14. package/src/index.d.ts.map +1 -1
  15. package/src/index.js +4 -0
  16. package/src/scripts/TemplateSync.js +31 -1
  17. package/src/security/XssProtection.d.ts.map +1 -1
  18. package/src/security/XssProtection.js +6 -2
  19. package/src/templates/project/basic/config/FileLogWriter.ts.tpl +1 -1
  20. package/src/templates/project/basic/config/SecretsManager.ts.tpl +1 -1
  21. package/src/templates/project/basic/config/StartupConfigValidator.ts.tpl +1 -1
  22. package/src/templates/project/basic/config/cloudflare.ts.tpl +1 -1
  23. package/src/templates/project/basic/config/logging/HttpLogger.ts.tpl +3 -3
  24. package/src/templates/project/basic/config/logging/SlackLogger.ts.tpl +2 -2
  25. package/src/templates/project/basic/config/security.ts.tpl +1 -1
  26. package/src/templates/project/basic/package.json.tpl +1 -1
  27. package/src/templates/project/basic/routes/api.ts.tpl +3 -1
  28. package/src/templates/project/basic/routes/broadcast.ts.tpl +1 -1
  29. package/src/templates/project/basic/routes/health.ts.tpl +3 -3
  30. package/src/templates/project/basic/routes/storage.ts.tpl +42 -0
  31. package/src/templates/project/basic/tsconfig.json.tpl +15 -1
  32. package/src/tools/http/Http.d.ts +0 -2
  33. package/src/tools/http/Http.d.ts.map +1 -1
  34. package/src/tools/http/Http.js +0 -2
  35. package/src/tools/storage/LocalSignedUrl.d.ts +12 -0
  36. package/src/tools/storage/LocalSignedUrl.d.ts.map +1 -0
  37. package/src/tools/storage/LocalSignedUrl.js +108 -0
  38. package/src/tools/storage/drivers/Local.d.ts +2 -1
  39. package/src/tools/storage/drivers/Local.d.ts.map +1 -1
  40. package/src/tools/storage/drivers/Local.js +39 -13
  41. package/src/tools/storage/index.d.ts +1 -1
  42. package/src/tools/storage/index.d.ts.map +1 -1
  43. package/src/tools/storage/index.js +4 -5
  44. package/src/tools/storage/testing.d.ts +1 -1
  45. package/src/tools/storage/testing.d.ts.map +1 -1
  46. package/src/tools/storage/testing.js +2 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Getting Started with Zintrust
2
2
 
3
- Welcome to Zintrust, a production-grade TypeScript backend framework with proven architectural patterns and zero external dependencies.
3
+ Welcome to Zintrust, a production-grade TypeScript backend framework with proven architectural patterns and a minimal core (no Express/Fastify). The published npm package also includes a few runtime dependencies for the CLI and developer experience.
4
4
 
5
5
  ## Quick Start (2 minutes)
6
6
 
@@ -20,7 +20,7 @@ Your API is now running at `http://localhost:7777`
20
20
 
21
21
  ## What is Zintrust?
22
22
 
23
- Zintrust is a **zero-dependency** backend framework built on:
23
+ Zintrust is a **minimal-core** backend framework built on:
24
24
 
25
25
  - ✅ **Pure Node.js** - No Express, Fastify, or external HTTP libraries
26
26
  - ✅ **Type-Safe** - Strict TypeScript with 100% type coverage
@@ -1 +1 @@
1
- {"version":3,"file":"zintrust.d.ts","sourceRoot":"","sources":["../../bin/zintrust.ts"],"names":[],"mappings":";AAEA;;;;;GAKG;AAmEH,OAAO,EAAE,CAAC"}
1
+ {"version":3,"file":"zintrust.d.ts","sourceRoot":"","sources":["../../bin/zintrust.ts"],"names":[],"mappings":";AAEA;;;;;GAKG;AA8FH,OAAO,EAAE,CAAC"}
package/bin/zintrust.js CHANGED
@@ -5,8 +5,63 @@
5
5
  * Usage: zintrust [command] [options]
6
6
  * Shortcuts: zin, z
7
7
  */
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ const loadPackageVersionFast = () => {
12
+ try {
13
+ const here = path.dirname(fileURLToPath(import.meta.url));
14
+ const packagePath = path.join(here, '../package.json');
15
+ const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
16
+ return typeof packageJson.version === 'string' ? packageJson.version : '0.0.0';
17
+ }
18
+ catch {
19
+ return '0.0.0';
20
+ }
21
+ };
22
+ const stripLeadingScriptArg = (rawArgs) => {
23
+ if (rawArgs.length === 0)
24
+ return rawArgs;
25
+ const first = rawArgs[0];
26
+ const looksLikeScript = typeof first === 'string' && (first.endsWith('.ts') || first.endsWith('.js'));
27
+ return looksLikeScript ? rawArgs.slice(1) : rawArgs;
28
+ };
29
+ const getArgsFromProcess = () => {
30
+ const rawArgs = process.argv.slice(2);
31
+ return { rawArgs, args: stripLeadingScriptArg(rawArgs) };
32
+ };
33
+ const isVersionRequest = (args) => {
34
+ return args.includes('-v') || args.includes('--version');
35
+ };
36
+ const shouldDebugArgs = (rawArgs) => {
37
+ return process.env['ZINTRUST_CLI_DEBUG_ARGS'] === '1' && rawArgs.includes('--verbose');
38
+ };
39
+ const handleCliFatal = async (error, context) => {
40
+ try {
41
+ const { Logger } = await import('../src/config/logger');
42
+ Logger.error(context, error);
43
+ }
44
+ catch {
45
+ // best-effort logging
46
+ }
47
+ try {
48
+ const { ErrorHandler } = await import('../src/cli/ErrorHandler');
49
+ ErrorHandler.handle(error);
50
+ }
51
+ catch {
52
+ // best-effort error handling
53
+ }
54
+ process.exit(1);
55
+ };
8
56
  async function main() {
9
57
  try {
58
+ // Fast path: print version and exit without bootstrapping the CLI.
59
+ // This keeps `zin -v` / `zin --version` snappy and avoids any debug output.
60
+ const { rawArgs: _rawArgs0, args: args0 } = getArgsFromProcess();
61
+ if (isVersionRequest(args0)) {
62
+ process.stdout.write(`${loadPackageVersionFast()}\n`);
63
+ return;
64
+ }
10
65
  const { EnvFileLoader } = await import('../src/cli/utils/EnvFileLoader');
11
66
  EnvFileLoader.ensureLoaded();
12
67
  const { CLI } = await import('../src/cli/CLI');
@@ -14,8 +69,8 @@ async function main() {
14
69
  // When executing via tsx (e.g. `npx tsx bin/zin.ts ...`), the script path can
15
70
  // appear as the first element of `process.argv.slice(2)`. Commander expects
16
71
  // args to start at the command name, so we strip a leading script path if present.
17
- const rawArgs = process.argv.slice(2);
18
- if (process.env['ZINTRUST_CLI_DEBUG_ARGS'] === '1') {
72
+ const { rawArgs, args } = getArgsFromProcess();
73
+ if (shouldDebugArgs(rawArgs)) {
19
74
  try {
20
75
  process.stderr.write(`[zintrust-cli] process.argv=${JSON.stringify(process.argv)}\n`);
21
76
  process.stderr.write(`[zintrust-cli] rawArgs=${JSON.stringify(rawArgs)}\n`);
@@ -24,45 +79,12 @@ async function main() {
24
79
  // ignore
25
80
  }
26
81
  }
27
- const args = rawArgs.length > 0 &&
28
- (rawArgs[0]?.endsWith('.ts') === true || rawArgs[0]?.endsWith('.js') === true)
29
- ? rawArgs.slice(1)
30
- : rawArgs;
31
82
  await cli.run(args);
32
83
  }
33
84
  catch (error) {
34
- try {
35
- const { Logger } = await import('../src/config/logger');
36
- Logger.error('CLI execution failed', error);
37
- }
38
- catch {
39
- // best-effort logging
40
- }
41
- try {
42
- const { ErrorHandler } = await import('../src/cli/ErrorHandler');
43
- ErrorHandler.handle(error);
44
- }
45
- catch {
46
- // best-effort error handling
47
- }
48
- process.exit(1);
85
+ await handleCliFatal(error, 'CLI execution failed');
49
86
  }
50
87
  }
51
88
  await main().catch(async (error) => {
52
- try {
53
- const { Logger } = await import('../src/config/logger');
54
- Logger.error('CLI fatal error', error);
55
- }
56
- catch {
57
- // best-effort logging
58
- }
59
- try {
60
- const { ErrorHandler } = await import('../src/cli/ErrorHandler');
61
- ErrorHandler.handle(error);
62
- }
63
- catch {
64
- // best-effort error handling
65
- }
66
- process.exit(1);
89
+ await handleCliFatal(error, 'CLI fatal error');
67
90
  });
68
- export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zintrust/core",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Production-grade TypeScript backend framework for JavaScript",
5
5
  "homepage": "https://zintrust.com",
6
6
  "repository": {
@@ -13,6 +13,20 @@
13
13
  "type": "module",
14
14
  "main": "src/index.js",
15
15
  "types": "src/index.d.ts",
16
+ "dependencies": {
17
+ "bcrypt": "^6.0.0",
18
+ "better-sqlite3": "^12.5.0",
19
+ "chalk": "^5.6.2",
20
+ "commander": "^14.0.2",
21
+ "inquirer": "^13.1.0",
22
+ "jsonwebtoken": "^9.0.3",
23
+ "tsx": "^4.21.0"
24
+ },
25
+ "overrides": {
26
+ "node-forge": "1.3.3",
27
+ "cross-spawn": "^7.0.5",
28
+ "glob": "^11.1.0"
29
+ },
16
30
  "bin": {
17
31
  "zintrust": "bin/zintrust.js",
18
32
  "zin": "bin/zin.js",
@@ -17,7 +17,7 @@ export const BaseCommand = Object.freeze({
17
17
  const getCommand = () => {
18
18
  const command = new Command(config.name);
19
19
  command.description(config.description);
20
- command.option('-v, --verbose', 'Enable verbose output');
20
+ command.option('--verbose', 'Enable verbose output');
21
21
  // Add custom options
22
22
  if (config.addOptions) {
23
23
  config.addOptions(command);
@@ -1 +1 @@
1
- {"version":3,"file":"CLI.d.ts","sourceRoot":"","sources":["../../../src/cli/CLI.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAyBH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAIpC,MAAM,WAAW,IAAI;IACnB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,UAAU,IAAI,OAAO,CAAC;CACvB;AAqLD;;;;;;;GAOG;AACH,eAAO,MAAM,GAAG;cACJ,IAAI;EAed,CAAC"}
1
+ {"version":3,"file":"CLI.d.ts","sourceRoot":"","sources":["../../../src/cli/CLI.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAyBH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAIpC,MAAM,WAAW,IAAI;IACnB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,UAAU,IAAI,OAAO,CAAC;CACvB;AAsLD;;;;;;;GAOG;AACH,eAAO,MAAM,GAAG;cACJ,IAAI;EAed,CAAC"}
package/src/cli/CLI.js CHANGED
@@ -159,12 +159,13 @@ const handleExecutionError = (error, version, log = true) => {
159
159
  */
160
160
  const runCLI = async (program, version, args) => {
161
161
  try {
162
- // Always show banner
163
- ErrorHandler.banner(version);
164
- // If version is requested, we've already shown the banner which includes the version.
162
+ // If version is requested, let Commander print it (no banner, fast/clean output).
165
163
  if (args.includes('-v') || args.includes('--version')) {
164
+ await program.parseAsync(['node', 'zintrust', ...args]);
166
165
  return;
167
166
  }
167
+ // Always show banner for normal commands
168
+ ErrorHandler.banner(version);
168
169
  // Show help if no arguments provided
169
170
  if (args.length === 0) {
170
171
  program.help();
@@ -3,7 +3,7 @@
3
3
  * Handles creation of new Zintrust projects
4
4
  */
5
5
  import { CommandOptions, IBaseCommand } from '../BaseCommand';
6
- type TemplateType = 'basic' | 'api' | 'microservice';
6
+ type TemplateType = 'basic' | 'api' | 'microservice' | 'fullstack';
7
7
  type DatabaseType = 'sqlite' | 'mysql' | 'postgresql' | 'mongodb';
8
8
  interface NewProjectConfigResult {
9
9
  template: TemplateType;
@@ -19,8 +19,9 @@ interface INewCommand extends IBaseCommand {
19
19
  getQuestions(name: string, defaults: NewProjectConfigResult): InquirerQuestion[];
20
20
  getSafeEnv(): NodeJS.ProcessEnv;
21
21
  getGitBinary(): string;
22
- runScaffolding(name: string, config: NewProjectConfigResult, overwrite?: boolean): Promise<unknown>;
23
- initializeGit(name: string): void;
22
+ runScaffolding(basePath: string, name: string, config: NewProjectConfigResult, overwrite?: boolean): Promise<unknown>;
23
+ initializeGit(projectPath: string): void;
24
+ promptForPackageManager(defaultPm: string): Promise<string | null>;
24
25
  }
25
26
  /**
26
27
  * New Command Factory
@@ -1 +1 @@
1
- {"version":3,"file":"NewCommand.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/NewCommand.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAe,cAAc,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAY7E,KAAK,YAAY,GAAG,OAAO,GAAG,KAAK,GAAG,cAAc,CAAC;AACrD,KAAK,YAAY,GAAG,QAAQ,GAAG,OAAO,GAAG,YAAY,GAAG,SAAS,CAAC;AAYlE,UAAU,sBAAsB;IAC9B,QAAQ,EAAE,YAAY,CAAC;IACvB,QAAQ,EAAE,YAAY,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,KAAK,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAiMhD,UAAU,WAAY,SAAQ,YAAY;IACxC,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;IACxF,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;IACzF,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,sBAAsB,GAAG,gBAAgB,EAAE,CAAC;IACjF,UAAU,IAAI,MAAM,CAAC,UAAU,CAAC;IAChC,YAAY,IAAI,MAAM,CAAC;IACvB,cAAc,CACZ,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,sBAAsB,EAC9B,SAAS,CAAC,EAAE,OAAO,GAClB,OAAO,CAAC,OAAO,CAAC,CAAC;IACpB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAwID;;;GAGG;AACH,eAAO,MAAM,UAAU;IACrB;;OAEG;cACO,WAAW;EAGrB,CAAC"}
1
+ {"version":3,"file":"NewCommand.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/NewCommand.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAe,cAAc,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAY7E,KAAK,YAAY,GAAG,OAAO,GAAG,KAAK,GAAG,cAAc,GAAG,WAAW,CAAC;AACnE,KAAK,YAAY,GAAG,QAAQ,GAAG,OAAO,GAAG,YAAY,GAAG,SAAS,CAAC;AAYlE,UAAU,sBAAsB;IAC9B,QAAQ,EAAE,YAAY,CAAC;IACvB,QAAQ,EAAE,YAAY,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,KAAK,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAsOhD,UAAU,WAAY,SAAQ,YAAY;IACxC,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;IACxF,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;IACzF,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,sBAAsB,GAAG,gBAAgB,EAAE,CAAC;IACjF,UAAU,IAAI,MAAM,CAAC,UAAU,CAAC;IAChC,YAAY,IAAI,MAAM,CAAC;IACvB,cAAc,CACZ,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,sBAAsB,EAC9B,SAAS,CAAC,EAAE,OAAO,GAClB,OAAO,CAAC,OAAO,CAAC,CAAC;IACpB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IACzC,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CACpE;AAmOD;;;GAGG;AACH,eAAO,MAAM,UAAU;IACrB;;OAEG;cACO,WAAW;EAGrB,CAAC"}
@@ -70,14 +70,14 @@ const toConfigResult = (config) => ({
70
70
  const getQuestions = (name, defaults) => {
71
71
  return [
72
72
  {
73
- type: 'list',
73
+ type: 'rawlist',
74
74
  name: 'template',
75
75
  message: 'Project template:',
76
- choices: ['basic', 'api', 'microservice'],
76
+ choices: ['basic', 'api', 'microservice', 'fullstack'],
77
77
  default: defaults.template,
78
78
  },
79
79
  {
80
- type: 'list',
80
+ type: 'rawlist',
81
81
  name: 'database',
82
82
  message: 'Database driver:',
83
83
  choices: ['sqlite', 'mysql', 'postgresql', 'mongodb'],
@@ -150,6 +150,27 @@ const isFailureResult = (result) => {
150
150
  const maybe = result;
151
151
  return maybe.success === false;
152
152
  };
153
+ const promptForPackageManager = async (defaultPm) => {
154
+ const choices = [
155
+ { name: 'npm', value: 'npm' },
156
+ { name: 'yarn', value: 'yarn' },
157
+ { name: 'pnpm', value: 'pnpm' },
158
+ { name: 'bun', value: 'bun' },
159
+ { name: 'Skip installation', value: null },
160
+ ];
161
+ const answer = await PromptHelper.prompt([
162
+ {
163
+ type: 'rawlist',
164
+ name: 'packageManager',
165
+ message: 'Select package manager for dependency installation:',
166
+ choices,
167
+ default: defaultPm,
168
+ },
169
+ ]);
170
+ if (typeof answer !== 'object' || answer === null)
171
+ return null;
172
+ return answer['packageManager'] ?? null;
173
+ };
153
174
  const installDependencies = async (projectPath, log, packageManager, force = false) => {
154
175
  // Respect CI by default — avoid network installs in CI unless explicitly allowed
155
176
  const isCi = Boolean(process.env['CI']);
@@ -158,6 +179,10 @@ const installDependencies = async (projectPath, log, packageManager, force = fal
158
179
  log.info('Skipping automatic dependency installation in CI environment.');
159
180
  return;
160
181
  }
182
+ if (packageManager === null) {
183
+ log.info('⏭️ Skipping dependency installation (not selected).');
184
+ return;
185
+ }
161
186
  log.info('📦 Installing dependencies (this may take a minute)...');
162
187
  const pm = packageManager ?? resolvePackageManager();
163
188
  const args = ['install'];
@@ -175,7 +200,7 @@ const installDependencies = async (projectPath, log, packageManager, force = fal
175
200
  };
176
201
  const addOptions = (command) => {
177
202
  command.argument('<name>', 'Project name');
178
- command.option('--template <type>', 'Project template (basic, api, microservice)', 'basic');
203
+ command.option('--template <type>', 'Project template (basic, api, microservice, fullstack)', 'basic');
179
204
  command.option('--database <type>', 'Database driver (sqlite, mysql, postgresql)', 'sqlite');
180
205
  command.option('--port <number>', 'Default port number', '3003');
181
206
  command.option('--author <name>', 'Project author');
@@ -189,31 +214,90 @@ const addOptions = (command) => {
189
214
  command.option('--force', 'Overwrite existing directory');
190
215
  command.option('--overwrite', 'Overwrite existing directory');
191
216
  };
192
- const executeNewCommand = async (options, command) => {
193
- try {
194
- const argName = options.args?.[0];
195
- const projectName = argName ?? (await PromptHelper.projectName('my-zintrust-app', true));
196
- if (!projectName)
197
- throw ErrorFactory.createCliError('Project name is required');
198
- const config = await command.getProjectConfig(projectName, options);
199
- command.info(chalk.bold(`\n🚀 Creating new Zintrust project in ${projectName}...\n`));
200
- const overwrite = options['overwrite'] === true || options['force'] === true ? true : undefined;
201
- const result = await command.runScaffolding(projectName, config, overwrite);
202
- if (isFailureResult(result)) {
203
- throw ErrorFactory.createCliError(result.message ?? 'Project scaffolding failed', result);
204
- }
205
- if (options['git'] !== false) {
206
- command.initializeGit(projectName);
217
+ const resolveProjectName = async (options) => {
218
+ const argName = options.args?.[0];
219
+ const projectName = argName ?? (await PromptHelper.projectName('my-zintrust-app', true));
220
+ if (!projectName)
221
+ throw ErrorFactory.createCliError('Project name is required');
222
+ return projectName;
223
+ };
224
+ const resolveProjectTarget = async (options) => {
225
+ const input = await resolveProjectName(options);
226
+ const isPathLike = input.includes('/') || input.includes('\\');
227
+ if (!isPathLike) {
228
+ return {
229
+ basePath: process.cwd(),
230
+ name: input,
231
+ projectPath: path.resolve(process.cwd(), input),
232
+ display: input,
233
+ cdPath: input,
234
+ };
235
+ }
236
+ const projectPath = path.resolve(process.cwd(), input);
237
+ const name = path.basename(projectPath);
238
+ const basePath = path.dirname(projectPath);
239
+ const cdPath = input.startsWith('/') ? projectPath : input;
240
+ return {
241
+ basePath,
242
+ name,
243
+ projectPath,
244
+ display: input,
245
+ cdPath,
246
+ };
247
+ };
248
+ const createProject = async (target, options, command) => {
249
+ const config = await command.getProjectConfig(target.name, options);
250
+ command.info(chalk.bold(`\n🚀 Creating new Zintrust project in ${target.display}...\n`));
251
+ const overwrite = options['overwrite'] === true || options['force'] === true ? true : undefined;
252
+ const result = await command.runScaffolding(target.basePath, target.name, config, overwrite);
253
+ if (isFailureResult(result)) {
254
+ throw ErrorFactory.createCliError(result.message ?? 'Project scaffolding failed', result);
255
+ }
256
+ };
257
+ const maybeInitializeGit = (options, command, target) => {
258
+ if (options['git'] !== false) {
259
+ command.initializeGit(target.projectPath);
260
+ }
261
+ };
262
+ const maybeInstallDependencies = async (options, command, target) => {
263
+ if (options['install'] === false)
264
+ return;
265
+ const projectPath = target.projectPath;
266
+ let pm = options['packageManager'] ??
267
+ options['package-manager'];
268
+ // If no package manager specified and not in non-interactive mode, prompt user
269
+ if (pm === undefined && options['interactive'] !== false && options['no-interactive'] !== true) {
270
+ const defaultPm = resolvePackageManager();
271
+ const selectedPm = await command.promptForPackageManager(defaultPm);
272
+ if (selectedPm === null) {
273
+ command.info('⏭️ Skipping dependency installation.');
274
+ pm = '__skip__'; // Signal to skip installation
207
275
  }
208
- if (options['install'] !== false) {
209
- const projectPath = path.resolve(process.cwd(), projectName);
210
- const pm = options['packageManager'] ??
211
- options['package-manager'];
212
- const force = options['install'] === true;
213
- await installDependencies(projectPath, command, pm, force);
276
+ else {
277
+ pm = selectedPm;
214
278
  }
215
- command.success(`\n✨ Project ${projectName} created successfully!`);
216
- command.info(`\nNext steps:\n cd ${projectName}\n npm run dev\n`);
279
+ }
280
+ const force = options['install'] === true;
281
+ if (pm === '__skip__')
282
+ return;
283
+ const effectivePm = pm ?? resolvePackageManager();
284
+ await installDependencies(projectPath, command, pm, force);
285
+ return effectivePm;
286
+ };
287
+ const executeNewCommand = async (options, command) => {
288
+ try {
289
+ const target = await resolveProjectTarget(options);
290
+ await createProject(target, options, command);
291
+ maybeInitializeGit(options, command, target);
292
+ const installedWithPm = await maybeInstallDependencies(options, command, target);
293
+ command.success(`\n✨ Project ${target.name} created successfully!`);
294
+ const optPm = options['packageManager'] ??
295
+ options['package-manager'];
296
+ const effectivePm = installedWithPm ?? optPm ?? resolvePackageManager();
297
+ const runDevCmd = effectivePm === 'yarn' || effectivePm === 'pnpm'
298
+ ? `${effectivePm} dev`
299
+ : `${effectivePm} run dev`;
300
+ command.info(`\nNext steps:\n cd ${target.cdPath}\n ${runDevCmd}\n`);
217
301
  }
218
302
  catch (error) {
219
303
  throw ErrorFactory.createCliError(`Project creation failed: ${extractErrorMessage(error)}`, error);
@@ -257,8 +341,8 @@ const createNewCommandInstance = () => {
257
341
  getProjectConfig: async (name, options) => {
258
342
  return commandInstance.promptForConfig(name, options);
259
343
  },
260
- runScaffolding: async (name, config, overwrite) => {
261
- return ProjectScaffolder.scaffold(process.cwd(), {
344
+ runScaffolding: async (basePath, name, config, overwrite) => {
345
+ return ProjectScaffolder.scaffold(basePath, {
262
346
  name,
263
347
  force: overwrite === true,
264
348
  template: config.template,
@@ -268,12 +352,14 @@ const createNewCommandInstance = () => {
268
352
  description: config.description,
269
353
  });
270
354
  },
271
- initializeGit: (name) => {
272
- const projectPath = path.resolve(process.cwd(), name);
355
+ initializeGit: (projectPath) => {
273
356
  if (checkGitInstalled()) {
274
357
  initializeGitRepo(projectPath, commandInstance);
275
358
  }
276
359
  },
360
+ promptForPackageManager: async (defaultPm) => {
361
+ return promptForPackageManager(defaultPm);
362
+ },
277
363
  execute: async (options) => {
278
364
  await executeNewCommand(options, commandInstance);
279
365
  },
@@ -1 +1 @@
1
- {"version":3,"file":"ProjectScaffolder.d.ts","sourceRoot":"","sources":["../../../../src/cli/scaffolding/ProjectScaffolder.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAOH,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;AAqWD,wBAAgB,qBAAqB,IAAI,MAAM,EAAE,CAEhD;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAMrE;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG;IAChE,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB,CAsBA;AAwID;;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;AAOH,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;AAqWD,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;AAwID;;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"}
@@ -317,7 +317,21 @@ export function getTemplate(name) {
317
317
  if (!fallback)
318
318
  return undefined;
319
319
  const disk = loadTemplateFromDisk(name, fallback);
320
- return disk ?? fallback;
320
+ if (disk)
321
+ return disk;
322
+ // If we don't have a dedicated disk template yet (e.g. fullstack/api/microservice),
323
+ // fall back to the starter project's file set so generated projects are runnable.
324
+ if (name !== 'basic') {
325
+ const basicFallback = TEMPLATE_MAP.get('basic');
326
+ const basicDisk = basicFallback ? loadTemplateFromDisk('basic', basicFallback) : undefined;
327
+ if (basicDisk) {
328
+ return {
329
+ ...fallback,
330
+ files: basicDisk.files,
331
+ };
332
+ }
333
+ }
334
+ return fallback;
321
335
  }
322
336
  export function validateOptions(options) {
323
337
  const errors = [];
package/src/index.d.ts CHANGED
@@ -24,6 +24,10 @@ export { QueryBuilder } from './orm/QueryBuilder';
24
24
  export type { IRelationship } from './orm/Relationships';
25
25
  export { Router } from './routing/Router';
26
26
  export type { IRouter } from './routing/Router';
27
+ export { delay, ensureDirSafe } from './common/index';
28
+ export { HttpClient } from './tools/http/Http';
29
+ export type { IHttpRequest, IHttpResponse } from './tools/http/Http';
30
+ export type { DatabaseConfig, ID1Database } from './orm/DatabaseAdapter';
27
31
  export { MemoryProfiler } from './profiling/MemoryProfiler';
28
32
  export { N1Detector } from './profiling/N1Detector';
29
33
  export { QueryLogger } from './profiling/QueryLogger';
@@ -32,12 +36,13 @@ export type { MemoryDelta, MemorySnapshot, N1Pattern, ProfileReport, QueryLogEnt
32
36
  export { ValidationError } from './validation/ValidationError';
33
37
  export type { FieldError } from './validation/ValidationError';
34
38
  export { Schema, Validator } from './validation/Validator';
39
+ export type { ISchema, SchemaType } from './validation/Validator';
35
40
  export { CsrfTokenManager } from './security/CsrfTokenManager';
36
- export type { CsrfTokenData } from './security/CsrfTokenManager';
41
+ export type { CsrfTokenData, CsrfTokenManagerType, ICsrfTokenManager, } from './security/CsrfTokenManager';
37
42
  export { Encryptor } from './security/Encryptor';
38
43
  export { Hash } from './security/Hash';
39
44
  export { JwtManager } from './security/JwtManager';
40
- export type { JwtOptions, JwtPayload } from './security/JwtManager';
45
+ export type { IJwtManager, JwtAlgorithm, JwtManagerType, JwtOptions, JwtPayload, } from './security/JwtManager';
41
46
  export { Xss } from './security/Xss';
42
47
  export { XssProtection } from './security/XssProtection';
43
48
  export { ErrorFactory } from './exceptions/ZintrustError';
@@ -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
@@ -18,6 +18,10 @@ export { Database, resetDatabase, useDatabase } from './orm/Database';
18
18
  export { Model } from './orm/Model';
19
19
  export { QueryBuilder } from './orm/QueryBuilder';
20
20
  export { Router } from './routing/Router';
21
+ // Common
22
+ export { delay, ensureDirSafe } from './common/index';
23
+ // HTTP Client
24
+ export { HttpClient } from './tools/http/Http';
21
25
  // Profiling
22
26
  export { MemoryProfiler } from './profiling/MemoryProfiler';
23
27
  export { N1Detector } from './profiling/N1Detector';
@@ -85,7 +85,10 @@ const syncProjectTemplateDir = (params) => {
85
85
  let updated = 0;
86
86
  let skipped = 0;
87
87
  for (const file of files) {
88
- const baseKey = `${params.baseDirRel}/${file.relPath}`;
88
+ const checksumSaltPart = typeof params.checksumSalt === 'string' && params.checksumSalt.length > 0
89
+ ? `|${params.checksumSalt}`
90
+ : '';
91
+ const baseKey = `${params.baseDirRel}/${file.relPath}${checksumSaltPart}`;
89
92
  const currentHash = hashFile(file.absPath);
90
93
  const storedHash = params.checksums[baseKey];
91
94
  const outRel = `${file.relPath}.tpl`;
@@ -108,6 +111,29 @@ const syncProjectTemplateDir = (params) => {
108
111
  }
109
112
  return { updated, skipped, total: files.length };
110
113
  };
114
+ const rewriteStarterTemplateImports = (relPath, content) => {
115
+ if (!relPath.endsWith('.ts') && !relPath.endsWith('.tsx') && !relPath.endsWith('.mts')) {
116
+ return content;
117
+ }
118
+ // Starter templates should import framework APIs from the public package surface,
119
+ // not from internal path-alias modules that only exist in the framework repo.
120
+ return (content
121
+ .replaceAll("'@routing/Router'", "'@zintrust/core'")
122
+ .replaceAll("'@orm/Database'", "'@zintrust/core'")
123
+ .replaceAll("'@orm/QueryBuilder'", "'@zintrust/core'")
124
+ .replaceAll("'@orm/DatabaseAdapter'", "'@zintrust/core'")
125
+ .replaceAll("'@exceptions/ZintrustError'", "'@zintrust/core'")
126
+ .replaceAll("'@common/index'", "'@zintrust/core'")
127
+ .replaceAll("'@httpClient/Http'", "'@zintrust/core'")
128
+ // Handle double-quoted module specifiers too
129
+ .replaceAll('"@routing/Router"', '"@zintrust/core"')
130
+ .replaceAll('"@orm/Database"', '"@zintrust/core"')
131
+ .replaceAll('"@orm/QueryBuilder"', '"@zintrust/core"')
132
+ .replaceAll('"@orm/DatabaseAdapter"', '"@zintrust/core"')
133
+ .replaceAll('"@exceptions/ZintrustError"', '"@zintrust/core"')
134
+ .replaceAll('"@common/index"', '"@zintrust/core"')
135
+ .replaceAll('"@httpClient/Http"', '"@zintrust/core"'));
136
+ };
111
137
  const syncRegistryMappings = (params) => {
112
138
  let updated = 0;
113
139
  let skipped = 0;
@@ -190,6 +216,8 @@ const syncStarterProjectTemplates = (params) => {
190
216
  baseDirRel: 'src/config',
191
217
  templateDirRel: `${params.projectRoot}/config`,
192
218
  description: 'Starter project config/* (from src/config/*)',
219
+ transformContent: rewriteStarterTemplateImports,
220
+ checksumSalt: 'starter-imports-v1',
193
221
  });
194
222
  const s3 = syncProjectTemplateDir({
195
223
  checksums: params.checksums,
@@ -202,6 +230,8 @@ const syncStarterProjectTemplates = (params) => {
202
230
  baseDirRel: 'routes',
203
231
  templateDirRel: `${params.projectRoot}/routes`,
204
232
  description: 'Starter project routes/*',
233
+ transformContent: rewriteStarterTemplateImports,
234
+ checksumSalt: 'starter-imports-v1',
205
235
  });
206
236
  const s5 = syncStarterEnvTemplate({
207
237
  checksums: params.checksums,
@@ -1 +1 @@
1
- {"version":3,"file":"XssProtection.d.ts","sourceRoot":"","sources":["../../../src/security/XssProtection.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAoSH;;GAEG;AACH,eAAO,MAAM,UAAU,GAAI,KAAK,OAAO,KAAG,MAGzC,CAAC;AAEF,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IACjC,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAChC,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAAC;CAClC;AAED;;;;GAIG;AACH,eAAO,MAAM,aAAa,EAAE,cAO1B,CAAC"}
1
+ {"version":3,"file":"XssProtection.d.ts","sourceRoot":"","sources":["../../../src/security/XssProtection.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA2SH;;GAEG;AACH,eAAO,MAAM,UAAU,GAAI,KAAK,OAAO,KAAG,MAGzC,CAAC;AAEF,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IACjC,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAChC,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAAC;CAClC;AAED;;;;GAIG;AACH,eAAO,MAAM,aAAa,EAAE,cAO1B,CAAC"}
@@ -36,8 +36,12 @@ const sanitizeHtml = (html) => {
36
36
  // Remove iframe, object, embed, and base tags
37
37
  sanitized = sanitized.replaceAll(/<(?:iframe|object|embed|base)\b[\s\S]*?>/gi, '');
38
38
  sanitized = sanitized.replaceAll(/<\/(?:iframe|object|embed|base)>/gi, '');
39
- // Remove event handlers (on*)
40
- sanitized = sanitized.replaceAll(/\bon\w+\s*=\s*(?:'[^']*'|"[^"]*"|`[^`]*`|[^\s>]*)/gi, '');
39
+ // Remove event handlers (on*). Re-apply until stable to avoid incomplete multi-character sanitization.
40
+ let previousSanitized;
41
+ do {
42
+ previousSanitized = sanitized;
43
+ sanitized = sanitized.replaceAll(/\bon\w+\s*=\s*(?:'[^']*'|"[^"]*"|`[^`]*`|[^\s>]*)/gi, '');
44
+ } while (sanitized !== previousSanitized);
41
45
  // Remove dangerous protocols in URL-bearing attributes.
42
46
  // This uses the same protocol normalization logic as encodeHref to prevent obfuscations like:
43
47
  // href="jav&#x61;script:..." or href="java\nscript:..." or href="%6a%61..."
@@ -5,7 +5,7 @@
5
5
  * This module imports Node built-ins and should be loaded only in Node environments.
6
6
  */
7
7
 
8
- import { ensureDirSafe } from '@common/index';
8
+ import { ensureDirSafe } from '@zintrust/core';
9
9
  import { Env } from '@config/env';
10
10
  import * as fs from '@node-singletons/fs';
11
11
  import * as path from '@node-singletons/path';
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { Logger } from '@config/logger';
8
- import { ErrorFactory } from '@exceptions/ZintrustError';
8
+ import { ErrorFactory } from '@zintrust/core';
9
9
 
10
10
  export interface CloudflareKV {
11
11
  get(key: string): Promise<string | null>;
@@ -1,5 +1,5 @@
1
1
  import { appConfig } from '@config/app';
2
- import { ErrorFactory } from '@exceptions/ZintrustError';
2
+ import { ErrorFactory } from '@zintrust/core';
3
3
 
4
4
  export type StartupConfigValidationError = {
5
5
  key: string;
@@ -5,7 +5,7 @@
5
5
  * This keeps runtime-specific globals out of adapters/drivers.
6
6
  */
7
7
 
8
- import type { DatabaseConfig, ID1Database } from '@orm/DatabaseAdapter';
8
+ import type { DatabaseConfig, ID1Database } from '@zintrust/core';
9
9
 
10
10
  export type WorkersEnv = Record<string, unknown>;
11
11
 
@@ -9,10 +9,10 @@
9
9
  * - HTTP_LOG_AUTH_TOKEN (optional)
10
10
  */
11
11
 
12
- import { delay } from '@common/index';
12
+ import { delay } from '@zintrust/core';
13
13
  import { Env } from '@config/env';
14
- import { ErrorFactory } from '@exceptions/ZintrustError';
15
- import { HttpClient } from '@httpClient/Http';
14
+ import { ErrorFactory } from '@zintrust/core';
15
+ import { HttpClient } from '@zintrust/core';
16
16
 
17
17
  export type HttpLogEvent = {
18
18
  timestamp: string;
@@ -10,8 +10,8 @@
10
10
  */
11
11
 
12
12
  import { Env } from '@config/env';
13
- import { ErrorFactory } from '@exceptions/ZintrustError';
14
- import { HttpClient } from '@httpClient/Http';
13
+ import { ErrorFactory } from '@zintrust/core';
14
+ import { HttpClient } from '@zintrust/core';
15
15
 
16
16
  export type SlackLogEvent = {
17
17
  timestamp: string;
@@ -7,7 +7,7 @@
7
7
  import { appConfig } from '@config/app';
8
8
  import { Env } from '@config/env';
9
9
  import { Logger } from '@config/logger';
10
- import { ErrorFactory } from '@exceptions/ZintrustError';
10
+ import { ErrorFactory } from '@zintrust/core';
11
11
 
12
12
  /**
13
13
  * Helper to warn about missing secrets
@@ -11,7 +11,7 @@
11
11
  "type-check": "tsc --noEmit"
12
12
  },
13
13
  "dependencies": {
14
- "@zintrust/core": "^0.1.1"
14
+ "@zintrust/core": "^0.1.4"
15
15
  },
16
16
  "devDependencies": {
17
17
  "tsx": "^4.21.0",
@@ -7,7 +7,8 @@ import { UserController } from '@app/Controllers/UserController';
7
7
  import { Env } from '@config/env';
8
8
  import { registerBroadcastRoutes } from '@routes/broadcast';
9
9
  import { registerHealthRoutes } from '@routes/health';
10
- import { type IRouter, Router } from '@routing/Router';
10
+ import { registerStorageRoutes } from '@routes/storage';
11
+ import { type IRouter, Router } from '@zintrust/core';
11
12
 
12
13
  export function registerRoutes(router: IRouter): void {
13
14
  const userController = UserController.create();
@@ -23,6 +24,7 @@ function registerPublicRoutes(router: IRouter): void {
23
24
  registerRootRoute(router);
24
25
  registerHealthRoutes(router);
25
26
  registerBroadcastRoutes(router);
27
+ registerStorageRoutes(router);
26
28
  }
27
29
 
28
30
  function registerRootRoute(router: IRouter): void {
@@ -5,7 +5,7 @@
5
5
  * Provider setup and secret provisioning remain CLI-only.
6
6
  */
7
7
 
8
- import { type IRouter, Router } from '@routing/Router';
8
+ import { type IRouter, Router } from '@zintrust/core';
9
9
 
10
10
  export function registerBroadcastRoutes(router: IRouter): void {
11
11
  Router.get(router, '/broadcast/health', async (_req, res) => {
@@ -7,9 +7,9 @@ import { appConfig } from '@/config';
7
7
  import { RuntimeHealthProbes } from '@/health/RuntimeHealthProbes';
8
8
  import { Env } from '@config/env';
9
9
  import { Logger } from '@config/logger';
10
- import { useDatabase } from '@orm/Database';
11
- import { QueryBuilder } from '@orm/QueryBuilder';
12
- import { type IRouter, Router } from '@routing/Router';
10
+ import { useDatabase } from '@zintrust/core';
11
+ import { QueryBuilder } from '@zintrust/core';
12
+ import { type IRouter, Router } from '@zintrust/core';
13
13
 
14
14
  export function registerHealthRoutes(router: IRouter): void {
15
15
  registerHealthRoute(router);
@@ -0,0 +1,42 @@
1
+ import { HTTP_HEADERS } from '@config/constants';
2
+ import { Env } from '@config/env';
3
+ import { type IRouter, Router } from '@zintrust/core';
4
+ import { LocalSignedUrl } from '@storage/LocalSignedUrl';
5
+ import { Storage } from '@storage/index';
6
+
7
+ export function registerStorageRoutes(router: IRouter): void {
8
+ Router.get(router, '/storage/download', async (req, res) => {
9
+ const tokenRaw = req.getQueryParam('token');
10
+ const token = typeof tokenRaw === 'string' ? tokenRaw : '';
11
+
12
+ if (token.trim() === '') {
13
+ res.setStatus(400).json({ message: 'Missing token' });
14
+ return;
15
+ }
16
+
17
+ const appKey = Env.get('APP_KEY', '');
18
+ if (appKey.trim() === '') {
19
+ res.setStatus(500).json({ message: 'Storage signing is not configured' });
20
+ return;
21
+ }
22
+
23
+ try {
24
+ const payload = LocalSignedUrl.verifyToken(token, appKey);
25
+
26
+ // Only local disk is supported by this route.
27
+ if (payload.disk !== 'local') {
28
+ res.setStatus(400).json({ message: 'Unsupported disk' });
29
+ return;
30
+ }
31
+
32
+ const contents = await Storage.get('local', payload.key);
33
+
34
+ res.setHeader(HTTP_HEADERS.CONTENT_TYPE, 'application/octet-stream');
35
+ res.setStatus(200).send(contents);
36
+ } catch {
37
+ res.setStatus(403).json({ message: 'Invalid or expired token' });
38
+ }
39
+ });
40
+ }
41
+
42
+ export default registerStorageRoutes;
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "compilerOptions": {
3
+ "baseUrl": ".",
3
4
  "outDir": "./dist",
4
5
  "rootDir": "./",
5
6
  "module": "ESNext",
@@ -10,9 +11,22 @@
10
11
  "paths": {
11
12
  "@/*": ["./src/*"],
12
13
  "@app/*": ["./app/*"],
14
+ "@toolkit/*": ["./app/Toolkit/*"],
13
15
  "@config/*": ["./config/*"],
14
16
  "@routes/*": ["./routes/*"],
15
- "@database/*": ["./database/*"]
17
+ "@database/*": ["./database/*"],
18
+
19
+ "@tools/*": ["./src/tools/*"],
20
+ "@httpClient/*": ["./src/tools/http/*"],
21
+ "@templates": ["./src/tools/templates/index.ts"],
22
+ "@templates/*": ["./src/tools/templates/*"],
23
+ "@mail/*": ["./src/tools/mail/*"],
24
+ "@storage": ["./src/tools/storage/index.ts"],
25
+ "@storage/*": ["./src/tools/storage/*"],
26
+ "@drivers/*": ["./src/tools/storage/drivers/*"],
27
+ "@notification/*": ["./src/tools/notification/*"],
28
+ "@broadcast/*": ["./src/tools/broadcast/*"],
29
+ "@queue/*": ["./src/tools/queue/*"]
16
30
  }
17
31
  },
18
32
  "include": ["src/**/*", "app/**/*", "routes/**/*", "database/**/*", "config/**/*"],
@@ -1,6 +1,5 @@
1
1
  /**
2
2
  * Http Client - Fluent HTTP request builder
3
- * Laravel-style HTTP client for making authenticated requests
4
3
  *
5
4
  * Usage:
6
5
  * await HttpClient.get('https://api.example.com/users').withAuth(token).send();
@@ -23,7 +22,6 @@ export interface IHttpRequest {
23
22
  }
24
23
  /**
25
24
  * HTTP Client - Sealed namespace for making HTTP requests
26
- * Provides Laravel-style fluent API
27
25
  */
28
26
  export declare const HttpClient: Readonly<{
29
27
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"Http.d.ts","sourceRoot":"","sources":["../../../../src/tools/http/Http.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,EAAsB,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAElF,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAE9D;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,YAAY,CAAC;IACtD,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,YAAY,CAAC;IAC3D,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,YAAY,CAAC;IACnE,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,YAAY,CAAC;IAChE,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,CAAC;IACtC,MAAM,IAAI,YAAY,CAAC;IACvB,MAAM,IAAI,YAAY,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,aAAa,CAAC,CAAC;CAChC;AAiJD;;;GAGG;AACH,eAAO,MAAM,UAAU;IACrB;;OAEG;aACM,MAAM,GAAG,YAAY;IAI9B;;OAEG;cACO,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,YAAY;IAQ/D;;OAEG;aACM,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,YAAY;IAQ9D;;OAEG;eACQ,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,YAAY;IAQhE;;OAEG;gBACS,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,YAAY;EAOjE,CAAC;AAEH,eAAe,UAAU,CAAC"}
1
+ {"version":3,"file":"Http.d.ts","sourceRoot":"","sources":["../../../../src/tools/http/Http.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,EAAsB,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAElF,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAE9D;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,YAAY,CAAC;IACtD,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,YAAY,CAAC;IAC3D,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,YAAY,CAAC;IACnE,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,YAAY,CAAC;IAChE,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,CAAC;IACtC,MAAM,IAAI,YAAY,CAAC;IACvB,MAAM,IAAI,YAAY,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,aAAa,CAAC,CAAC;CAChC;AAiJD;;GAEG;AACH,eAAO,MAAM,UAAU;IACrB;;OAEG;aACM,MAAM,GAAG,YAAY;IAI9B;;OAEG;cACO,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,YAAY;IAQ/D;;OAEG;aACM,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,YAAY;IAQ9D;;OAEG;eACQ,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,YAAY;IAQhE;;OAEG;gBACS,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,YAAY;EAOjE,CAAC;AAEH,eAAe,UAAU,CAAC"}
@@ -1,6 +1,5 @@
1
1
  /**
2
2
  * Http Client - Fluent HTTP request builder
3
- * Laravel-style HTTP client for making authenticated requests
4
3
  *
5
4
  * Usage:
6
5
  * await HttpClient.get('https://api.example.com/users').withAuth(token).send();
@@ -118,7 +117,6 @@ const createRequestBuilder = (method, url, initialBody) => {
118
117
  };
119
118
  /**
120
119
  * HTTP Client - Sealed namespace for making HTTP requests
121
- * Provides Laravel-style fluent API
122
120
  */
123
121
  export const HttpClient = Object.freeze({
124
122
  /**
@@ -0,0 +1,12 @@
1
+ export type LocalSignedUrlPayload = {
2
+ disk: 'local';
3
+ key: string;
4
+ exp: number;
5
+ method: 'GET';
6
+ };
7
+ export declare const LocalSignedUrl: Readonly<{
8
+ createToken(payload: LocalSignedUrlPayload, secret: string): string;
9
+ verifyToken(token: string, secret: string, nowMs?: number): LocalSignedUrlPayload;
10
+ }>;
11
+ export default LocalSignedUrl;
12
+ //# sourceMappingURL=LocalSignedUrl.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LocalSignedUrl.d.ts","sourceRoot":"","sources":["../../../../src/tools/storage/LocalSignedUrl.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,qBAAqB,GAAG;IAClC,IAAI,EAAE,OAAO,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,KAAK,CAAC;CACf,CAAC;AA8DF,eAAO,MAAM,cAAc;yBACJ,qBAAqB,UAAU,MAAM,GAAG,MAAM;uBAyBhD,MAAM,UAAU,MAAM,UAAS,MAAM,GAAgB,qBAAqB;EA2C7F,CAAC;AAEH,eAAe,cAAc,CAAC"}
@@ -0,0 +1,108 @@
1
+ import { ErrorFactory } from '../../exceptions/ZintrustError';
2
+ import { createHmac } from '../../node-singletons/crypto';
3
+ const base64UrlEncode = (value) => {
4
+ const base64 = Buffer.isBuffer(value)
5
+ ? value.toString('base64')
6
+ : Buffer.from(value).toString('base64');
7
+ // replace characters used in regular base64 and remove any trailing '=' padding
8
+ let result = base64.replaceAll('+', '-').replaceAll('/', '_');
9
+ // Remove trailing '=' characters without using a regex to avoid potential super-linear backtracking.
10
+ while (result.endsWith('=')) {
11
+ result = result.slice(0, -1);
12
+ }
13
+ return result;
14
+ };
15
+ const base64UrlDecodeToString = (value) => {
16
+ const padded = value + '==='.slice((value.length + 3) % 4);
17
+ const base64 = padded.replaceAll('-', '+').replaceAll('_', '/');
18
+ return Buffer.from(base64, 'base64').toString('utf8');
19
+ };
20
+ const timingSafeEquals = (a, b) => {
21
+ if (a.length !== b.length)
22
+ return false;
23
+ let result = 0;
24
+ for (let i = 0; i < a.length; i++) {
25
+ result |= (a.codePointAt(i) ?? 0) ^ (b.codePointAt(i) ?? 0);
26
+ }
27
+ return result === 0;
28
+ };
29
+ const assertValidKey = (key) => {
30
+ if (key.trim() === '') {
31
+ throw ErrorFactory.createValidationError('Local signed url: key is required');
32
+ }
33
+ // Hard fail on obvious traversal / absolute paths.
34
+ // Keep this strict; keys should be relative like `uploads/a.png`.
35
+ if (key.startsWith('/') || key.startsWith('\\')) {
36
+ throw ErrorFactory.createValidationError('Local signed url: key must be relative');
37
+ }
38
+ const segments = key.split(/[/\\]+/g);
39
+ if (segments.some((s) => s === '..' || s === '.')) {
40
+ throw ErrorFactory.createValidationError('Local signed url: invalid key');
41
+ }
42
+ if (key.includes('\0')) {
43
+ throw ErrorFactory.createValidationError('Local signed url: invalid key');
44
+ }
45
+ };
46
+ const sign = (payloadEncoded, secret) => {
47
+ if (secret.trim() === '') {
48
+ throw ErrorFactory.createConfigError('Local signed url: signing secret not configured (set APP_KEY)');
49
+ }
50
+ const signature = createHmac('sha256', secret).update(payloadEncoded).digest();
51
+ return base64UrlEncode(signature);
52
+ };
53
+ export const LocalSignedUrl = Object.freeze({
54
+ createToken(payload, secret) {
55
+ assertValidKey(payload.key);
56
+ if (payload.disk !== 'local') {
57
+ throw ErrorFactory.createValidationError('Local signed url: unsupported disk', {
58
+ disk: payload.disk,
59
+ });
60
+ }
61
+ if (payload.method !== 'GET') {
62
+ throw ErrorFactory.createValidationError('Local signed url: unsupported method', {
63
+ method: payload.method,
64
+ });
65
+ }
66
+ if (!Number.isFinite(payload.exp) || payload.exp <= 0) {
67
+ throw ErrorFactory.createValidationError('Local signed url: invalid expiration');
68
+ }
69
+ const payloadEncoded = base64UrlEncode(JSON.stringify(payload));
70
+ const signatureEncoded = sign(payloadEncoded, secret);
71
+ return `${payloadEncoded}.${signatureEncoded}`;
72
+ },
73
+ verifyToken(token, secret, nowMs = Date.now()) {
74
+ if (token.trim() === '') {
75
+ throw ErrorFactory.createValidationError('Local signed url: token is required');
76
+ }
77
+ const parts = token.split('.');
78
+ if (parts.length !== 2) {
79
+ throw ErrorFactory.createValidationError('Local signed url: malformed token');
80
+ }
81
+ const payloadEncoded = parts[0] ?? '';
82
+ const signatureEncoded = parts[1] ?? '';
83
+ const expectedSignature = sign(payloadEncoded, secret);
84
+ if (!timingSafeEquals(signatureEncoded, expectedSignature)) {
85
+ throw ErrorFactory.createSecurityError('Local signed url: invalid signature');
86
+ }
87
+ let payload;
88
+ try {
89
+ payload = JSON.parse(base64UrlDecodeToString(payloadEncoded));
90
+ }
91
+ catch (err) {
92
+ throw ErrorFactory.createValidationError('Local signed url: invalid payload', { error: err });
93
+ }
94
+ const p = payload;
95
+ if (p.disk !== 'local' ||
96
+ typeof p.key !== 'string' ||
97
+ typeof p.exp !== 'number' ||
98
+ p.method !== 'GET') {
99
+ throw ErrorFactory.createValidationError('Local signed url: invalid payload');
100
+ }
101
+ assertValidKey(p.key);
102
+ if (p.exp < nowMs) {
103
+ throw ErrorFactory.createSecurityError('Local signed url: token expired');
104
+ }
105
+ return p;
106
+ },
107
+ });
108
+ export default LocalSignedUrl;
@@ -3,12 +3,13 @@ export type LocalConfig = {
3
3
  url?: string;
4
4
  };
5
5
  export declare const LocalDriver: Readonly<{
6
+ resolveKey(config: LocalConfig, key: string): string;
6
7
  put(config: LocalConfig, key: string, content: string | Buffer): Promise<string>;
7
8
  get(config: LocalConfig, key: string): Promise<Buffer>;
8
9
  exists(config: LocalConfig, key: string): Promise<boolean>;
9
10
  delete(config: LocalConfig, key: string): Promise<void>;
10
11
  url(config: LocalConfig, key: string): string | undefined;
11
- tempUrl(config: LocalConfig, key: string, _options?: {
12
+ tempUrl(config: LocalConfig, key: string, options?: {
12
13
  expiresIn?: number;
13
14
  method?: "GET" | "PUT";
14
15
  }): string;
@@ -1 +1 @@
1
- {"version":3,"file":"Local.d.ts","sourceRoot":"","sources":["../../../../../src/tools/storage/drivers/Local.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,eAAO,MAAM,WAAW;gBACJ,WAAW,OAAO,MAAM,WAAW,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;gBAkBpE,WAAW,OAAO,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;mBASvC,WAAW,OAAO,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;mBAU3C,WAAW,OAAO,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;gBAUjD,WAAW,OAAO,MAAM,GAAG,MAAM,GAAG,SAAS;oBAM/C,WAAW,OACd,MAAM,aACA;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,KAAK,GAAG,KAAK,CAAA;KAAE,GACxD,MAAM;EAST,CAAC;AAEH,eAAe,WAAW,CAAC"}
1
+ {"version":3,"file":"Local.d.ts","sourceRoot":"","sources":["../../../../../src/tools/storage/drivers/Local.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,eAAO,MAAM,WAAW;uBACH,WAAW,OAAO,MAAM,GAAG,MAAM;gBAuBlC,WAAW,OAAO,MAAM,WAAW,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;gBAapE,WAAW,OAAO,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;mBASvC,WAAW,OAAO,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;mBAU3C,WAAW,OAAO,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;gBASjD,WAAW,OAAO,MAAM,GAAG,MAAM,GAAG,SAAS;oBAM/C,WAAW,OACd,MAAM,YACD;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,KAAK,GAAG,KAAK,CAAA;KAAE,GACvD,MAAM;EA4BT,CAAC;AAEH,eAAe,WAAW,CAAC"}
@@ -1,13 +1,28 @@
1
+ import { Env } from '../../../config/env';
1
2
  import { ErrorFactory } from '../../../exceptions/ZintrustError';
2
3
  import { fsPromises as fs } from '../../../node-singletons/fs';
3
4
  import * as path from '../../../node-singletons/path';
5
+ import { LocalSignedUrl } from '../LocalSignedUrl';
4
6
  export const LocalDriver = Object.freeze({
5
- async put(config, key, content) {
6
- const root = config.root;
7
- if (!root || root.trim() === '') {
7
+ resolveKey(config, key) {
8
+ if (!config.root || config.root.trim() === '') {
8
9
  throw ErrorFactory.createConfigError('Local storage root is not configured');
9
10
  }
10
- const fullPath = path.join(root, key);
11
+ if (key.trim() === '') {
12
+ throw ErrorFactory.createValidationError('Local storage: key is required');
13
+ }
14
+ if (key.startsWith('/') || key.startsWith('\\')) {
15
+ throw ErrorFactory.createValidationError('Local storage: key must be relative');
16
+ }
17
+ const segments = key.split(/[/\\]+/g);
18
+ if (segments.some((s) => s === '..' || s === '.')) {
19
+ throw ErrorFactory.createValidationError('Local storage: invalid key');
20
+ }
21
+ const fullPath = path.resolve(path.join(config.root, key));
22
+ return fullPath;
23
+ },
24
+ async put(config, key, content) {
25
+ const fullPath = LocalDriver.resolveKey(config, key);
11
26
  const dir = path.dirname(fullPath);
12
27
  await fs.mkdir(dir, { recursive: true });
13
28
  if (typeof content === 'string') {
@@ -19,7 +34,7 @@ export const LocalDriver = Object.freeze({
19
34
  return fullPath;
20
35
  },
21
36
  async get(config, key) {
22
- const fullPath = path.join(config.root, key);
37
+ const fullPath = LocalDriver.resolveKey(config, key);
23
38
  try {
24
39
  return await fs.readFile(fullPath);
25
40
  }
@@ -28,7 +43,7 @@ export const LocalDriver = Object.freeze({
28
43
  }
29
44
  },
30
45
  async exists(config, key) {
31
- const fullPath = path.join(config.root, key);
46
+ const fullPath = LocalDriver.resolveKey(config, key);
32
47
  try {
33
48
  await fs.access(fullPath);
34
49
  return true;
@@ -38,13 +53,12 @@ export const LocalDriver = Object.freeze({
38
53
  }
39
54
  },
40
55
  async delete(config, key) {
41
- const fullPath = path.join(config.root, key);
56
+ const fullPath = LocalDriver.resolveKey(config, key);
42
57
  try {
43
58
  await fs.unlink(fullPath);
44
59
  }
45
- catch (err) {
60
+ catch {
46
61
  // ignore not found
47
- void err; // NOSONAR
48
62
  }
49
63
  },
50
64
  url(config, key) {
@@ -52,12 +66,24 @@ export const LocalDriver = Object.freeze({
52
66
  return undefined;
53
67
  return `${config.url.replace(/\/$/, '')}/${key}`;
54
68
  },
55
- tempUrl(config, key, _options) {
56
- const url = LocalDriver.url(config, key);
57
- if (url === undefined) {
69
+ tempUrl(config, key, options) {
70
+ if (options?.method === 'PUT') {
71
+ throw ErrorFactory.createValidationError('Local storage: tempUrl does not support PUT');
72
+ }
73
+ if (config?.url === undefined || config.url.trim() === '') {
58
74
  throw ErrorFactory.createConfigError('Local storage: url is not configured (set STORAGE_URL)');
59
75
  }
60
- return url;
76
+ const appKey = Env.get('APP_KEY', '');
77
+ if (appKey.trim() === '') {
78
+ throw ErrorFactory.createConfigError('Local storage: APP_KEY is required for signed tempUrl()');
79
+ }
80
+ // Ensure key is safe before embedding in a signed token.
81
+ LocalDriver.resolveKey(config, key);
82
+ const expiresInMs = Math.max(1, options?.expiresIn ?? 60_000);
83
+ const exp = Date.now() + expiresInMs;
84
+ const token = LocalSignedUrl.createToken({ disk: 'local', key, exp, method: 'GET' }, appKey);
85
+ const baseUrl = config.url.replace(/\/$/, '');
86
+ return `${baseUrl}/download?token=${encodeURIComponent(token)}`;
61
87
  },
62
88
  });
63
89
  export default LocalDriver;
@@ -18,7 +18,7 @@ export declare const Storage: Readonly<{
18
18
  exists(disk: string | undefined, path: string): Promise<boolean>;
19
19
  delete(disk: string | undefined, path: string): Promise<void>;
20
20
  url(disk: string | undefined, path: string): string;
21
- tempUrl(disk: string | undefined, path: string, options?: TempUrlOptions): string;
21
+ tempUrl(disk: string | undefined, path: string, options?: TempUrlOptions): Promise<string>;
22
22
  }>;
23
23
  export default Storage;
24
24
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/tools/storage/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAE/C,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,IAAI,GAAG,KAAK,GAAG,IAAI,CAAC;AAErD,MAAM,MAAM,WAAW,GAAG;IACxB,MAAM,EAAE,OAAO,WAAW,GAAG,OAAO,QAAQ,GAAG,OAAO,QAAQ,GAAG,OAAO,SAAS,CAAC;IAClF,MAAM,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,KAAK,cAAc,GAAG;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,KAAK,GAAG,KAAK,CAAA;CAAE,CAAC;AAsCrE,eAAO,MAAM,OAAO;mBACH,MAAM,GAAG,WAAW;cAqBnB,MAAM,GAAG,SAAS,QAAQ,MAAM,YAAY,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;cAW7E,MAAM,GAAG,SAAS,QAAQ,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;iBAW/C,MAAM,GAAG,SAAS,QAAQ,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;iBASnD,MAAM,GAAG,SAAS,QAAQ,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;cASzD,MAAM,GAAG,SAAS,QAAQ,MAAM,GAAG,MAAM;kBAYrC,MAAM,GAAG,SAAS,QAAQ,MAAM,YAAY,cAAc,GAAG,MAAM;EAiBjF,CAAC;AAEH,eAAe,OAAO,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/tools/storage/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAGxD,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAE/C,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,IAAI,GAAG,KAAK,GAAG,IAAI,CAAC;AAErD,MAAM,MAAM,WAAW,GAAG;IACxB,MAAM,EAAE,OAAO,WAAW,GAAG,OAAO,QAAQ,GAAG,OAAO,QAAQ,GAAG,OAAO,SAAS,CAAC;IAClF,MAAM,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,KAAK,cAAc,GAAG;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,KAAK,GAAG,KAAK,CAAA;CAAE,CAAC;AAsCrE,eAAO,MAAM,OAAO;mBACH,MAAM,GAAG,WAAW;cAqBnB,MAAM,GAAG,SAAS,QAAQ,MAAM,YAAY,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;cAW7E,MAAM,GAAG,SAAS,QAAQ,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;iBAW/C,MAAM,GAAG,SAAS,QAAQ,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;iBASnD,MAAM,GAAG,SAAS,QAAQ,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;cASzD,MAAM,GAAG,SAAS,QAAQ,MAAM,GAAG,MAAM;kBAY/B,MAAM,GAAG,SAAS,QAAQ,MAAM,YAAY,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC;EAqBhG,CAAC;AAEH,eAAe,OAAO,CAAC"}
@@ -1,7 +1,6 @@
1
+ import { GcsDriver } from '../../tools/storage/drivers/Gcs';
1
2
  import { storageConfig } from '../../config/storage';
2
3
  import { ErrorFactory } from '../../exceptions/ZintrustError';
3
- // import { GcsDriver } from './drivers/Gcs';
4
- import { GcsDriver } from '../../tools/storage/drivers/Gcs';
5
4
  import { LocalDriver } from './drivers/Local';
6
5
  import { R2Driver } from './drivers/R2';
7
6
  import { S3Driver } from './drivers/S3';
@@ -71,7 +70,7 @@ export const Storage = Object.freeze({
71
70
  if (typeof driver.get !== 'function') {
72
71
  throw ErrorFactory.createConfigError('Storage: driver is missing get()');
73
72
  }
74
- return Promise.resolve(driver.get(d.config, path));
73
+ return driver.get(d.config, path);
75
74
  },
76
75
  async exists(disk, path) {
77
76
  const d = Storage.getDisk(disk);
@@ -96,11 +95,11 @@ export const Storage = Object.freeze({
96
95
  }
97
96
  return url;
98
97
  },
99
- tempUrl(disk, path, options) {
98
+ async tempUrl(disk, path, options) {
100
99
  const d = Storage.getDisk(disk);
101
100
  const driver = d.driver;
102
101
  if (typeof driver.tempUrl === 'function') {
103
- return driver.tempUrl(d.config, path, options);
102
+ return Promise.resolve(driver.tempUrl(d.config, path, options));
104
103
  }
105
104
  const url = typeof driver.url === 'function' ? driver.url(d.config, path) : undefined;
106
105
  if (typeof url !== 'string' || url.trim() === '') {
@@ -13,7 +13,7 @@ export declare const FakeStorage: Readonly<{
13
13
  tempUrl(disk: string, path: string, options?: {
14
14
  expiresIn?: number;
15
15
  method?: "GET" | "PUT";
16
- }): string;
16
+ }): Promise<string>;
17
17
  assertExists(disk: string, path: string): void;
18
18
  assertMissing(disk: string, path: string): void;
19
19
  getPuts(): FakePut[];
@@ -1 +1 @@
1
- {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../../../../src/tools/storage/testing.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,OAAO,GAAG;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAIF,eAAO,MAAM,WAAW;WACT,KAAK,CAAC,OAAO,CAAC;cAEX,MAAM,QAAQ,MAAM,YAAY,MAAM;cAM5C,MAAM,QAAQ,MAAM;iBAMjB,MAAM,QAAQ,MAAM;iBAId,MAAM,QAAQ,MAAM;cAW7B,MAAM,QAAQ,MAAM;kBAKhB,MAAM,QAAQ,MAAM,YAAY;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,KAAK,GAAG,KAAK,CAAA;KAAE;uBAOzE,MAAM,QAAQ,MAAM;wBAMnB,MAAM,QAAQ,MAAM;;;EAgBxC,CAAC;AAEH,eAAe,WAAW,CAAC"}
1
+ {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../../../../src/tools/storage/testing.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,OAAO,GAAG;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAIF,eAAO,MAAM,WAAW;WACT,KAAK,CAAC,OAAO,CAAC;cAEX,MAAM,QAAQ,MAAM,YAAY,MAAM;cAM5C,MAAM,QAAQ,MAAM;iBAMjB,MAAM,QAAQ,MAAM;iBAId,MAAM,QAAQ,MAAM;cAW7B,MAAM,QAAQ,MAAM;kBAMtB,MAAM,QACN,MAAM,YACF;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,KAAK,GAAG,KAAK,CAAA;KAAE;uBASvC,MAAM,QAAQ,MAAM;wBAMnB,MAAM,QAAQ,MAAM;;;EAgBxC,CAAC;AAEH,eAAe,WAAW,CAAC"}
@@ -25,9 +25,10 @@ export const FakeStorage = Object.freeze({
25
25
  return `fake://${disk}/${path}`;
26
26
  },
27
27
  // tempUrl builder is a convenience: matches the real API shape
28
- tempUrl(disk, path, options) {
28
+ async tempUrl(disk, path, options) {
29
29
  const expiresIn = options?.expiresIn ?? 900;
30
30
  const method = options?.method ?? 'GET';
31
+ await Promise.resolve();
31
32
  return `fake://${disk}/${path}?expiresIn=${expiresIn}&method=${method}`;
32
33
  },
33
34
  // Test assertions