aegisnode 0.0.3 → 0.0.5
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 +1613 -1471
- package/package.json +1 -1
- package/scripts/smoke-test.js +186 -2
- package/src/cli/commands/createapp.js +11 -174
- package/src/cli/commands/doctor.js +98 -19
- package/src/cli/commands/fixapp.js +65 -0
- package/src/cli/commands/generateloader.js +37 -0
- package/src/cli/commands/startproject.js +1 -1
- package/src/cli/index.js +32 -1
- package/src/cli/utils/apps.js +253 -0
- package/src/cli/utils/scaffolds.js +17 -3
- package/src/runtime/kernel.js +10 -0
- package/src/runtime/upload.js +48 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { ensureValidName, normalizeMountPath } from '../utils/fs.js';
|
|
3
|
+
import { resolveProjectRoot } from '../utils/project.js';
|
|
4
|
+
import {
|
|
5
|
+
detectSettingsMode,
|
|
6
|
+
ensureAppScaffold,
|
|
7
|
+
readAppsConfig,
|
|
8
|
+
updateAppRegistry,
|
|
9
|
+
updateProjectRoutesFile,
|
|
10
|
+
} from '../utils/apps.js';
|
|
11
|
+
|
|
12
|
+
export async function runFixApp({ appName, projectRoot, mount } = {}) {
|
|
13
|
+
if (!appName) {
|
|
14
|
+
throw new Error('Missing app name. Usage: aegisnode fix --app <app-name>');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
ensureValidName(appName, 'app');
|
|
18
|
+
|
|
19
|
+
const resolvedRoot = await resolveProjectRoot(projectRoot || process.cwd());
|
|
20
|
+
const settingsMode = await detectSettingsMode(resolvedRoot);
|
|
21
|
+
const existingApps = await readAppsConfig(settingsMode);
|
|
22
|
+
const existingApp = existingApps.find((entry) => entry.name === appName) || null;
|
|
23
|
+
const appMount = existingApp?.mount || normalizeMountPath(mount || `/${appName}`);
|
|
24
|
+
|
|
25
|
+
const scaffoldResult = await ensureAppScaffold(resolvedRoot, appName, { overwrite: false });
|
|
26
|
+
|
|
27
|
+
let registryUpdated = false;
|
|
28
|
+
if (!existingApp) {
|
|
29
|
+
await updateAppRegistry(
|
|
30
|
+
resolvedRoot,
|
|
31
|
+
[...existingApps, { name: appName, mount: appMount }],
|
|
32
|
+
settingsMode,
|
|
33
|
+
);
|
|
34
|
+
registryUpdated = true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const routesResult = await updateProjectRoutesFile(resolvedRoot, appName, appMount);
|
|
38
|
+
const relativeWritten = scaffoldResult.written.map((target) => path.relative(resolvedRoot, target));
|
|
39
|
+
|
|
40
|
+
if (relativeWritten.length === 0 && !registryUpdated && !routesResult.updatedImport && !routesResult.updatedRoute) {
|
|
41
|
+
console.log(`App "${appName}" is already complete.`);
|
|
42
|
+
} else {
|
|
43
|
+
console.log(`App "${appName}" repaired at ${resolvedRoot}/apps/${appName}`);
|
|
44
|
+
if (relativeWritten.length > 0) {
|
|
45
|
+
console.log(`Created missing files: ${relativeWritten.join(', ')}`);
|
|
46
|
+
}
|
|
47
|
+
if (registryUpdated) {
|
|
48
|
+
console.log(`Added app "${appName}" to settings.apps with mount ${appMount}`);
|
|
49
|
+
}
|
|
50
|
+
if (routesResult.updatedImport || routesResult.updatedRoute) {
|
|
51
|
+
console.log(`Updated routes.js registration for app "${appName}"`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
rootDir: resolvedRoot,
|
|
57
|
+
appName,
|
|
58
|
+
mount: appMount,
|
|
59
|
+
createdFiles: scaffoldResult.written,
|
|
60
|
+
skippedFiles: scaffoldResult.skipped,
|
|
61
|
+
registryUpdated,
|
|
62
|
+
routesUpdated: routesResult.updatedImport || routesResult.updatedRoute,
|
|
63
|
+
routesFile: routesResult.routesFile,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { exists, writeFile } from '../utils/fs.js';
|
|
3
|
+
import { resolveProjectRoot } from '../utils/project.js';
|
|
4
|
+
import { renderProjectAppJs, renderProjectLoaderCjs } from '../utils/scaffolds.js';
|
|
5
|
+
|
|
6
|
+
async function ensureStartupFile(rootDir, fileName, content, output) {
|
|
7
|
+
const target = path.join(rootDir, fileName);
|
|
8
|
+
if (await exists(target)) {
|
|
9
|
+
output.log(`${fileName} already exists.`);
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
await writeFile(target, content);
|
|
14
|
+
output.log(`Generated ${fileName}.`);
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function runGenerateLoader({
|
|
19
|
+
projectRoot,
|
|
20
|
+
output = console,
|
|
21
|
+
} = {}) {
|
|
22
|
+
const resolvedRoot = await resolveProjectRoot(projectRoot || process.cwd());
|
|
23
|
+
const createdApp = await ensureStartupFile(resolvedRoot, 'app.js', renderProjectAppJs(), output);
|
|
24
|
+
const createdLoader = await ensureStartupFile(resolvedRoot, 'loader.cjs', renderProjectLoaderCjs(), output);
|
|
25
|
+
|
|
26
|
+
if (!createdApp && !createdLoader) {
|
|
27
|
+
output.log(`Startup entry files already exist in ${resolvedRoot}`);
|
|
28
|
+
} else {
|
|
29
|
+
output.log(`Startup entry files are ready in ${resolvedRoot}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
rootDir: resolvedRoot,
|
|
34
|
+
createdApp,
|
|
35
|
+
createdLoader,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -53,7 +53,7 @@ async function createBaseProjectFiles(projectRoot, projectName) {
|
|
|
53
53
|
await writeFile(path.join(projectRoot, '.env'), renderProjectEnv(appSecret));
|
|
54
54
|
await writeFile(path.join(projectRoot, '.env.example'), renderEnvExample());
|
|
55
55
|
|
|
56
|
-
await writeFile(path.join(projectRoot, 'settings.js'), renderProjectSettings(projectName, apps));
|
|
56
|
+
await writeFile(path.join(projectRoot, 'settings.js'), renderProjectSettings(projectName, apps, appSecret));
|
|
57
57
|
await writeFile(path.join(projectRoot, 'routes.js'), renderProjectRoutes());
|
|
58
58
|
}
|
|
59
59
|
|
package/src/cli/index.js
CHANGED
|
@@ -4,6 +4,8 @@ import { runServer } from './commands/runserver.js';
|
|
|
4
4
|
import { generateArtifact } from './commands/generate.js';
|
|
5
5
|
import { runDoctor } from './commands/doctor.js';
|
|
6
6
|
import { runUpdateDependencies } from './commands/updatedeps.js';
|
|
7
|
+
import { runGenerateLoader } from './commands/generateloader.js';
|
|
8
|
+
import { runFixApp } from './commands/fixapp.js';
|
|
7
9
|
|
|
8
10
|
function printHelp() {
|
|
9
11
|
console.log(`AegisNode CLI
|
|
@@ -11,9 +13,11 @@ function printHelp() {
|
|
|
11
13
|
Usage:
|
|
12
14
|
aegisnode startproject <project-name>
|
|
13
15
|
aegisnode createapp <app-name> [--project <path>] [--mount </path>]
|
|
16
|
+
aegisnode fix [--app <app-name>] [--project <path>]
|
|
14
17
|
aegisnode generate <type> <name> --app <app-name> [--project <path>]
|
|
15
18
|
aegisnode runserver [--project <path>] [--port <number>]
|
|
16
|
-
aegisnode
|
|
19
|
+
aegisnode generateloader [--project <path>]
|
|
20
|
+
aegisnode doctor [--app <app-name>] [--project <path>]
|
|
17
21
|
aegisnode updatedeps [--project <path>]
|
|
18
22
|
|
|
19
23
|
Examples:
|
|
@@ -22,8 +26,11 @@ Examples:
|
|
|
22
26
|
npm install
|
|
23
27
|
aegisnode runserver
|
|
24
28
|
aegisnode createapp users
|
|
29
|
+
aegisnode fix --app users
|
|
25
30
|
aegisnode generate view user --app users
|
|
26
31
|
aegisnode generate validator user --app users
|
|
32
|
+
aegisnode generateloader --project blog
|
|
33
|
+
aegisnode doctor --app users --project blog
|
|
27
34
|
aegisnode updatedeps --project blog
|
|
28
35
|
`);
|
|
29
36
|
}
|
|
@@ -120,8 +127,32 @@ export async function runCli(argv) {
|
|
|
120
127
|
}
|
|
121
128
|
|
|
122
129
|
case 'doctor': {
|
|
130
|
+
if (positional.length > 0) {
|
|
131
|
+
throw new Error('Doctor app target must use --app <app-name>. Usage: aegisnode doctor [--app <app-name>] [--project <path>]');
|
|
132
|
+
}
|
|
123
133
|
await runDoctor({
|
|
124
134
|
projectRoot: flags.project ? String(flags.project) : process.cwd(),
|
|
135
|
+
appName: flags.app ? String(flags.app) : null,
|
|
136
|
+
});
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
case 'fix':
|
|
141
|
+
case 'fixapp': {
|
|
142
|
+
if (positional.length > 0) {
|
|
143
|
+
throw new Error('Fix app target must use --app <app-name>. Usage: aegisnode fix [--app <app-name>] [--project <path>]');
|
|
144
|
+
}
|
|
145
|
+
await runFixApp({
|
|
146
|
+
appName: flags.app ? String(flags.app) : undefined,
|
|
147
|
+
projectRoot: flags.project ? String(flags.project) : process.cwd(),
|
|
148
|
+
});
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
case 'generateloader':
|
|
153
|
+
case 'loader': {
|
|
154
|
+
await runGenerateLoader({
|
|
155
|
+
projectRoot: flags.project ? String(flags.project) : process.cwd(),
|
|
125
156
|
});
|
|
126
157
|
return;
|
|
127
158
|
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { pathToFileURL } from 'url';
|
|
4
|
+
import { ensureDir, ensureValidName, exists, normalizeMountPath, writeFile } from './fs.js';
|
|
5
|
+
import {
|
|
6
|
+
renderAppModelTest,
|
|
7
|
+
renderAppModelsFile,
|
|
8
|
+
renderAppRoutes,
|
|
9
|
+
renderAppRoutesTest,
|
|
10
|
+
renderAppServiceTest,
|
|
11
|
+
renderAppServicesFile,
|
|
12
|
+
renderAppSubscribersFile,
|
|
13
|
+
renderAppUtilsFile,
|
|
14
|
+
renderAppValidatorTest,
|
|
15
|
+
renderAppValidatorsFile,
|
|
16
|
+
renderAppViewsFile,
|
|
17
|
+
renderSettingsApps,
|
|
18
|
+
} from './scaffolds.js';
|
|
19
|
+
|
|
20
|
+
export const APPS_START = '// AEGIS_APPS_START';
|
|
21
|
+
export const APPS_END = '// AEGIS_APPS_END';
|
|
22
|
+
|
|
23
|
+
function renderAppEntries(apps) {
|
|
24
|
+
return apps
|
|
25
|
+
.map((app) => ` { name: ${JSON.stringify(app.name)}, mount: ${JSON.stringify(app.mount)} },`)
|
|
26
|
+
.join('\n');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function escapeRegExp(value) {
|
|
30
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function toImportName(appName) {
|
|
34
|
+
const safe = appName
|
|
35
|
+
.split(/[-_\s]+/)
|
|
36
|
+
.filter(Boolean)
|
|
37
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
38
|
+
.join('');
|
|
39
|
+
|
|
40
|
+
if (!safe) {
|
|
41
|
+
return 'appRoutes';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return `${safe.charAt(0).toLowerCase()}${safe.slice(1)}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function readDefaultExport(filePath) {
|
|
48
|
+
const moduleUrl = `${pathToFileURL(filePath).href}?t=${Date.now()}`;
|
|
49
|
+
const loaded = await import(moduleUrl);
|
|
50
|
+
return loaded?.default;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function detectSettingsMode(projectRoot) {
|
|
54
|
+
const single = path.join(projectRoot, 'settings.js');
|
|
55
|
+
const split = path.join(projectRoot, 'settings', 'apps.js');
|
|
56
|
+
|
|
57
|
+
if (await exists(single)) {
|
|
58
|
+
return { mode: 'single', file: single };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (await exists(split)) {
|
|
62
|
+
return { mode: 'split', file: split };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
throw new Error(`Not an AegisNode project root: missing ${single} (or legacy ${split})`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function readAppsConfig(settingsMode) {
|
|
69
|
+
const normalizeApp = (entry) => {
|
|
70
|
+
if (!entry || typeof entry !== 'object' || typeof entry.name !== 'string') {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
ensureValidName(entry.name, 'app');
|
|
75
|
+
return {
|
|
76
|
+
name: entry.name,
|
|
77
|
+
mount: normalizeMountPath(entry.mount || `/${entry.name}`),
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
if (settingsMode.mode === 'single') {
|
|
82
|
+
const settings = await readDefaultExport(settingsMode.file);
|
|
83
|
+
const apps = settings?.apps;
|
|
84
|
+
|
|
85
|
+
if (!Array.isArray(apps)) {
|
|
86
|
+
throw new Error(`settings.js must export { apps: [] }. File: ${settingsMode.file}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return apps.map(normalizeApp).filter(Boolean);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const apps = await readDefaultExport(settingsMode.file);
|
|
93
|
+
if (!Array.isArray(apps)) {
|
|
94
|
+
throw new Error(`settings/apps.js must export an array. File: ${settingsMode.file}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return apps.map(normalizeApp).filter(Boolean);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function updateSingleSettingsApps(settingsFile, apps) {
|
|
101
|
+
const current = await fs.readFile(settingsFile, 'utf8');
|
|
102
|
+
|
|
103
|
+
if (!current.includes(APPS_START) || !current.includes(APPS_END)) {
|
|
104
|
+
throw new Error(`settings.js is missing ${APPS_START}/${APPS_END} markers: ${settingsFile}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const replacement = `${APPS_START}\n${renderAppEntries(apps)}\n ${APPS_END}`;
|
|
108
|
+
const updated = current.replace(/\/\/ AEGIS_APPS_START[\s\S]*?\/\/ AEGIS_APPS_END/m, replacement);
|
|
109
|
+
|
|
110
|
+
await writeFile(settingsFile, updated);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function updateAppRegistry(projectRoot, apps, settingsMode) {
|
|
114
|
+
if (settingsMode.mode === 'single') {
|
|
115
|
+
await updateSingleSettingsApps(settingsMode.file, apps);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
await writeFile(path.join(projectRoot, 'settings', 'apps.js'), renderSettingsApps(apps));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function getAppScaffoldEntries(appName) {
|
|
123
|
+
const appRoot = path.join('apps', appName);
|
|
124
|
+
return [
|
|
125
|
+
{
|
|
126
|
+
target: path.join(appRoot, 'views.js'),
|
|
127
|
+
content: renderAppViewsFile(appName),
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
target: path.join(appRoot, 'models.js'),
|
|
131
|
+
content: renderAppModelsFile(appName),
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
target: path.join(appRoot, 'validators.js'),
|
|
135
|
+
content: renderAppValidatorsFile(appName),
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
target: path.join(appRoot, 'services.js'),
|
|
139
|
+
content: renderAppServicesFile(appName),
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
target: path.join(appRoot, 'utils.js'),
|
|
143
|
+
content: renderAppUtilsFile(),
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
target: path.join(appRoot, 'subscribers.js'),
|
|
147
|
+
content: renderAppSubscribersFile(appName),
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
target: path.join(appRoot, 'routes.js'),
|
|
151
|
+
content: renderAppRoutes(appName),
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
target: path.join(appRoot, 'tests', 'models.test.js'),
|
|
155
|
+
content: renderAppModelTest(appName),
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
target: path.join(appRoot, 'tests', 'validators.test.js'),
|
|
159
|
+
content: renderAppValidatorTest(appName),
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
target: path.join(appRoot, 'tests', 'services.test.js'),
|
|
163
|
+
content: renderAppServiceTest(appName),
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
target: path.join(appRoot, 'tests', 'routes.test.js'),
|
|
167
|
+
content: renderAppRoutesTest(appName),
|
|
168
|
+
},
|
|
169
|
+
];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function ensureAppScaffold(projectRoot, appName, { overwrite = false } = {}) {
|
|
173
|
+
ensureValidName(appName, 'app');
|
|
174
|
+
|
|
175
|
+
const appRoot = path.join(projectRoot, 'apps', appName);
|
|
176
|
+
await ensureDir(appRoot);
|
|
177
|
+
await ensureDir(path.join(appRoot, 'tests'));
|
|
178
|
+
|
|
179
|
+
const written = [];
|
|
180
|
+
const skipped = [];
|
|
181
|
+
|
|
182
|
+
for (const entry of getAppScaffoldEntries(appName)) {
|
|
183
|
+
const target = path.join(projectRoot, entry.target);
|
|
184
|
+
if (!overwrite && await exists(target)) {
|
|
185
|
+
skipped.push(target);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
await writeFile(target, entry.content);
|
|
190
|
+
written.push(target);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
appRoot,
|
|
195
|
+
written,
|
|
196
|
+
skipped,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function updateProjectRoutesFile(projectRoot, appName, mountPath) {
|
|
201
|
+
const routesFile = path.join(projectRoot, 'routes.js');
|
|
202
|
+
if (!(await exists(routesFile))) {
|
|
203
|
+
// Keep backward compatibility for legacy projects that still use routes/index.js.
|
|
204
|
+
return {
|
|
205
|
+
routesFile,
|
|
206
|
+
updatedImport: false,
|
|
207
|
+
updatedRoute: false,
|
|
208
|
+
skipped: true,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const importName = toImportName(appName);
|
|
213
|
+
const importLine = `import ${importName} from './apps/${appName}/routes.js';`;
|
|
214
|
+
const routeLine = ` route.use(${JSON.stringify(mountPath)}, ${importName});`;
|
|
215
|
+
const routePattern = new RegExp(`route\\.use\\([^\\n]*,\\s*${escapeRegExp(importName)}\\s*\\);`);
|
|
216
|
+
|
|
217
|
+
let content = await fs.readFile(routesFile, 'utf8');
|
|
218
|
+
let updatedImport = false;
|
|
219
|
+
let updatedRoute = false;
|
|
220
|
+
|
|
221
|
+
if (!content.includes(importLine)) {
|
|
222
|
+
if (content.includes('// AEGIS_APP_IMPORTS_START') && content.includes('// AEGIS_APP_IMPORTS_END')) {
|
|
223
|
+
content = content.replace('// AEGIS_APP_IMPORTS_END', `${importLine}\n// AEGIS_APP_IMPORTS_END`);
|
|
224
|
+
} else {
|
|
225
|
+
content = `${importLine}\n${content}`;
|
|
226
|
+
}
|
|
227
|
+
updatedImport = true;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (!routePattern.test(content)) {
|
|
231
|
+
if (content.includes('// AEGIS_PROJECT_APP_ROUTES_START') && content.includes('// AEGIS_PROJECT_APP_ROUTES_END')) {
|
|
232
|
+
content = content.replace(' // AEGIS_PROJECT_APP_ROUTES_END', `${routeLine}\n // AEGIS_PROJECT_APP_ROUTES_END`);
|
|
233
|
+
updatedRoute = true;
|
|
234
|
+
} else {
|
|
235
|
+
const match = content.match(/register\s*\([^)]*\)\s*{[\s\S]*?\n\s*}/m);
|
|
236
|
+
if (match) {
|
|
237
|
+
content = content.replace(match[0], `${match[0]}\n${routeLine}`);
|
|
238
|
+
updatedRoute = true;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (updatedImport || updatedRoute) {
|
|
244
|
+
await writeFile(routesFile, content);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
routesFile,
|
|
249
|
+
updatedImport,
|
|
250
|
+
updatedRoute,
|
|
251
|
+
skipped: false,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
@@ -53,7 +53,7 @@ export function renderProjectLoaderCjs() {
|
|
|
53
53
|
`;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
export function renderProjectSettings(projectName, apps) {
|
|
56
|
+
export function renderProjectSettings(projectName, apps, appSecret = '') {
|
|
57
57
|
return `export default {
|
|
58
58
|
appName: '${projectName}',
|
|
59
59
|
env: process.env.NODE_ENV || 'development',
|
|
@@ -61,8 +61,9 @@ export function renderProjectSettings(projectName, apps) {
|
|
|
61
61
|
port: process.env.PORT ? Number(process.env.PORT) : 3000,
|
|
62
62
|
trustProxy: false,
|
|
63
63
|
security: {
|
|
64
|
-
// Loaded from .env by default.
|
|
65
|
-
|
|
64
|
+
// Loaded from .env by default. Scaffold also embeds the generated secret as a fallback.
|
|
65
|
+
// Replace or rotate APP_SECRET in production.
|
|
66
|
+
appSecret: process.env.APP_SECRET || ${JSON.stringify(appSecret)},
|
|
66
67
|
},
|
|
67
68
|
logging: {
|
|
68
69
|
level: process.env.LOG_LEVEL || 'info',
|
|
@@ -402,6 +403,19 @@ export default {
|
|
|
402
403
|
`;
|
|
403
404
|
}
|
|
404
405
|
|
|
406
|
+
export function renderAppUtilsFile() {
|
|
407
|
+
return `/**
|
|
408
|
+
* App-local pure utilities.
|
|
409
|
+
*
|
|
410
|
+
* Use this file for small reusable functions that belong only to this app.
|
|
411
|
+
* Keep database access in models, business workflows in services, and request
|
|
412
|
+
* validation/sanitization in validators.
|
|
413
|
+
*/
|
|
414
|
+
|
|
415
|
+
export {};
|
|
416
|
+
`;
|
|
417
|
+
}
|
|
418
|
+
|
|
405
419
|
export function renderAppSubscribersFile(appName) {
|
|
406
420
|
return `export default function register${toPascalCase(appName)}Subscribers({ events, logger }) {
|
|
407
421
|
events.subscribe('app.booted', ({ appName }) => {
|
package/src/runtime/kernel.js
CHANGED
|
@@ -3148,6 +3148,16 @@ function attachCsrfProtection(expressApp, config, logger, auth = null) {
|
|
|
3148
3148
|
}
|
|
3149
3149
|
|
|
3150
3150
|
const provided = extractCsrfToken(req, csrfConfig);
|
|
3151
|
+
if (!provided && isMultipartRequestContentType(req.headers?.['content-type'])) {
|
|
3152
|
+
req.aegis = req.aegis || {};
|
|
3153
|
+
req.aegis.csrf = {
|
|
3154
|
+
deferredMultipart: true,
|
|
3155
|
+
fieldName: csrfConfig.fieldName,
|
|
3156
|
+
token,
|
|
3157
|
+
};
|
|
3158
|
+
return next();
|
|
3159
|
+
}
|
|
3160
|
+
|
|
3151
3161
|
if (!provided || !constantTimeEqual(provided, token)) {
|
|
3152
3162
|
return res.status(403).json({ error: 'CSRF token missing or invalid' });
|
|
3153
3163
|
}
|
package/src/runtime/upload.js
CHANGED
|
@@ -120,6 +120,47 @@ function createUploadError(code, message, statusCode) {
|
|
|
120
120
|
return error;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
function constantTimeEqual(left, right) {
|
|
124
|
+
if (typeof left !== 'string' || typeof right !== 'string') {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const a = Buffer.from(left);
|
|
129
|
+
const b = Buffer.from(right);
|
|
130
|
+
if (a.length !== b.length) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
return crypto.timingSafeEqual(a, b);
|
|
136
|
+
} catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function validateDeferredMultipartCsrf(req) {
|
|
142
|
+
const csrfState = req?.aegis?.csrf;
|
|
143
|
+
if (!csrfState || csrfState.deferredMultipart !== true) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const expected = typeof csrfState.token === 'string' ? csrfState.token : '';
|
|
148
|
+
const fieldName = typeof csrfState.fieldName === 'string' && csrfState.fieldName.length > 0
|
|
149
|
+
? csrfState.fieldName
|
|
150
|
+
: '_csrf';
|
|
151
|
+
const provided = req?.body && typeof req.body === 'object'
|
|
152
|
+
? req.body[fieldName]
|
|
153
|
+
: '';
|
|
154
|
+
|
|
155
|
+
delete req.aegis.csrf;
|
|
156
|
+
|
|
157
|
+
if (!expected || typeof provided !== 'string' || !constantTimeEqual(provided, expected)) {
|
|
158
|
+
return createUploadError('AEGIS_CSRF_INVALID', 'CSRF token missing or invalid', 403);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
123
164
|
function resolveUploadError(error) {
|
|
124
165
|
if (!error) {
|
|
125
166
|
return null;
|
|
@@ -207,6 +248,13 @@ function wrapUploadMiddleware(middleware) {
|
|
|
207
248
|
return (req, res, next) => {
|
|
208
249
|
middleware(req, res, (error) => {
|
|
209
250
|
if (!error) {
|
|
251
|
+
const csrfError = validateDeferredMultipartCsrf(req);
|
|
252
|
+
if (csrfError) {
|
|
253
|
+
res.status(csrfError.statusCode || 403).json({
|
|
254
|
+
error: csrfError.message || 'CSRF token missing or invalid',
|
|
255
|
+
});
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
210
258
|
next();
|
|
211
259
|
return;
|
|
212
260
|
}
|