@zintrust/core 0.1.20 → 0.1.21
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/package.json +2 -1
- package/src/boot/Application.d.ts.map +1 -1
- package/src/boot/Application.js +48 -10
- package/src/boot/bootstrap.js +2 -0
- package/src/cli/commands/MigrateCommand.d.ts.map +1 -1
- package/src/cli/commands/MigrateCommand.js +36 -3
- package/src/cli/d1/D1SqlMigrations.d.ts.map +1 -1
- package/src/cli/d1/D1SqlMigrations.js +6 -1
- package/src/cli/scaffolding/ControllerGenerator.js +4 -4
- package/src/cli/scaffolding/GovernanceScaffolder.js +1 -1
- package/src/cli/scaffolding/MigrationGenerator.js +1 -1
- package/src/cli/scaffolding/ModelGenerator.js +1 -1
- package/src/cli/scaffolding/RouteGenerator.js +1 -1
- package/src/cli/scaffolding/ServiceScaffolder.js +4 -4
- package/src/config/broadcast.d.ts +14 -28
- package/src/config/broadcast.d.ts.map +1 -1
- package/src/config/broadcast.js +69 -35
- package/src/config/cache.d.ts +13 -45
- package/src/config/cache.d.ts.map +1 -1
- package/src/config/cache.js +69 -25
- package/src/config/database.d.ts +22 -64
- package/src/config/database.d.ts.map +1 -1
- package/src/config/database.js +99 -31
- package/src/config/env.d.ts +6 -0
- package/src/config/env.d.ts.map +1 -1
- package/src/config/env.js +7 -0
- package/src/config/index.d.ts +32 -136
- package/src/config/index.d.ts.map +1 -1
- package/src/config/mail.d.ts +19 -55
- package/src/config/mail.d.ts.map +1 -1
- package/src/config/mail.js +63 -21
- package/src/config/middleware.d.ts +24 -0
- package/src/config/middleware.d.ts.map +1 -1
- package/src/config/middleware.js +72 -52
- package/src/config/notification.d.ts +14 -27
- package/src/config/notification.d.ts.map +1 -1
- package/src/config/notification.js +82 -36
- package/src/config/queue.d.ts +21 -51
- package/src/config/queue.d.ts.map +1 -1
- package/src/config/queue.js +72 -27
- package/src/config/storage.d.ts +27 -34
- package/src/config/storage.d.ts.map +1 -1
- package/src/config/storage.js +97 -56
- package/src/config/type.d.ts +12 -1
- package/src/config/type.d.ts.map +1 -1
- package/src/http/parsers/MultipartParser.d.ts.map +1 -1
- package/src/http/parsers/MultipartParser.js +69 -42
- package/src/index.d.ts +9 -5
- package/src/index.d.ts.map +1 -1
- package/src/index.js +1 -0
- package/src/microservices/PostgresAdapter.d.ts.map +1 -1
- package/src/microservices/PostgresAdapter.js +0 -1
- package/src/migrations/MigratorFactory.d.ts.map +1 -1
- package/src/migrations/MigratorFactory.js +18 -2
- package/src/node-singletons/fs.d.ts +1 -1
- package/src/node-singletons/fs.d.ts.map +1 -1
- package/src/node-singletons/fs.js +1 -1
- package/src/orm/Database.d.ts +2 -1
- package/src/orm/Database.d.ts.map +1 -1
- package/src/orm/Database.js +110 -67
- package/src/orm/DatabaseAdapter.d.ts +1 -0
- package/src/orm/DatabaseAdapter.d.ts.map +1 -1
- package/src/orm/DatabaseRuntimeRegistration.d.ts.map +1 -1
- package/src/orm/DatabaseRuntimeRegistration.js +12 -0
- package/src/orm/QueryBuilder.d.ts +1 -1
- package/src/orm/QueryBuilder.d.ts.map +1 -1
- package/src/orm/QueryBuilder.js +4 -3
- package/src/orm/adapters/SQLiteAdapter.js +1 -1
- package/src/performance/Optimizer.d.ts +6 -6
- package/src/performance/Optimizer.d.ts.map +1 -1
- package/src/performance/Optimizer.js +133 -52
- package/src/routing/doc.d.ts +4 -5
- package/src/routing/doc.d.ts.map +1 -1
- package/src/routing/doc.js +35 -20
- package/src/routing/publicRoot.d.ts +9 -0
- package/src/routing/publicRoot.d.ts.map +1 -1
- package/src/routing/publicRoot.js +63 -2
- package/src/runtime/StartupConfigFileRegistry.d.ts +20 -0
- package/src/runtime/StartupConfigFileRegistry.d.ts.map +1 -0
- package/src/runtime/StartupConfigFileRegistry.js +44 -0
- package/src/runtime/useFileLoader.d.ts +26 -0
- package/src/runtime/useFileLoader.d.ts.map +1 -0
- package/src/runtime/useFileLoader.js +188 -0
- package/src/scripts/TemplateSync.js +4 -4
- package/src/security/XssProtection.d.ts.map +1 -1
- package/src/security/XssProtection.js +62 -14
- package/src/templates/project/basic/config/broadcast.ts.tpl +33 -17
- package/src/templates/project/basic/config/cache.ts.tpl +35 -17
- package/src/templates/project/basic/config/database.ts.tpl +68 -32
- package/src/templates/project/basic/config/logging/HttpLogger.ts.tpl +7 -114
- package/src/templates/project/basic/config/mail.ts.tpl +59 -13
- package/src/templates/project/basic/config/notification.ts.tpl +28 -17
- package/src/templates/project/basic/config/queue.ts.tpl +49 -17
- package/src/templates/project/basic/config/storage.ts.tpl +55 -18
- package/src/templates/project/basic/config/type.ts.tpl +0 -1
- package/src/templates/project/basic/src/index.ts.tpl +3 -0
- package/src/templates/project/basic/config/logging/KvLogger.ts.tpl +0 -181
- package/src/templates/project/basic/config/logging/SlackLogger.ts.tpl +0 -156
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare enum StartupConfigFile {
|
|
2
|
+
Broadcast = "config/broadcast.ts",
|
|
3
|
+
Cache = "config/cache.ts",
|
|
4
|
+
Database = "config/database.ts",
|
|
5
|
+
Mail = "config/mail.ts",
|
|
6
|
+
Middleware = "config/middleware.ts",
|
|
7
|
+
Notification = "config/notification.ts",
|
|
8
|
+
Queue = "config/queue.ts",
|
|
9
|
+
Storage = "config/storage.ts"
|
|
10
|
+
}
|
|
11
|
+
export declare const StartupConfigFileRegistry: Readonly<{
|
|
12
|
+
preload(files: readonly StartupConfigFile[]): Promise<void>;
|
|
13
|
+
isPreloaded(): boolean;
|
|
14
|
+
get<T>(file: StartupConfigFile): T | undefined;
|
|
15
|
+
has(file: StartupConfigFile): boolean;
|
|
16
|
+
/** Intended for tests only. */
|
|
17
|
+
clear(): void;
|
|
18
|
+
}>;
|
|
19
|
+
export default StartupConfigFileRegistry;
|
|
20
|
+
//# sourceMappingURL=StartupConfigFileRegistry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StartupConfigFileRegistry.d.ts","sourceRoot":"","sources":["../../../src/runtime/StartupConfigFileRegistry.ts"],"names":[],"mappings":"AAEA,oBAAY,iBAAiB;IAC3B,SAAS,wBAAwB;IACjC,KAAK,oBAAoB;IACzB,QAAQ,uBAAuB;IAC/B,IAAI,mBAAmB;IACvB,UAAU,yBAAyB;IACnC,YAAY,2BAA2B;IACvC,KAAK,oBAAoB;IACzB,OAAO,sBAAsB;CAC9B;AAKD,eAAO,MAAM,yBAAyB;mBACf,SAAS,iBAAiB,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;mBAelD,OAAO;QAIlB,CAAC,QAAQ,iBAAiB,GAAG,CAAC,GAAG,SAAS;cAIpC,iBAAiB,GAAG,OAAO;IAIrC,+BAA+B;aACtB,IAAI;EAIb,CAAC;AAEH,eAAe,yBAAyB,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import useFileLoader from '../runtime/useFileLoader.js';
|
|
2
|
+
export var StartupConfigFile;
|
|
3
|
+
(function (StartupConfigFile) {
|
|
4
|
+
StartupConfigFile["Broadcast"] = "config/broadcast.ts";
|
|
5
|
+
StartupConfigFile["Cache"] = "config/cache.ts";
|
|
6
|
+
StartupConfigFile["Database"] = "config/database.ts";
|
|
7
|
+
StartupConfigFile["Mail"] = "config/mail.ts";
|
|
8
|
+
StartupConfigFile["Middleware"] = "config/middleware.ts";
|
|
9
|
+
StartupConfigFile["Notification"] = "config/notification.ts";
|
|
10
|
+
StartupConfigFile["Queue"] = "config/queue.ts";
|
|
11
|
+
StartupConfigFile["Storage"] = "config/storage.ts";
|
|
12
|
+
})(StartupConfigFile || (StartupConfigFile = {}));
|
|
13
|
+
const cache = new Map();
|
|
14
|
+
let preloaded = false;
|
|
15
|
+
export const StartupConfigFileRegistry = Object.freeze({
|
|
16
|
+
async preload(files) {
|
|
17
|
+
const tasks = files.map(async (file) => {
|
|
18
|
+
const loader = useFileLoader(file);
|
|
19
|
+
if (!loader.exists()) {
|
|
20
|
+
cache.delete(file);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const value = await loader.get();
|
|
24
|
+
cache.set(file, value);
|
|
25
|
+
});
|
|
26
|
+
await Promise.all(tasks);
|
|
27
|
+
preloaded = true;
|
|
28
|
+
},
|
|
29
|
+
isPreloaded() {
|
|
30
|
+
return preloaded;
|
|
31
|
+
},
|
|
32
|
+
get(file) {
|
|
33
|
+
return cache.get(file);
|
|
34
|
+
},
|
|
35
|
+
has(file) {
|
|
36
|
+
return cache.has(file);
|
|
37
|
+
},
|
|
38
|
+
/** Intended for tests only. */
|
|
39
|
+
clear() {
|
|
40
|
+
cache.clear();
|
|
41
|
+
preloaded = false;
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
export default StartupConfigFileRegistry;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project file loader
|
|
3
|
+
*
|
|
4
|
+
* Loads project-owned files (typically config modules) from the project root.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* - useFileLoader('config/mail.ts').get<TypeMail>()
|
|
8
|
+
* - useFileLoader('config', 'mail.ts').get<TypeMail>()
|
|
9
|
+
*/
|
|
10
|
+
export type FileLoader = Readonly<{
|
|
11
|
+
/** Absolute filesystem candidates (in resolution order). */
|
|
12
|
+
candidates: () => readonly string[];
|
|
13
|
+
/** Returns the first existing candidate path (or the first candidate if none exist). */
|
|
14
|
+
path: () => string;
|
|
15
|
+
/** Whether any candidate exists on disk. */
|
|
16
|
+
exists: () => boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Loads the file via ESM dynamic import and returns:
|
|
19
|
+
* - `default` export when present
|
|
20
|
+
* - otherwise the full module namespace object
|
|
21
|
+
*/
|
|
22
|
+
get: <T = unknown>() => Promise<T>;
|
|
23
|
+
}>;
|
|
24
|
+
export declare const useFileLoader: (...args: [string] | [string, ...string[]]) => FileLoader;
|
|
25
|
+
export default useFileLoader;
|
|
26
|
+
//# sourceMappingURL=useFileLoader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useFileLoader.d.ts","sourceRoot":"","sources":["../../../src/runtime/useFileLoader.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAUH,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAC;IAChC,4DAA4D;IAC5D,UAAU,EAAE,MAAM,SAAS,MAAM,EAAE,CAAC;IACpC,wFAAwF;IACxF,IAAI,EAAE,MAAM,MAAM,CAAC;IACnB,4CAA4C;IAC5C,MAAM,EAAE,MAAM,OAAO,CAAC;IACtB;;;;OAIG;IACH,GAAG,EAAE,CAAC,CAAC,GAAG,OAAO,OAAO,OAAO,CAAC,CAAC,CAAC,CAAC;CACpC,CAAC,CAAC;AAyIH,eAAO,MAAM,aAAa,GAAI,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,KAAG,UA+DzE,CAAC;AAEF,eAAe,aAAa,CAAC"}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project file loader
|
|
3
|
+
*
|
|
4
|
+
* Loads project-owned files (typically config modules) from the project root.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* - useFileLoader('config/mail.ts').get<TypeMail>()
|
|
8
|
+
* - useFileLoader('config', 'mail.ts').get<TypeMail>()
|
|
9
|
+
*/
|
|
10
|
+
import { ErrorFactory } from '../exceptions/ZintrustError.js';
|
|
11
|
+
import { existsSync } from '../node-singletons/fs.js';
|
|
12
|
+
import pathModule, { extname, join, resolve, sep } from '../node-singletons/path.js';
|
|
13
|
+
import processModule from '../node-singletons/process.js';
|
|
14
|
+
import { pathToFileURL } from '../node-singletons/url.js';
|
|
15
|
+
const resolveProjectRoot = () => {
|
|
16
|
+
const isTestRuntime = () => {
|
|
17
|
+
const nodeEnv = processModule.env?.['NODE_ENV'];
|
|
18
|
+
const isVitest = processModule.env?.['VITEST'] !== undefined ||
|
|
19
|
+
processModule.env?.['VITEST_WORKER_ID'] !== undefined ||
|
|
20
|
+
processModule.env?.['VITEST_POOL_ID'] !== undefined;
|
|
21
|
+
return nodeEnv === 'testing' || isVitest;
|
|
22
|
+
};
|
|
23
|
+
const isCoreRepo = (cwdPath) => {
|
|
24
|
+
const fromNpm = processModule.env?.['npm_package_name'];
|
|
25
|
+
if (fromNpm === '@zintrust/core')
|
|
26
|
+
return true;
|
|
27
|
+
// Vitest suites (notably CoverageBoost) may mock `@node-singletons/fs` to return
|
|
28
|
+
// `existsSync() === true` for everything and `readFileSync() === '{}'`, which makes
|
|
29
|
+
// any package.json-based detection unreliable. Instead, detect a core repo checkout
|
|
30
|
+
// by checking whether this module is being executed from within the current cwd.
|
|
31
|
+
// - core repo tests: import.meta.url points into `<cwd>/src/...`
|
|
32
|
+
// - consumer apps: import.meta.url points into `node_modules/@zintrust/core/...`
|
|
33
|
+
try {
|
|
34
|
+
const cwdAbs = resolve(cwdPath);
|
|
35
|
+
const selfUrl = new URL(import.meta.url);
|
|
36
|
+
const selfPath = decodeURIComponent(selfUrl.pathname);
|
|
37
|
+
const selfAbs = resolve(selfPath);
|
|
38
|
+
return selfAbs === cwdAbs || selfAbs.startsWith(cwdAbs + sep);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const fromEnv = processModule.env?.['ZINTRUST_PROJECT_ROOT'];
|
|
45
|
+
if (typeof fromEnv === 'string' && fromEnv.trim().length > 0)
|
|
46
|
+
return fromEnv.trim();
|
|
47
|
+
const cwd = processModule.cwd();
|
|
48
|
+
// In the ZinTrust core repo, `config/*.ts` are templates (not consumer app config).
|
|
49
|
+
// During core test runs, we avoid auto-loading them to prevent unexpected overrides.
|
|
50
|
+
// In normal runs, keep the historical behavior (projectRoot = cwd).
|
|
51
|
+
if (isCoreRepo(cwd) && isTestRuntime()) {
|
|
52
|
+
return resolve(cwd, '.zintrust-internal-project-root');
|
|
53
|
+
}
|
|
54
|
+
return cwd;
|
|
55
|
+
};
|
|
56
|
+
const isInternalProjectRoot = (projectRoot) => pathModule.basename(projectRoot) === '.zintrust-internal-project-root';
|
|
57
|
+
const normalizeProjectRelativePath = (raw) => {
|
|
58
|
+
const value = String(raw ?? '').trim();
|
|
59
|
+
if (value.length === 0) {
|
|
60
|
+
throw ErrorFactory.createConfigError('useFileLoader() requires a non-empty path');
|
|
61
|
+
}
|
|
62
|
+
if (value.includes('\u0000')) {
|
|
63
|
+
throw ErrorFactory.createSecurityError('Invalid file path (null byte)');
|
|
64
|
+
}
|
|
65
|
+
const normalized = value.replaceAll('\\', '/').replace(/^\.\/+/, '');
|
|
66
|
+
if (pathModule.isAbsolute(normalized)) {
|
|
67
|
+
throw ErrorFactory.createSecurityError('Absolute paths are not allowed', { requested: value });
|
|
68
|
+
}
|
|
69
|
+
return normalized;
|
|
70
|
+
};
|
|
71
|
+
const resolveWithinProjectRoot = (projectRoot, relativePath) => {
|
|
72
|
+
const rootAbs = resolve(projectRoot);
|
|
73
|
+
const candidateAbs = resolve(projectRoot, relativePath);
|
|
74
|
+
if (candidateAbs === rootAbs)
|
|
75
|
+
return candidateAbs;
|
|
76
|
+
if (!candidateAbs.startsWith(rootAbs + sep)) {
|
|
77
|
+
throw ErrorFactory.createSecurityError('Invalid file path (path traversal detected)', {
|
|
78
|
+
projectRoot: rootAbs,
|
|
79
|
+
requested: relativePath,
|
|
80
|
+
resolved: candidateAbs,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return candidateAbs;
|
|
84
|
+
};
|
|
85
|
+
const replaceExtension = (filePath, nextExt) => {
|
|
86
|
+
const current = extname(filePath);
|
|
87
|
+
if (current.length === 0)
|
|
88
|
+
return `${filePath}${nextExt}`;
|
|
89
|
+
return filePath.slice(0, -current.length) + nextExt;
|
|
90
|
+
};
|
|
91
|
+
const unique = (items) => {
|
|
92
|
+
const seen = new Set();
|
|
93
|
+
const out = [];
|
|
94
|
+
for (const item of items) {
|
|
95
|
+
if (seen.has(item))
|
|
96
|
+
continue;
|
|
97
|
+
seen.add(item);
|
|
98
|
+
out.push(item);
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
};
|
|
102
|
+
const buildCandidateAbsolutePaths = (projectRoot, relativePath) => {
|
|
103
|
+
const ext = extname(relativePath);
|
|
104
|
+
const baseRelCandidates = unique([
|
|
105
|
+
relativePath,
|
|
106
|
+
...(ext.length === 0
|
|
107
|
+
? [`${relativePath}.ts`, `${relativePath}.js`, `${relativePath}.mjs`]
|
|
108
|
+
: []),
|
|
109
|
+
...(ext === '.ts'
|
|
110
|
+
? [replaceExtension(relativePath, '.js'), replaceExtension(relativePath, '.mjs')]
|
|
111
|
+
: []),
|
|
112
|
+
...(ext === '.js'
|
|
113
|
+
? [replaceExtension(relativePath, '.mjs'), replaceExtension(relativePath, '.ts')]
|
|
114
|
+
: []),
|
|
115
|
+
...(ext === '.mjs'
|
|
116
|
+
? [replaceExtension(relativePath, '.js'), replaceExtension(relativePath, '.ts')]
|
|
117
|
+
: []),
|
|
118
|
+
]);
|
|
119
|
+
const absCandidates = baseRelCandidates.flatMap((rel) => [
|
|
120
|
+
resolveWithinProjectRoot(projectRoot, rel),
|
|
121
|
+
resolveWithinProjectRoot(projectRoot, join('dist', rel)),
|
|
122
|
+
]);
|
|
123
|
+
return unique(absCandidates);
|
|
124
|
+
};
|
|
125
|
+
const importModule = async (filePath) => {
|
|
126
|
+
const url = pathToFileURL(filePath).href;
|
|
127
|
+
return (await import(url));
|
|
128
|
+
};
|
|
129
|
+
export const useFileLoader = (...args) => {
|
|
130
|
+
const relativePath = args.length === 1
|
|
131
|
+
? normalizeProjectRelativePath(args[0])
|
|
132
|
+
: normalizeProjectRelativePath(args.join('/'));
|
|
133
|
+
const projectRoot = resolveProjectRoot();
|
|
134
|
+
const isInternalRoot = isInternalProjectRoot(projectRoot);
|
|
135
|
+
const candidates = buildCandidateAbsolutePaths(projectRoot, relativePath);
|
|
136
|
+
// The core repo uses `.zintrust-internal-project-root` as a sentinel during core test runs.
|
|
137
|
+
// In this mode, we must *never* import project config templates.
|
|
138
|
+
// This also avoids test suites that mock `existsSync()` globally (e.g. CoverageBoost).
|
|
139
|
+
const exists = () => (isInternalRoot ? false : candidates.some((c) => existsSync(c)));
|
|
140
|
+
const resolveFirstExistingPath = () => {
|
|
141
|
+
if (isInternalRoot)
|
|
142
|
+
return candidates[0] ?? resolveWithinProjectRoot(projectRoot, relativePath);
|
|
143
|
+
for (const candidate of candidates) {
|
|
144
|
+
if (existsSync(candidate))
|
|
145
|
+
return candidate;
|
|
146
|
+
}
|
|
147
|
+
return candidates[0] ?? resolveWithinProjectRoot(projectRoot, relativePath);
|
|
148
|
+
};
|
|
149
|
+
const get = async () => {
|
|
150
|
+
if (isInternalRoot) {
|
|
151
|
+
throw ErrorFactory.createNotFoundError('Project file not found', {
|
|
152
|
+
projectRoot,
|
|
153
|
+
relativePath,
|
|
154
|
+
candidates,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
const candidate = candidates.find((c) => existsSync(c));
|
|
158
|
+
if (candidate === undefined) {
|
|
159
|
+
throw ErrorFactory.createNotFoundError('Project file not found', {
|
|
160
|
+
projectRoot,
|
|
161
|
+
relativePath,
|
|
162
|
+
candidates,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
const mod = await importModule(candidate);
|
|
167
|
+
if (Object.hasOwn(mod, 'default') && mod.default !== undefined) {
|
|
168
|
+
return mod.default;
|
|
169
|
+
}
|
|
170
|
+
return mod;
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
throw ErrorFactory.createTryCatchError('Failed to import project file', {
|
|
174
|
+
candidate,
|
|
175
|
+
projectRoot,
|
|
176
|
+
relativePath,
|
|
177
|
+
error,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
return Object.freeze({
|
|
182
|
+
candidates: () => candidates,
|
|
183
|
+
path: resolveFirstExistingPath,
|
|
184
|
+
exists,
|
|
185
|
+
get,
|
|
186
|
+
});
|
|
187
|
+
};
|
|
188
|
+
export default useFileLoader;
|
|
@@ -140,10 +140,10 @@ const rewriteStarterTemplateImports = (relPath, content) => {
|
|
|
140
140
|
.replaceAll('"@common/uuid"', '"@zintrust/core"')
|
|
141
141
|
// Starter templates should not rely on local config/env wrappers.
|
|
142
142
|
// Normalize Env imports to come from the public package surface.
|
|
143
|
-
.replaceAll("from '../env';", "from '
|
|
144
|
-
.replaceAll('from "../env";', 'from "
|
|
145
|
-
.replaceAll("from './env';", "from '
|
|
146
|
-
.replaceAll('from "./env";', 'from "
|
|
143
|
+
.replaceAll("from '../env';", "from '../index.js';")
|
|
144
|
+
.replaceAll('from "../env";', 'from "../index.js";')
|
|
145
|
+
.replaceAll("from './env';", "from '../index.js';")
|
|
146
|
+
.replaceAll('from "./env";', 'from "../index.js";')
|
|
147
147
|
// Node-singletons are internal to this repo; starter templates should use Node built-ins.
|
|
148
148
|
.replaceAll("'@node-singletons/fs'", "'node:fs'")
|
|
149
149
|
.replaceAll('"@node-singletons/fs"', '"node:fs"')
|
|
@@ -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;AA0WH;;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"}
|
|
@@ -27,30 +27,51 @@ const escapeHtml = (text) => {
|
|
|
27
27
|
/**
|
|
28
28
|
* Sanitize HTML by removing dangerous tags and attributes
|
|
29
29
|
*/
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
return '';
|
|
33
|
-
}
|
|
30
|
+
const MAX_SANITIZE_LOOPS = 50;
|
|
31
|
+
function removeScripts(input) {
|
|
34
32
|
// Remove script tags and content (loop until stable to avoid incomplete multi-character sanitization)
|
|
35
|
-
let sanitized =
|
|
36
|
-
let
|
|
33
|
+
let sanitized = input;
|
|
34
|
+
let prevSanitized;
|
|
35
|
+
let loopCount = 0;
|
|
37
36
|
do {
|
|
38
|
-
|
|
37
|
+
prevSanitized = sanitized;
|
|
39
38
|
sanitized = sanitized.replaceAll(/<script\b[\s\S]*?<\/script[^<]*?>/gi, '');
|
|
40
|
-
|
|
39
|
+
loopCount++;
|
|
40
|
+
} while (sanitized !== prevSanitized && loopCount < MAX_SANITIZE_LOOPS);
|
|
41
|
+
if (loopCount >= MAX_SANITIZE_LOOPS) {
|
|
42
|
+
Logger.warn('[XSS] Sanitization loop limit reached (script removal)');
|
|
43
|
+
return ''; // Fail safe: return empty string if attack detected
|
|
44
|
+
}
|
|
45
|
+
return sanitized;
|
|
46
|
+
}
|
|
47
|
+
function removeDangerousTags(input) {
|
|
41
48
|
// Remove iframe, object, embed, and base tags
|
|
49
|
+
let sanitized = input;
|
|
42
50
|
sanitized = sanitized.replaceAll(/<(?:iframe|object|embed|base)\b[\s\S]*?>/gi, '');
|
|
43
51
|
sanitized = sanitized.replaceAll(/<\/(?:iframe|object|embed|base)>/gi, '');
|
|
52
|
+
return sanitized;
|
|
53
|
+
}
|
|
54
|
+
function removeEventHandlers(input) {
|
|
44
55
|
// Remove event handlers (on*). Re-apply until stable to avoid incomplete multi-character sanitization.
|
|
45
|
-
let
|
|
56
|
+
let sanitized = input;
|
|
57
|
+
let prevSanitized;
|
|
58
|
+
let loopCount = 0;
|
|
46
59
|
do {
|
|
47
|
-
|
|
60
|
+
prevSanitized = sanitized;
|
|
48
61
|
sanitized = sanitized.replaceAll(/\bon\w+\s*=\s*(?:'[^']*'|"[^"]*"|`[^`]*`|[^\s>]*)/gi, '');
|
|
49
|
-
|
|
62
|
+
loopCount++;
|
|
63
|
+
} while (sanitized !== prevSanitized && loopCount < MAX_SANITIZE_LOOPS);
|
|
64
|
+
if (loopCount >= MAX_SANITIZE_LOOPS) {
|
|
65
|
+
Logger.warn('[XSS] Sanitization loop limit reached (event handler removal)');
|
|
66
|
+
return '';
|
|
67
|
+
}
|
|
68
|
+
return sanitized;
|
|
69
|
+
}
|
|
70
|
+
function stripDangerousUrlAttributes(input) {
|
|
50
71
|
// Remove dangerous protocols in URL-bearing attributes.
|
|
51
72
|
// This uses the same protocol normalization logic as encodeHref to prevent obfuscations like:
|
|
52
73
|
// href="javascript:..." or href="java\nscript:..." or href="%6a%61..."
|
|
53
|
-
|
|
74
|
+
return input.replaceAll(/(\s)(href|src|action|formaction|xlink:href)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi, (match, _leadingWhitespace, _attributeName, doubleQuotedValue, singleQuotedValue, unquotedValue) => {
|
|
54
75
|
const rawValue = doubleQuotedValue ?? singleQuotedValue ?? unquotedValue ?? '';
|
|
55
76
|
const protocolCheck = normalizeHrefForProtocolCheck(rawValue);
|
|
56
77
|
// Allow relative URLs and fragments.
|
|
@@ -92,19 +113,46 @@ const sanitizeHtml = (html) => {
|
|
|
92
113
|
// Otherwise, keep the attribute (e.g. relative-like values without a scheme).
|
|
93
114
|
return match;
|
|
94
115
|
});
|
|
116
|
+
}
|
|
117
|
+
function removeStyles(input) {
|
|
95
118
|
// Remove style tags and style attributes with potentially dangerous content
|
|
119
|
+
let sanitized = input;
|
|
96
120
|
let prevSanitized;
|
|
121
|
+
let loopCount = 0;
|
|
97
122
|
do {
|
|
98
123
|
prevSanitized = sanitized;
|
|
99
124
|
sanitized = sanitized.replaceAll(/<style\b[\s\S]*?<\/style>/gi, '');
|
|
100
|
-
|
|
125
|
+
loopCount++;
|
|
126
|
+
} while (sanitized !== prevSanitized && loopCount < MAX_SANITIZE_LOOPS);
|
|
127
|
+
if (loopCount >= MAX_SANITIZE_LOOPS) {
|
|
128
|
+
Logger.warn('[XSS] Sanitization loop limit reached (style removal)');
|
|
129
|
+
return '';
|
|
130
|
+
}
|
|
101
131
|
sanitized = sanitized.replaceAll(/\bstyle\s*=\s*(?:'[^']*'|"[^"]*"|[^\s>]*)/gi, '');
|
|
132
|
+
return sanitized;
|
|
133
|
+
}
|
|
134
|
+
function sanitizeHtml(html) {
|
|
135
|
+
if (typeof html !== 'string') {
|
|
136
|
+
return '';
|
|
137
|
+
}
|
|
138
|
+
let sanitized = html;
|
|
139
|
+
sanitized = removeScripts(sanitized);
|
|
140
|
+
if (!sanitized)
|
|
141
|
+
return '';
|
|
142
|
+
sanitized = removeDangerousTags(sanitized);
|
|
143
|
+
sanitized = removeEventHandlers(sanitized);
|
|
144
|
+
if (!sanitized)
|
|
145
|
+
return '';
|
|
146
|
+
sanitized = stripDangerousUrlAttributes(sanitized);
|
|
147
|
+
sanitized = removeStyles(sanitized);
|
|
148
|
+
if (!sanitized)
|
|
149
|
+
return '';
|
|
102
150
|
// Remove form elements
|
|
103
151
|
sanitized = sanitized.replaceAll(/<form\b[\s\S]*?<\/form>/gi, '');
|
|
104
152
|
// Remove object and embed tags
|
|
105
153
|
sanitized = sanitized.replaceAll(/<(?:object|embed|applet|meta|link|base)\b[\s\S]*?>/gi, '');
|
|
106
154
|
return sanitized.trim();
|
|
107
|
-
}
|
|
155
|
+
}
|
|
108
156
|
/**
|
|
109
157
|
* Encode URI component to prevent injection in URLs
|
|
110
158
|
*/
|
|
@@ -1,23 +1,39 @@
|
|
|
1
|
+
import { Env, type BroadcastConfigOverrides } from '@zintrust/core';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
* Broadcast Configuration (
|
|
4
|
+
* Broadcast Configuration (default override)
|
|
3
5
|
*
|
|
4
6
|
* Keep this file declarative:
|
|
5
7
|
* - Core owns env parsing/default logic.
|
|
6
|
-
* - Projects can override
|
|
8
|
+
* - Projects can override config by editing values below.
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
11
|
+
export default {
|
|
12
|
+
default: Env.get('BROADCAST_CONNECTION', Env.get('BROADCAST_DRIVER', 'inmemory')),
|
|
13
|
+
drivers: {
|
|
14
|
+
inmemory: {
|
|
15
|
+
driver: 'inmemory' as const,
|
|
16
|
+
},
|
|
17
|
+
pusher: {
|
|
18
|
+
driver: 'pusher' as const,
|
|
19
|
+
appId: Env.get('PUSHER_APP_ID', ''),
|
|
20
|
+
key: Env.get('PUSHER_APP_KEY', ''),
|
|
21
|
+
secret: Env.get('PUSHER_APP_SECRET', ''),
|
|
22
|
+
cluster: Env.get('PUSHER_APP_CLUSTER', ''),
|
|
23
|
+
useTLS: Env.getBool('PUSHER_USE_TLS', true),
|
|
24
|
+
},
|
|
25
|
+
redis: {
|
|
26
|
+
driver: 'redis' as const,
|
|
27
|
+
host: Env.get('BROADCAST_REDIS_HOST', Env.get('REDIS_HOST', 'localhost')),
|
|
28
|
+
port: Env.getInt('BROADCAST_REDIS_PORT', Env.getInt('REDIS_PORT', 6379)),
|
|
29
|
+
password: Env.get('BROADCAST_REDIS_PASSWORD', Env.get('REDIS_PASSWORD', '')),
|
|
30
|
+
channelPrefix: Env.get('BROADCAST_CHANNEL_PREFIX', 'broadcast:'),
|
|
31
|
+
},
|
|
32
|
+
redishttps: {
|
|
33
|
+
driver: 'redishttps' as const,
|
|
34
|
+
endpoint: Env.get('REDIS_HTTPS_ENDPOINT', ''),
|
|
35
|
+
token: Env.get('REDIS_HTTPS_TOKEN', ''),
|
|
36
|
+
channelPrefix: Env.get('BROADCAST_CHANNEL_PREFIX', 'broadcast:'),
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
} satisfies BroadcastConfigOverrides;
|
|
@@ -1,23 +1,41 @@
|
|
|
1
|
+
import { Env, type CacheConfigOverrides } from '@zintrust/core';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
* Cache Configuration (
|
|
4
|
+
* Cache Configuration (default override)
|
|
3
5
|
*
|
|
4
6
|
* Keep this file declarative:
|
|
5
7
|
* - Core owns env parsing/default logic.
|
|
6
|
-
* - Projects can override
|
|
8
|
+
* - Projects can override config by editing values below.
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
11
|
+
export default {
|
|
12
|
+
default: Env.get('CACHE_CONNECTION', Env.get('CACHE_DRIVER', 'memory')).trim().toLowerCase(),
|
|
13
|
+
drivers: {
|
|
14
|
+
memory: {
|
|
15
|
+
driver: 'memory' as const,
|
|
16
|
+
ttl: Env.getInt('CACHE_MEMORY_TTL', 3600),
|
|
17
|
+
},
|
|
18
|
+
redis: {
|
|
19
|
+
driver: 'redis' as const,
|
|
20
|
+
host: Env.get('REDIS_HOST', 'localhost'),
|
|
21
|
+
port: Env.getInt('REDIS_PORT', 6379),
|
|
22
|
+
ttl: Env.getInt('CACHE_REDIS_TTL', 3600),
|
|
23
|
+
},
|
|
24
|
+
mongodb: {
|
|
25
|
+
driver: 'mongodb' as const,
|
|
26
|
+
uri: Env.get('MONGO_URI', ''),
|
|
27
|
+
db: Env.get('MONGO_DB', 'zintrust_cache'),
|
|
28
|
+
ttl: Env.getInt('CACHE_MONGO_TTL', 3600),
|
|
29
|
+
},
|
|
30
|
+
kv: {
|
|
31
|
+
driver: 'kv' as const,
|
|
32
|
+
ttl: Env.getInt('CACHE_KV_TTL', 3600),
|
|
33
|
+
},
|
|
34
|
+
'kv-remote': {
|
|
35
|
+
driver: 'kv-remote' as const,
|
|
36
|
+
ttl: Env.getInt('CACHE_KV_TTL', 3600),
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
keyPrefix: Env.get('CACHE_KEY_PREFIX', 'zintrust:'),
|
|
40
|
+
ttl: Env.getInt('CACHE_TTL', 3600),
|
|
41
|
+
} satisfies CacheConfigOverrides;
|
|
@@ -1,38 +1,74 @@
|
|
|
1
|
-
|
|
2
|
-
* Database Configuration (template)
|
|
3
|
-
*
|
|
4
|
-
* This file is intentionally kept simple and editable:
|
|
5
|
-
* - Developers can add/remove connections in `connections`.
|
|
6
|
-
* - Developers can edit `databaseConfigObj` to customize behavior.
|
|
7
|
-
*
|
|
8
|
-
* Core owns the default logic (sqlite path/name resolution, env handling, etc.).
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { databaseConfig as coreDatabaseConfig } from '@zintrust/core';
|
|
12
|
-
import type { DatabaseConfigShape, DatabaseConnections } from '@zintrust/core';
|
|
1
|
+
import { Env, type DatabaseConfigOverrides } from '@zintrust/core';
|
|
13
2
|
|
|
14
3
|
/**
|
|
15
|
-
*
|
|
4
|
+
* Database Configuration (default override)
|
|
16
5
|
*
|
|
17
|
-
*
|
|
6
|
+
* Keep this file declarative:
|
|
7
|
+
* - Core owns driver setup and env parsing/default logic.
|
|
8
|
+
* - Projects can override config by editing values below.
|
|
18
9
|
*/
|
|
19
|
-
export const connections = {
|
|
20
|
-
sqlite: coreDatabaseConfig.connections.sqlite,
|
|
21
|
-
d1: coreDatabaseConfig.connections.d1,
|
|
22
|
-
'd1-remote': coreDatabaseConfig.connections['d1-remote'],
|
|
23
|
-
postgresql: coreDatabaseConfig.connections.postgresql,
|
|
24
|
-
mysql: coreDatabaseConfig.connections.mysql,
|
|
25
|
-
} satisfies DatabaseConnections;
|
|
26
10
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
connections,
|
|
35
|
-
} satisfies DatabaseConfigShape;
|
|
11
|
+
const parseReadHosts = (raw: string): string[] | undefined => {
|
|
12
|
+
const list = String(raw ?? '')
|
|
13
|
+
.split(',')
|
|
14
|
+
.map((v) => v.trim())
|
|
15
|
+
.filter((v) => v.length > 0);
|
|
16
|
+
return list.length > 0 ? list : undefined;
|
|
17
|
+
};
|
|
36
18
|
|
|
37
|
-
export
|
|
38
|
-
|
|
19
|
+
export default {
|
|
20
|
+
default: Env.get('DB_CONNECTION', 'mysql'),
|
|
21
|
+
connections: {
|
|
22
|
+
sqlite: {
|
|
23
|
+
driver: 'sqlite' as const,
|
|
24
|
+
database: 'database/sqlite.db',
|
|
25
|
+
migrations: 'database/migrations',
|
|
26
|
+
},
|
|
27
|
+
postgresql: {
|
|
28
|
+
driver: 'postgresql' as const,
|
|
29
|
+
host: Env.get('DB_HOST', 'localhost'),
|
|
30
|
+
port: Env.getInt('DB_PORT_POSTGRESQL', 5432),
|
|
31
|
+
database: Env.get('DB_DATABASE_POSTGRESQL', 'postgres'),
|
|
32
|
+
username: Env.get('DB_USERNAME_POSTGRESQL', 'postgres'),
|
|
33
|
+
password: Env.get('DB_PASSWORD_POSTGRESQL', 'pass'),
|
|
34
|
+
ssl: Env.getBool('DB_SSL', false),
|
|
35
|
+
readHosts: parseReadHosts(Env.get('DB_READ_HOSTS_POSTGRESQL', '127.0.0.1')),
|
|
36
|
+
pooling: {
|
|
37
|
+
enabled: Env.getBool('DB_POOLING', true),
|
|
38
|
+
min: Env.getInt('DB_POOL_MIN', 5),
|
|
39
|
+
max: Env.getInt('DB_POOL_MAX', 20),
|
|
40
|
+
idleTimeout: Env.getInt('DB_IDLE_TIMEOUT', 30000),
|
|
41
|
+
connectionTimeout: Env.getInt('DB_CONNECTION_TIMEOUT', 10000),
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
mysql: {
|
|
45
|
+
driver: 'mysql' as const,
|
|
46
|
+
host: Env.get('DB_HOST', 'localhost'),
|
|
47
|
+
port: Env.getInt('DB_PORT', 3306),
|
|
48
|
+
database: Env.get('DB_DATABASE', 'zintrust'),
|
|
49
|
+
username: Env.get('DB_USERNAME', 'root'),
|
|
50
|
+
password: Env.get('DB_PASSWORD', 'pass'),
|
|
51
|
+
readHosts: parseReadHosts(Env.get('DB_READ_HOSTS', '127.0.0.1')),
|
|
52
|
+
pooling: {
|
|
53
|
+
enabled: Env.getBool('DB_POOLING', true),
|
|
54
|
+
min: Env.getInt('DB_POOL_MIN', 5),
|
|
55
|
+
max: Env.getInt('DB_POOL_MAX', 20),
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
sqlserver: {
|
|
59
|
+
driver: 'sqlserver' as const,
|
|
60
|
+
host: Env.get('DB_HOST_MSSQL', Env.get('DB_HOST', 'localhost')),
|
|
61
|
+
port: Env.getInt('DB_PORT_MSSQL', 1433),
|
|
62
|
+
database: Env.get('DB_DATABASE_MSSQL', 'zintrust'),
|
|
63
|
+
username: Env.get('DB_USERNAME_MSSQL', 'sa'),
|
|
64
|
+
password: Env.get('DB_PASSWORD_MSSQL', 'pass'),
|
|
65
|
+
readHosts: parseReadHosts(Env.get('DB_READ_HOSTS_MSSQL', '127.0.0.1')),
|
|
66
|
+
},
|
|
67
|
+
d1: {
|
|
68
|
+
driver: 'd1' as const,
|
|
69
|
+
},
|
|
70
|
+
'd1-remote': {
|
|
71
|
+
driver: 'd1-remote' as const,
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
} satisfies DatabaseConfigOverrides;
|