@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.
- package/README.md +2 -2
- package/bin/zintrust.d.ts.map +1 -1
- package/bin/zintrust.js +59 -37
- package/package.json +15 -1
- package/src/cli/BaseCommand.js +1 -1
- package/src/cli/CLI.d.ts.map +1 -1
- package/src/cli/CLI.js +4 -3
- package/src/cli/commands/NewCommand.d.ts +4 -3
- package/src/cli/commands/NewCommand.d.ts.map +1 -1
- package/src/cli/commands/NewCommand.js +117 -31
- package/src/cli/scaffolding/ProjectScaffolder.d.ts.map +1 -1
- package/src/cli/scaffolding/ProjectScaffolder.js +15 -1
- package/src/index.d.ts +7 -2
- package/src/index.d.ts.map +1 -1
- package/src/index.js +4 -0
- package/src/scripts/TemplateSync.js +31 -1
- package/src/security/XssProtection.d.ts.map +1 -1
- package/src/security/XssProtection.js +6 -2
- package/src/templates/project/basic/config/FileLogWriter.ts.tpl +1 -1
- package/src/templates/project/basic/config/SecretsManager.ts.tpl +1 -1
- package/src/templates/project/basic/config/StartupConfigValidator.ts.tpl +1 -1
- package/src/templates/project/basic/config/cloudflare.ts.tpl +1 -1
- package/src/templates/project/basic/config/logging/HttpLogger.ts.tpl +3 -3
- package/src/templates/project/basic/config/logging/SlackLogger.ts.tpl +2 -2
- package/src/templates/project/basic/config/security.ts.tpl +1 -1
- package/src/templates/project/basic/package.json.tpl +1 -1
- package/src/templates/project/basic/routes/api.ts.tpl +3 -1
- package/src/templates/project/basic/routes/broadcast.ts.tpl +1 -1
- package/src/templates/project/basic/routes/health.ts.tpl +3 -3
- package/src/templates/project/basic/routes/storage.ts.tpl +42 -0
- package/src/templates/project/basic/tsconfig.json.tpl +15 -1
- package/src/tools/http/Http.d.ts +0 -2
- package/src/tools/http/Http.d.ts.map +1 -1
- package/src/tools/http/Http.js +0 -2
- package/src/tools/storage/LocalSignedUrl.d.ts +12 -0
- package/src/tools/storage/LocalSignedUrl.d.ts.map +1 -0
- package/src/tools/storage/LocalSignedUrl.js +108 -0
- package/src/tools/storage/drivers/Local.d.ts +2 -1
- package/src/tools/storage/drivers/Local.d.ts.map +1 -1
- package/src/tools/storage/drivers/Local.js +39 -13
- package/src/tools/storage/index.d.ts +1 -1
- package/src/tools/storage/index.d.ts.map +1 -1
- package/src/tools/storage/index.js +4 -5
- package/src/tools/storage/testing.d.ts +1 -1
- package/src/tools/storage/testing.d.ts.map +1 -1
- 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
|
|
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 **
|
|
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
|
package/bin/zintrust.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"zintrust.d.ts","sourceRoot":"","sources":["../../bin/zintrust.ts"],"names":[],"mappings":";AAEA;;;;;GAKG;
|
|
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 =
|
|
18
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|
package/src/cli/BaseCommand.js
CHANGED
|
@@ -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('
|
|
20
|
+
command.option('--verbose', 'Enable verbose output');
|
|
21
21
|
// Add custom options
|
|
22
22
|
if (config.addOptions) {
|
|
23
23
|
config.addOptions(command);
|
package/src/cli/CLI.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
-
//
|
|
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(
|
|
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;
|
|
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: '
|
|
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: '
|
|
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
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
209
|
-
|
|
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
|
-
|
|
216
|
-
|
|
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(
|
|
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: (
|
|
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,
|
|
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
|
-
|
|
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';
|
package/src/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,YAAY,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,YAAY,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAC9D,YAAY,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACpE,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAClE,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACrE,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACnE,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,YAAY,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,YAAY,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAG/C,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,YAAY,EACV,WAAW,EACX,cAAc,EACd,SAAS,EACT,aAAa,EACb,aAAa,GACd,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAC9D,YAAY,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,YAAY,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,YAAY,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAC9D,YAAY,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACpE,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAClE,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACrE,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACnE,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,YAAY,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,YAAY,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAG/C,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAGrD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGpE,YAAY,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAGxE,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,YAAY,EACV,WAAW,EACX,cAAc,EACd,SAAS,EACT,aAAa,EACb,aAAa,GACd,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAC9D,YAAY,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAC1D,YAAY,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAGjE,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAC9D,YAAY,EACV,aAAa,EACb,oBAAoB,EACpB,iBAAiB,GAClB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,YAAY,EACV,WAAW,EACX,YAAY,EACZ,cAAc,EACd,UAAU,EACV,UAAU,GACX,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAGxD,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAGzD,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,YAAY,EAAE,UAAU,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACjF,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAGxE,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAClC,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEhE,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,YAAY,EAAE,UAAU,IAAI,gBAAgB,EAAE,MAAM,oBAAoB,CAAC"}
|
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
|
|
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;
|
|
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
|
-
|
|
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="javascript:..." 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 '@
|
|
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
|
* This keeps runtime-specific globals out of adapters/drivers.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { DatabaseConfig, ID1Database } from '@
|
|
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 '@
|
|
12
|
+
import { delay } from '@zintrust/core';
|
|
13
13
|
import { Env } from '@config/env';
|
|
14
|
-
import { ErrorFactory } from '@
|
|
15
|
-
import { HttpClient } from '@
|
|
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 '@
|
|
14
|
-
import { HttpClient } from '@
|
|
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 '@
|
|
10
|
+
import { ErrorFactory } from '@zintrust/core';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Helper to warn about missing secrets
|
|
@@ -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 {
|
|
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 '@
|
|
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 '@
|
|
11
|
-
import { QueryBuilder } from '@
|
|
12
|
-
import { type IRouter, Router } from '@
|
|
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/**/*"],
|
package/src/tools/http/Http.d.ts
CHANGED
|
@@ -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
|
|
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"}
|
package/src/tools/http/Http.js
CHANGED
|
@@ -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,
|
|
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":"
|
|
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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
56
|
+
const fullPath = LocalDriver.resolveKey(config, key);
|
|
42
57
|
try {
|
|
43
58
|
await fs.unlink(fullPath);
|
|
44
59
|
}
|
|
45
|
-
catch
|
|
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,
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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":"
|
|
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
|
|
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;
|
|
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
|