aegisnode 0.0.2 → 0.0.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 +1559 -1462
- package/package.json +6 -3
- package/scripts/smoke-test.js +99 -2
- package/src/cli/commands/doctor.js +25 -0
- package/src/cli/commands/generateloader.js +37 -0
- package/src/cli/commands/startproject.js +1 -1
- package/src/cli/index.js +11 -0
- package/src/cli/utils/scaffolds.js +4 -3
- package/src/runtime/kernel.js +10 -0
- package/src/runtime/upload.js +48 -0
- package/assets/aegisnode-banner.png +0 -0
- package/assets/aegisnode-banner.svg +0 -66
- package/assets/aegisnode-icon.png +0 -0
- package/assets/aegisnode-icon.svg +0 -43
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aegisnode",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "A view-first Node.js framework for modular web apps and JSON APIs with CLI scaffolding, runtime injection, auth, uploads, i18n, mail, and WebSocket support.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
"test": "node ./scripts/smoke-test.js"
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
|
-
"assets",
|
|
20
19
|
"bin",
|
|
21
20
|
"src",
|
|
22
21
|
"scripts",
|
|
@@ -38,7 +37,11 @@
|
|
|
38
37
|
"uploads",
|
|
39
38
|
"websocket"
|
|
40
39
|
],
|
|
41
|
-
"author": "",
|
|
40
|
+
"author": "jason90 <sidiki90@gmail.com>",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/jason09/aegisnode.git"
|
|
44
|
+
},
|
|
42
45
|
"license": "MIT",
|
|
43
46
|
"dependencies": {
|
|
44
47
|
"ejs": "^3.1.10",
|
package/scripts/smoke-test.js
CHANGED
|
@@ -10,6 +10,7 @@ import { createApp } from '../src/cli/commands/createapp.js';
|
|
|
10
10
|
import { generateArtifact } from '../src/cli/commands/generate.js';
|
|
11
11
|
import { createKernel } from '../src/runtime/kernel.js';
|
|
12
12
|
import { runServer } from '../src/cli/commands/runserver.js';
|
|
13
|
+
import { runGenerateLoader } from '../src/cli/commands/generateloader.js';
|
|
13
14
|
import { runProject } from '../src/index.js';
|
|
14
15
|
import { createAuthManager, normalizeAuthConfig } from '../src/runtime/auth.js';
|
|
15
16
|
import { loadProjectConfig } from '../src/runtime/config.js';
|
|
@@ -137,8 +138,10 @@ async function main() {
|
|
|
137
138
|
await startProject({ projectName, cwd: sandboxRoot });
|
|
138
139
|
const generatedProjectEnv = await fs.readFile(path.join(projectRoot, '.env'), 'utf8');
|
|
139
140
|
assert.match(generatedProjectEnv, /^APP_SECRET=.{16,}$/m);
|
|
141
|
+
const generatedAppSecret = generatedProjectEnv.match(/^APP_SECRET=(.+)$/m)?.[1]?.trim();
|
|
142
|
+
assert.ok(generatedAppSecret);
|
|
140
143
|
const generatedSettings = await fs.readFile(path.join(projectRoot, 'settings.js'), 'utf8');
|
|
141
|
-
assert.
|
|
144
|
+
assert.ok(generatedSettings.includes(`appSecret: process.env.APP_SECRET || ${JSON.stringify(generatedAppSecret)}`));
|
|
142
145
|
await assert.rejects(
|
|
143
146
|
() => runProject({
|
|
144
147
|
rootDir: projectRoot,
|
|
@@ -404,6 +407,74 @@ dkcqnJD4SGWVeG+KhA==
|
|
|
404
407
|
await fs.access(filePath);
|
|
405
408
|
}
|
|
406
409
|
|
|
410
|
+
await fs.unlink(path.join(projectRoot, 'app.js'));
|
|
411
|
+
await fs.unlink(path.join(projectRoot, 'loader.cjs'));
|
|
412
|
+
const restoredStartupEntries = await runGenerateLoader({
|
|
413
|
+
projectRoot,
|
|
414
|
+
output: {
|
|
415
|
+
log() {},
|
|
416
|
+
},
|
|
417
|
+
});
|
|
418
|
+
assert.equal(restoredStartupEntries.createdApp, true);
|
|
419
|
+
assert.equal(restoredStartupEntries.createdLoader, true);
|
|
420
|
+
await fs.access(path.join(projectRoot, 'app.js'));
|
|
421
|
+
await fs.access(path.join(projectRoot, 'loader.cjs'));
|
|
422
|
+
|
|
423
|
+
await fs.writeFile(
|
|
424
|
+
path.join(envProjectRoot, 'settings.js'),
|
|
425
|
+
`export default {
|
|
426
|
+
appName: 'envdemo',
|
|
427
|
+
env: 'production',
|
|
428
|
+
logging: {
|
|
429
|
+
level: 'info',
|
|
430
|
+
},
|
|
431
|
+
security: {
|
|
432
|
+
appSecret: process.env.APP_SECRET || '',
|
|
433
|
+
ddos: {
|
|
434
|
+
maxRequests: 120,
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
environments: {
|
|
438
|
+
default: {
|
|
439
|
+
security: {
|
|
440
|
+
ddos: {
|
|
441
|
+
windowMs: 45000,
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
production: {
|
|
446
|
+
logging: { level: 'warn' },
|
|
447
|
+
security: { ddos: { maxRequests: 80 } },
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
};
|
|
451
|
+
`,
|
|
452
|
+
'utf8',
|
|
453
|
+
);
|
|
454
|
+
await fs.unlink(path.join(envProjectRoot, 'loader.cjs'));
|
|
455
|
+
const missingLoaderDoctorReport = await runDoctor({
|
|
456
|
+
projectRoot: envProjectRoot,
|
|
457
|
+
failOnError: false,
|
|
458
|
+
output: {
|
|
459
|
+
log() {},
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
assert.equal(missingLoaderDoctorReport.summary.errors, 1);
|
|
463
|
+
assert.ok(
|
|
464
|
+
missingLoaderDoctorReport.entries.some((entry) => (
|
|
465
|
+
entry.level === 'ERROR'
|
|
466
|
+
&& /loader\.cjs is missing for production startup/.test(entry.message)
|
|
467
|
+
)),
|
|
468
|
+
);
|
|
469
|
+
const restoredProductionLoader = await runGenerateLoader({
|
|
470
|
+
projectRoot: envProjectRoot,
|
|
471
|
+
output: {
|
|
472
|
+
log() {},
|
|
473
|
+
},
|
|
474
|
+
});
|
|
475
|
+
assert.equal(restoredProductionLoader.createdLoader, true);
|
|
476
|
+
await fs.access(path.join(envProjectRoot, 'loader.cjs'));
|
|
477
|
+
|
|
407
478
|
const registryPackages = new Map([
|
|
408
479
|
['alpha', '2.0.0'],
|
|
409
480
|
['@scope/bravo', '3.4.0'],
|
|
@@ -1288,7 +1359,7 @@ export default {
|
|
|
1288
1359
|
);
|
|
1289
1360
|
await fs.writeFile(
|
|
1290
1361
|
path.join(projectRoot, 'routes.js'),
|
|
1291
|
-
`export default {\n register(route) {\n route.get('/csrf-token', (req, res) => {\n res.json({ token: req.csrfToken() });\n });\n\n route.get('/csrf-form', (req, res) => {\n return res.render('csrf-form', { layout: false });\n });\n\n route.post('/submit', (req, res) => {\n res.json({ ok: true, body: req.body || {} });\n });\n },\n};\n`,
|
|
1362
|
+
`export default {\n register(route) {\n route.get('/csrf-token', (req, res) => {\n res.json({ token: req.csrfToken() });\n });\n\n route.get('/csrf-form', (req, res) => {\n return res.render('csrf-form', { layout: false });\n });\n\n route.post('/submit', (req, res) => {\n res.json({ ok: true, body: req.body || {} });\n });\n\n route.post('/submit-upload', route.upload.none(), (req, res) => {\n res.json({ ok: true, body: req.body || {} });\n });\n },\n};\n`,
|
|
1292
1363
|
'utf8',
|
|
1293
1364
|
);
|
|
1294
1365
|
|
|
@@ -1361,6 +1432,32 @@ export default {
|
|
|
1361
1432
|
assert.equal(validJsonTokenJson.ok, true);
|
|
1362
1433
|
assert.equal(validJsonTokenJson.body.name, 'json-with-token');
|
|
1363
1434
|
|
|
1435
|
+
const missingMultipartTokenBody = new FormData();
|
|
1436
|
+
missingMultipartTokenBody.set('name', 'multipart-without-token');
|
|
1437
|
+
const missingMultipartTokenResponse = await fetch(`http://127.0.0.1:${csrfPort}/submit-upload`, {
|
|
1438
|
+
method: 'POST',
|
|
1439
|
+
headers: {
|
|
1440
|
+
cookie: csrfCookie,
|
|
1441
|
+
},
|
|
1442
|
+
body: missingMultipartTokenBody,
|
|
1443
|
+
});
|
|
1444
|
+
assert.equal(missingMultipartTokenResponse.status, 403);
|
|
1445
|
+
|
|
1446
|
+
const validMultipartTokenBody = new FormData();
|
|
1447
|
+
validMultipartTokenBody.set('_csrf', csrfTokenJson.token);
|
|
1448
|
+
validMultipartTokenBody.set('name', 'multipart-with-token');
|
|
1449
|
+
const validMultipartTokenResponse = await fetch(`http://127.0.0.1:${csrfPort}/submit-upload`, {
|
|
1450
|
+
method: 'POST',
|
|
1451
|
+
headers: {
|
|
1452
|
+
cookie: csrfCookie,
|
|
1453
|
+
},
|
|
1454
|
+
body: validMultipartTokenBody,
|
|
1455
|
+
});
|
|
1456
|
+
assert.equal(validMultipartTokenResponse.status, 200);
|
|
1457
|
+
const validMultipartTokenJson = await validMultipartTokenResponse.json();
|
|
1458
|
+
assert.equal(validMultipartTokenJson.ok, true);
|
|
1459
|
+
assert.equal(validMultipartTokenJson.body.name, 'multipart-with-token');
|
|
1460
|
+
|
|
1364
1461
|
const csrfFormResponse = await fetch(`http://127.0.0.1:${csrfPort}/csrf-form`, {
|
|
1365
1462
|
headers: {
|
|
1366
1463
|
cookie: csrfCookie,
|
|
@@ -70,6 +70,30 @@ async function runAppChecks(rootDir, config, collector) {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
async function runStartupEntryChecks(rootDir, config, collector) {
|
|
74
|
+
const env = String(config.env || process.env.NODE_ENV || 'development').trim().toLowerCase();
|
|
75
|
+
const appEntryPath = path.join(rootDir, 'app.js');
|
|
76
|
+
const loaderEntryPath = path.join(rootDir, 'loader.cjs');
|
|
77
|
+
const appEntryExists = await fileExists(appEntryPath);
|
|
78
|
+
const loaderEntryExists = await fileExists(loaderEntryPath);
|
|
79
|
+
|
|
80
|
+
if (appEntryExists) {
|
|
81
|
+
collector.ok('app.js exists.');
|
|
82
|
+
} else if (env === 'production') {
|
|
83
|
+
collector.error('app.js is missing for production startup. Run "aegisnode generateloader" to restore startup entry files.');
|
|
84
|
+
} else {
|
|
85
|
+
collector.warn('app.js is missing. Run "aegisnode generateloader" to restore startup entry files.');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (loaderEntryExists) {
|
|
89
|
+
collector.ok('loader.cjs exists.');
|
|
90
|
+
} else if (env === 'production') {
|
|
91
|
+
collector.error('loader.cjs is missing for production startup. Run "aegisnode generateloader" to restore it.');
|
|
92
|
+
} else {
|
|
93
|
+
collector.warn('loader.cjs is missing. Generate it with "aegisnode generateloader" before non-development startup.');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
73
97
|
function runSecurityChecks(config, collector) {
|
|
74
98
|
const env = String(config.env || process.env.NODE_ENV || 'development');
|
|
75
99
|
const security = config.security || {};
|
|
@@ -179,6 +203,7 @@ export async function runDoctor({
|
|
|
179
203
|
collector.ok(`Environment: ${config.env || 'development'}`);
|
|
180
204
|
|
|
181
205
|
await runAppChecks(resolvedRoot, config, collector);
|
|
206
|
+
await runStartupEntryChecks(resolvedRoot, config, collector);
|
|
182
207
|
runSecurityChecks(config, collector);
|
|
183
208
|
runAuthChecks(config, collector);
|
|
184
209
|
runApiChecks(config, collector);
|
|
@@ -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,7 @@ 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';
|
|
7
8
|
|
|
8
9
|
function printHelp() {
|
|
9
10
|
console.log(`AegisNode CLI
|
|
@@ -13,6 +14,7 @@ Usage:
|
|
|
13
14
|
aegisnode createapp <app-name> [--project <path>] [--mount </path>]
|
|
14
15
|
aegisnode generate <type> <name> --app <app-name> [--project <path>]
|
|
15
16
|
aegisnode runserver [--project <path>] [--port <number>]
|
|
17
|
+
aegisnode generateloader [--project <path>]
|
|
16
18
|
aegisnode doctor [--project <path>]
|
|
17
19
|
aegisnode updatedeps [--project <path>]
|
|
18
20
|
|
|
@@ -24,6 +26,7 @@ Examples:
|
|
|
24
26
|
aegisnode createapp users
|
|
25
27
|
aegisnode generate view user --app users
|
|
26
28
|
aegisnode generate validator user --app users
|
|
29
|
+
aegisnode generateloader --project blog
|
|
27
30
|
aegisnode updatedeps --project blog
|
|
28
31
|
`);
|
|
29
32
|
}
|
|
@@ -126,6 +129,14 @@ export async function runCli(argv) {
|
|
|
126
129
|
return;
|
|
127
130
|
}
|
|
128
131
|
|
|
132
|
+
case 'generateloader':
|
|
133
|
+
case 'loader': {
|
|
134
|
+
await runGenerateLoader({
|
|
135
|
+
projectRoot: flags.project ? String(flags.project) : process.cwd(),
|
|
136
|
+
});
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
129
140
|
case 'updatedeps': {
|
|
130
141
|
await runUpdateDependencies({
|
|
131
142
|
projectRoot: flags.project ? String(flags.project) : process.cwd(),
|
|
@@ -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',
|
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
|
}
|
|
Binary file
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
<svg width="1600" height="520" viewBox="0 0 1600 520" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
-
<defs>
|
|
3
|
-
<linearGradient id="banner-bg" x1="0" y1="0" x2="1600" y2="520" gradientUnits="userSpaceOnUse">
|
|
4
|
-
<stop stop-color="#0E1737"/>
|
|
5
|
-
<stop offset="0.5" stop-color="#103C70"/>
|
|
6
|
-
<stop offset="1" stop-color="#14A49F"/>
|
|
7
|
-
</linearGradient>
|
|
8
|
-
<linearGradient id="glow" x1="224" y1="80" x2="496" y2="352" gradientUnits="userSpaceOnUse">
|
|
9
|
-
<stop stop-color="#FFFFFF" stop-opacity="0.9"/>
|
|
10
|
-
<stop offset="1" stop-color="#D5EAFF" stop-opacity="0.75"/>
|
|
11
|
-
</linearGradient>
|
|
12
|
-
<linearGradient id="line" x1="184" y1="315" x2="392" y2="162" gradientUnits="userSpaceOnUse">
|
|
13
|
-
<stop stop-color="#0E4E8A"/>
|
|
14
|
-
<stop offset="1" stop-color="#1AA7A1"/>
|
|
15
|
-
</linearGradient>
|
|
16
|
-
<filter id="soft" x="0" y="0" width="1600" height="520" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
|
17
|
-
<feGaussianBlur stdDeviation="0"/>
|
|
18
|
-
</filter>
|
|
19
|
-
</defs>
|
|
20
|
-
|
|
21
|
-
<rect width="1600" height="520" rx="40" fill="url(#banner-bg)"/>
|
|
22
|
-
|
|
23
|
-
<g opacity="0.22" stroke="#A5D7FF" stroke-width="1.4">
|
|
24
|
-
<path d="M0 90H1600"/>
|
|
25
|
-
<path d="M0 150H1600"/>
|
|
26
|
-
<path d="M0 210H1600"/>
|
|
27
|
-
<path d="M0 270H1600"/>
|
|
28
|
-
<path d="M0 330H1600"/>
|
|
29
|
-
<path d="M0 390H1600"/>
|
|
30
|
-
<path d="M0 450H1600"/>
|
|
31
|
-
</g>
|
|
32
|
-
|
|
33
|
-
<circle cx="1280" cy="160" r="170" fill="#7EC8FF" fill-opacity="0.12"/>
|
|
34
|
-
<circle cx="1430" cy="390" r="210" fill="#44E0D0" fill-opacity="0.1"/>
|
|
35
|
-
|
|
36
|
-
<g filter="url(#soft)">
|
|
37
|
-
<path d="M288 78L431 135V257C431 341 370 411 288 460C206 411 145 341 145 257V135L288 78Z" fill="url(#glow)"/>
|
|
38
|
-
<path d="M288 100L409 149V251C409 324 358 386 288 425C218 386 167 324 167 251V149L288 100Z" fill="#E9F4FF"/>
|
|
39
|
-
|
|
40
|
-
<g stroke="url(#line)" stroke-width="18" stroke-linecap="round" stroke-linejoin="round">
|
|
41
|
-
<path d="M200 349L288 190L376 349"/>
|
|
42
|
-
<path d="M239 285H337"/>
|
|
43
|
-
<path d="M201 349H376"/>
|
|
44
|
-
</g>
|
|
45
|
-
|
|
46
|
-
<g fill="#0B2A50">
|
|
47
|
-
<circle cx="200" cy="349" r="16"/>
|
|
48
|
-
<circle cx="288" cy="190" r="16"/>
|
|
49
|
-
<circle cx="376" cy="349" r="16"/>
|
|
50
|
-
</g>
|
|
51
|
-
</g>
|
|
52
|
-
|
|
53
|
-
<g fill="#EAF5FF">
|
|
54
|
-
<text x="520" y="226" font-family="Segoe UI, Trebuchet MS, Arial, sans-serif" font-size="118" font-weight="700" letter-spacing="0.6">AegisNode</text>
|
|
55
|
-
<text x="525" y="296" font-family="Segoe UI, Trebuchet MS, Arial, sans-serif" font-size="38" font-weight="500" fill="#CDE8FF">
|
|
56
|
-
View-first Node.js framework starter
|
|
57
|
-
</text>
|
|
58
|
-
</g>
|
|
59
|
-
|
|
60
|
-
<g>
|
|
61
|
-
<rect x="523" y="333" width="510" height="58" rx="14" fill="#0E2D57" fill-opacity="0.78"/>
|
|
62
|
-
<text x="552" y="372" font-family="Segoe UI, Trebuchet MS, Arial, sans-serif" font-size="28" font-weight="600" fill="#8FE6DA">
|
|
63
|
-
CLI | DI | Events | SQL/NoSQL | WebSocket
|
|
64
|
-
</text>
|
|
65
|
-
</g>
|
|
66
|
-
</svg>
|
|
Binary file
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
-
<defs>
|
|
3
|
-
<linearGradient id="bg" x1="44" y1="36" x2="474" y2="496" gradientUnits="userSpaceOnUse">
|
|
4
|
-
<stop stop-color="#0F1B3D"/>
|
|
5
|
-
<stop offset="0.55" stop-color="#0F4C81"/>
|
|
6
|
-
<stop offset="1" stop-color="#14A4A0"/>
|
|
7
|
-
</linearGradient>
|
|
8
|
-
<linearGradient id="shield" x1="256" y1="98" x2="256" y2="394" gradientUnits="userSpaceOnUse">
|
|
9
|
-
<stop stop-color="#F8FAFF" stop-opacity="0.98"/>
|
|
10
|
-
<stop offset="1" stop-color="#D5E9FF" stop-opacity="0.94"/>
|
|
11
|
-
</linearGradient>
|
|
12
|
-
<linearGradient id="accent" x1="175" y1="309" x2="340" y2="189" gradientUnits="userSpaceOnUse">
|
|
13
|
-
<stop stop-color="#0E4E8A"/>
|
|
14
|
-
<stop offset="1" stop-color="#1AA7A1"/>
|
|
15
|
-
</linearGradient>
|
|
16
|
-
</defs>
|
|
17
|
-
|
|
18
|
-
<rect x="20" y="20" width="472" height="472" rx="116" fill="url(#bg)"/>
|
|
19
|
-
|
|
20
|
-
<g opacity="0.2" stroke="#9FD4FF" stroke-width="1.2">
|
|
21
|
-
<path d="M80 138H432"/>
|
|
22
|
-
<path d="M80 188H432"/>
|
|
23
|
-
<path d="M80 238H432"/>
|
|
24
|
-
<path d="M80 288H432"/>
|
|
25
|
-
<path d="M80 338H432"/>
|
|
26
|
-
<path d="M80 388H432"/>
|
|
27
|
-
</g>
|
|
28
|
-
|
|
29
|
-
<path d="M256 94L366 138V232C366 296 319 350 256 388C193 350 146 296 146 232V138L256 94Z" fill="url(#shield)"/>
|
|
30
|
-
<path d="M256 111L351 149V228C351 285 311 334 256 366C201 334 161 285 161 228V149L256 111Z" fill="#E9F4FF"/>
|
|
31
|
-
|
|
32
|
-
<g stroke="url(#accent)" stroke-width="14" stroke-linecap="round" stroke-linejoin="round">
|
|
33
|
-
<path d="M189 304L256 182L323 304"/>
|
|
34
|
-
<path d="M218 254H294"/>
|
|
35
|
-
<path d="M190 304H323"/>
|
|
36
|
-
</g>
|
|
37
|
-
|
|
38
|
-
<g fill="#0C2F58">
|
|
39
|
-
<circle cx="189" cy="304" r="13"/>
|
|
40
|
-
<circle cx="256" cy="182" r="13"/>
|
|
41
|
-
<circle cx="323" cy="304" r="13"/>
|
|
42
|
-
</g>
|
|
43
|
-
</svg>
|