@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.
Files changed (98) hide show
  1. package/package.json +2 -1
  2. package/src/boot/Application.d.ts.map +1 -1
  3. package/src/boot/Application.js +48 -10
  4. package/src/boot/bootstrap.js +2 -0
  5. package/src/cli/commands/MigrateCommand.d.ts.map +1 -1
  6. package/src/cli/commands/MigrateCommand.js +36 -3
  7. package/src/cli/d1/D1SqlMigrations.d.ts.map +1 -1
  8. package/src/cli/d1/D1SqlMigrations.js +6 -1
  9. package/src/cli/scaffolding/ControllerGenerator.js +4 -4
  10. package/src/cli/scaffolding/GovernanceScaffolder.js +1 -1
  11. package/src/cli/scaffolding/MigrationGenerator.js +1 -1
  12. package/src/cli/scaffolding/ModelGenerator.js +1 -1
  13. package/src/cli/scaffolding/RouteGenerator.js +1 -1
  14. package/src/cli/scaffolding/ServiceScaffolder.js +4 -4
  15. package/src/config/broadcast.d.ts +14 -28
  16. package/src/config/broadcast.d.ts.map +1 -1
  17. package/src/config/broadcast.js +69 -35
  18. package/src/config/cache.d.ts +13 -45
  19. package/src/config/cache.d.ts.map +1 -1
  20. package/src/config/cache.js +69 -25
  21. package/src/config/database.d.ts +22 -64
  22. package/src/config/database.d.ts.map +1 -1
  23. package/src/config/database.js +99 -31
  24. package/src/config/env.d.ts +6 -0
  25. package/src/config/env.d.ts.map +1 -1
  26. package/src/config/env.js +7 -0
  27. package/src/config/index.d.ts +32 -136
  28. package/src/config/index.d.ts.map +1 -1
  29. package/src/config/mail.d.ts +19 -55
  30. package/src/config/mail.d.ts.map +1 -1
  31. package/src/config/mail.js +63 -21
  32. package/src/config/middleware.d.ts +24 -0
  33. package/src/config/middleware.d.ts.map +1 -1
  34. package/src/config/middleware.js +72 -52
  35. package/src/config/notification.d.ts +14 -27
  36. package/src/config/notification.d.ts.map +1 -1
  37. package/src/config/notification.js +82 -36
  38. package/src/config/queue.d.ts +21 -51
  39. package/src/config/queue.d.ts.map +1 -1
  40. package/src/config/queue.js +72 -27
  41. package/src/config/storage.d.ts +27 -34
  42. package/src/config/storage.d.ts.map +1 -1
  43. package/src/config/storage.js +97 -56
  44. package/src/config/type.d.ts +12 -1
  45. package/src/config/type.d.ts.map +1 -1
  46. package/src/http/parsers/MultipartParser.d.ts.map +1 -1
  47. package/src/http/parsers/MultipartParser.js +69 -42
  48. package/src/index.d.ts +9 -5
  49. package/src/index.d.ts.map +1 -1
  50. package/src/index.js +1 -0
  51. package/src/microservices/PostgresAdapter.d.ts.map +1 -1
  52. package/src/microservices/PostgresAdapter.js +0 -1
  53. package/src/migrations/MigratorFactory.d.ts.map +1 -1
  54. package/src/migrations/MigratorFactory.js +18 -2
  55. package/src/node-singletons/fs.d.ts +1 -1
  56. package/src/node-singletons/fs.d.ts.map +1 -1
  57. package/src/node-singletons/fs.js +1 -1
  58. package/src/orm/Database.d.ts +2 -1
  59. package/src/orm/Database.d.ts.map +1 -1
  60. package/src/orm/Database.js +110 -67
  61. package/src/orm/DatabaseAdapter.d.ts +1 -0
  62. package/src/orm/DatabaseAdapter.d.ts.map +1 -1
  63. package/src/orm/DatabaseRuntimeRegistration.d.ts.map +1 -1
  64. package/src/orm/DatabaseRuntimeRegistration.js +12 -0
  65. package/src/orm/QueryBuilder.d.ts +1 -1
  66. package/src/orm/QueryBuilder.d.ts.map +1 -1
  67. package/src/orm/QueryBuilder.js +4 -3
  68. package/src/orm/adapters/SQLiteAdapter.js +1 -1
  69. package/src/performance/Optimizer.d.ts +6 -6
  70. package/src/performance/Optimizer.d.ts.map +1 -1
  71. package/src/performance/Optimizer.js +133 -52
  72. package/src/routing/doc.d.ts +4 -5
  73. package/src/routing/doc.d.ts.map +1 -1
  74. package/src/routing/doc.js +35 -20
  75. package/src/routing/publicRoot.d.ts +9 -0
  76. package/src/routing/publicRoot.d.ts.map +1 -1
  77. package/src/routing/publicRoot.js +63 -2
  78. package/src/runtime/StartupConfigFileRegistry.d.ts +20 -0
  79. package/src/runtime/StartupConfigFileRegistry.d.ts.map +1 -0
  80. package/src/runtime/StartupConfigFileRegistry.js +44 -0
  81. package/src/runtime/useFileLoader.d.ts +26 -0
  82. package/src/runtime/useFileLoader.d.ts.map +1 -0
  83. package/src/runtime/useFileLoader.js +188 -0
  84. package/src/scripts/TemplateSync.js +4 -4
  85. package/src/security/XssProtection.d.ts.map +1 -1
  86. package/src/security/XssProtection.js +62 -14
  87. package/src/templates/project/basic/config/broadcast.ts.tpl +33 -17
  88. package/src/templates/project/basic/config/cache.ts.tpl +35 -17
  89. package/src/templates/project/basic/config/database.ts.tpl +68 -32
  90. package/src/templates/project/basic/config/logging/HttpLogger.ts.tpl +7 -114
  91. package/src/templates/project/basic/config/mail.ts.tpl +59 -13
  92. package/src/templates/project/basic/config/notification.ts.tpl +28 -17
  93. package/src/templates/project/basic/config/queue.ts.tpl +49 -17
  94. package/src/templates/project/basic/config/storage.ts.tpl +55 -18
  95. package/src/templates/project/basic/config/type.ts.tpl +0 -1
  96. package/src/templates/project/basic/src/index.ts.tpl +3 -0
  97. package/src/templates/project/basic/config/logging/KvLogger.ts.tpl +0 -181
  98. 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 '@zintrust/core';")
144
- .replaceAll('from "../env";', 'from "@zintrust/core";')
145
- .replaceAll("from './env';", "from '@zintrust/core';")
146
- .replaceAll('from "./env";', 'from "@zintrust/core";')
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;AA6SH;;GAEG;AACH,eAAO,MAAM,UAAU,GAAI,KAAK,OAAO,KAAG,MAGzC,CAAC;AAEF,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IACjC,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAChC,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAAC;CAClC;AAED;;;;GAIG;AACH,eAAO,MAAM,aAAa,EAAE,cAO1B,CAAC"}
1
+ {"version":3,"file":"XssProtection.d.ts","sourceRoot":"","sources":["../../../src/security/XssProtection.ts"],"names":[],"mappings":"AAAA;;;;GAIG;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 sanitizeHtml = (html) => {
31
- if (typeof html !== 'string') {
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 = html;
36
- let prevScriptSanitized;
33
+ let sanitized = input;
34
+ let prevSanitized;
35
+ let loopCount = 0;
37
36
  do {
38
- prevScriptSanitized = sanitized;
37
+ prevSanitized = sanitized;
39
38
  sanitized = sanitized.replaceAll(/<script\b[\s\S]*?<\/script[^<]*?>/gi, '');
40
- } while (sanitized !== prevScriptSanitized);
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 previousSanitized;
56
+ let sanitized = input;
57
+ let prevSanitized;
58
+ let loopCount = 0;
46
59
  do {
47
- previousSanitized = sanitized;
60
+ prevSanitized = sanitized;
48
61
  sanitized = sanitized.replaceAll(/\bon\w+\s*=\s*(?:'[^']*'|"[^"]*"|`[^`]*`|[^\s>]*)/gi, '');
49
- } while (sanitized !== previousSanitized);
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="jav&#x61;script:..." or href="java\nscript:..." or href="%6a%61..."
53
- sanitized = sanitized.replaceAll(/(\s)(href|src|action|formaction|xlink:href)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi, (match, _leadingWhitespace, _attributeName, doubleQuotedValue, singleQuotedValue, unquotedValue) => {
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
- } while (sanitized !== prevSanitized);
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 (template)
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 values by editing `drivers` and `broadcastConfigObj`.
8
+ * - Projects can override config by editing values below.
7
9
  */
8
10
 
9
- import { broadcastConfig as coreBroadcastConfig } from '@zintrust/core';
10
-
11
- type BroadcastConfigShape = typeof coreBroadcastConfig;
12
-
13
- export const drivers = {
14
- ...coreBroadcastConfig.drivers,
15
- } satisfies BroadcastConfigShape['drivers'];
16
-
17
- export const broadcastConfigObj = {
18
- ...coreBroadcastConfig,
19
- drivers,
20
- } satisfies BroadcastConfigShape;
21
-
22
- const broadcastConfig = broadcastConfigObj;
23
- export default broadcastConfig;
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 (template)
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 values by editing `drivers` and `cacheConfigObj`.
8
+ * - Projects can override config by editing values below.
7
9
  */
8
10
 
9
- import { cacheConfig as coreCacheConfig } from '@zintrust/core';
10
-
11
- type CacheConfigShape = typeof coreCacheConfig;
12
-
13
- export const drivers = {
14
- ...coreCacheConfig.drivers,
15
- } satisfies CacheConfigShape['drivers'];
16
-
17
- export const cacheConfigObj = {
18
- ...coreCacheConfig,
19
- drivers,
20
- } satisfies CacheConfigShape;
21
-
22
- export const cacheConfig = cacheConfigObj;
23
- export type CacheConfig = typeof cacheConfig;
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
- * Editable connections map.
4
+ * Database Configuration (default override)
16
5
  *
17
- * Defaults are sourced from core so you inherit framework-safe behavior.
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
- * Editable database config object.
29
- *
30
- * You can override any top-level keys from core, while keeping core defaults.
31
- */
32
- export const databaseConfigObj = {
33
- ...coreDatabaseConfig,
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 const databaseConfig = databaseConfigObj;
38
- export type DatabaseConfig = typeof databaseConfig;
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;