inertjs-core 1.0.0-beta.0

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 ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "inertjs-core",
3
+ "version": "1.0.0-beta.0",
4
+ "type": "module",
5
+ "main": "src/index.js",
6
+ "dependencies": {
7
+ "inertjs-optimizer": "^1.0.0-beta.0"
8
+ },
9
+ "engines": {
10
+ "node": ">=22.0.0"
11
+ }
12
+ }
package/src/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { CoreServer } from './server.js';
2
+ export { getScope } from './scope.js';
@@ -0,0 +1,390 @@
1
+ import { Readable } from 'node:stream';
2
+ import { pathToFileURL } from 'node:url';
3
+ import { getScope } from './scope.js';
4
+ import { renderToStream } from '../../vector/src/stream.js';
5
+ import { raw } from '../../vector/src/index.js';
6
+ import { getCspHeader, validateCsrfToken } from '../../shield/src/index.js';
7
+ import { serveStatic } from './static.js';
8
+ import path from 'node:path';
9
+ import fs from 'node:fs/promises';
10
+ import { optimizeImage } from 'inertjs-optimizer';
11
+
12
+ function parseCookies(cookieStr) {
13
+ if (!cookieStr) return {};
14
+ return cookieStr.split(';').reduce((acc, str) => {
15
+ const [k, v] = str.split('=');
16
+ if (k && v) acc[k.trim()] = v.trim();
17
+ return acc;
18
+ }, {});
19
+ }
20
+
21
+ const flashCache = new Map();
22
+
23
+ export async function handleRequest(req, res, config, trie) {
24
+ const scope = getScope();
25
+ const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
26
+
27
+ if (req.method === 'GET' && !req.headers['accept']?.includes('application/vnd.inert.pulse+json') && flashCache.has(url.pathname)) {
28
+ const start = performance.now();
29
+ const cachedHtml = flashCache.get(url.pathname);
30
+ // Securely inject the unique nonce for this request
31
+ const safeHtml = cachedHtml.replace(/nonce="[^"]*"/g, `nonce="${scope.nonce}"`);
32
+ const csp = getCspHeader(scope.nonce, config);
33
+ res.writeHead(200, {
34
+ 'Content-Type': 'text/html; charset=utf-8',
35
+ 'Content-Security-Policy': csp,
36
+ 'X-Content-Type-Options': 'nosniff',
37
+ 'X-Frame-Options': 'DENY',
38
+ 'X-Inert-Flash': 'Hit',
39
+ 'X-Inert-Flash-Time': `${(performance.now() - start).toFixed(2)}ms`
40
+ });
41
+ res.end(safeHtml);
42
+ return;
43
+ }
44
+
45
+ if (url.pathname === '/_inert/dev/stream') {
46
+ res.writeHead(200, {
47
+ 'Content-Type': 'text/event-stream',
48
+ 'Cache-Control': 'no-cache',
49
+ 'Connection': 'keep-alive'
50
+ });
51
+ res.write('data: connected\n\n');
52
+ // Keep connection alive to detect disconnects instantly
53
+ const keepAlive = setInterval(() => res.write(':\n\n'), 15000);
54
+ req.on('close', () => clearInterval(keepAlive));
55
+ return;
56
+ }
57
+
58
+ if (url.pathname.startsWith('/_inert/pulse/')) {
59
+ const filename = url.pathname.replace('/_inert/pulse/', '');
60
+ const filePath = path.resolve(process.cwd(), 'packages/pulse/src', filename);
61
+ const served = await serveStatic(req, res, filePath);
62
+ if (!served) {
63
+ res.writeHead(404);
64
+ res.end('Not Found');
65
+ }
66
+ return;
67
+ }
68
+
69
+ if (url.pathname.startsWith('/_inert/image')) {
70
+ const src = url.searchParams.get('src');
71
+ const width = url.searchParams.get('w');
72
+ const height = url.searchParams.get('h');
73
+ const quality = url.searchParams.get('q');
74
+
75
+ if (!src) {
76
+ res.writeHead(400);
77
+ res.end('Missing src parameter');
78
+ return;
79
+ }
80
+
81
+ try {
82
+ const filePath = path.join(process.cwd(), src.replace(/^\/+/, ''));
83
+ // Basic security check to prevent directory traversal
84
+ if (!filePath.startsWith(process.cwd())) {
85
+ res.writeHead(403);
86
+ res.end('Forbidden');
87
+ return;
88
+ }
89
+
90
+ const buffer = await fs.readFile(filePath);
91
+ const optimizedBuffer = await optimizeImage(buffer, { width, height, quality });
92
+
93
+ res.writeHead(200, {
94
+ 'Content-Type': 'image/webp',
95
+ 'Cache-Control': 'public, max-age=31536000, immutable'
96
+ });
97
+ res.end(optimizedBuffer);
98
+ } catch (err) {
99
+ console.error(`[InertJS] Image optimization failed:`, err);
100
+ res.writeHead(500);
101
+ res.end('Error optimizing image');
102
+ }
103
+ return;
104
+ }
105
+
106
+ if (url.pathname.startsWith('/public/')) {
107
+ const filename = url.pathname.replace('/public/', '');
108
+ // Need to detect content type based on extension
109
+ const ext = path.extname(filename).toLowerCase();
110
+ const contentTypes = {
111
+ '.jpg': 'image/jpeg',
112
+ '.jpeg': 'image/jpeg',
113
+ '.png': 'image/png',
114
+ '.svg': 'image/svg+xml',
115
+ '.css': 'text/css',
116
+ '.webp': 'image/webp',
117
+ '.ico': 'image/x-icon'
118
+ };
119
+ const cType = contentTypes[ext] || 'application/octet-stream';
120
+ const filePath = path.resolve(process.cwd(), 'public', filename);
121
+ const served = await serveStatic(req, res, filePath, cType);
122
+ if (!served) {
123
+ res.writeHead(404);
124
+ res.end('Not Found');
125
+ }
126
+ return;
127
+ }
128
+
129
+ if (url.pathname.startsWith('/node_modules/')) {
130
+ const filePath = path.resolve(process.cwd(), url.pathname.slice(1));
131
+ const served = await serveStatic(req, res, filePath);
132
+ if (!served) {
133
+ res.writeHead(404);
134
+ res.end('Not Found');
135
+ }
136
+ return;
137
+ }
138
+
139
+ const segments = url.pathname.split('/').filter(Boolean);
140
+
141
+ let match = trie.match(segments);
142
+ let is404 = false;
143
+
144
+ if (!match) {
145
+ match = trie.match(['404']);
146
+ is404 = true;
147
+ if (!match) {
148
+ res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' });
149
+ res.end(`<!DOCTYPE html>
150
+ <html lang="en">
151
+ <head>
152
+ <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>404 Not Found - InertJS</title>
153
+ <style>
154
+ body { font-family: system-ui, -apple-system, sans-serif; background-color: #0f172a; color: #f8fafc; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; margin: 0; }
155
+ .container { text-align: center; max-width: 600px; padding: 2rem; }
156
+ h1 { font-size: 5rem; margin: 0; background: linear-gradient(to right, #818cf8, #22d3ee); -webkit-background-clip: text; color: transparent; font-weight: 900; }
157
+ p { color: #94a3b8; font-size: 1.25rem; margin-bottom: 2rem; }
158
+ a { background: #1e293b; color: #f8fafc; text-decoration: none; padding: 0.75rem 2rem; border-radius: 9999px; font-weight: 600; border: 1px solid #334155; transition: all 0.2s; }
159
+ a:hover { background: #4f46e5; border-color: #4f46e5; }
160
+ </style>
161
+ </head>
162
+ <body>
163
+ <div class="container">
164
+ <h1>404</h1>
165
+ <p>The route could not be resolved by the InertJS router.</p>
166
+ <a href="/">Return Home</a>
167
+ </div>
168
+ </body>
169
+ </html>`);
170
+ return;
171
+ }
172
+ }
173
+
174
+ const { route, params } = match;
175
+
176
+ try {
177
+ // 1. Run guard() chain
178
+ if (route.guard) {
179
+ const guardModule = await import(pathToFileURL(route.guard).href);
180
+ if (guardModule.guard) {
181
+ const allowed = await guardModule.guard({ req, params, scope });
182
+ if (!allowed) {
183
+ res.writeHead(403, { 'Content-Type': 'text/plain' });
184
+ res.end('Forbidden');
185
+ return;
186
+ }
187
+ }
188
+ }
189
+
190
+ // 2. Wire handling (API endpoint)
191
+ if (route.wire && !route.view) {
192
+ const method = req.method.toUpperCase();
193
+
194
+ // CSRF Protection for mutating methods
195
+ if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(method)) {
196
+ const cookies = parseCookies(req.headers.cookie);
197
+ const headerToken = req.headers['x-inert-csrf'];
198
+ if (!validateCsrfToken(cookies['__inert_csrf'], headerToken)) {
199
+ res.writeHead(403, { 'Content-Type': 'text/plain' });
200
+ res.end('CSRF Validation Failed');
201
+ return;
202
+ }
203
+ }
204
+
205
+ const wireModule = await import(pathToFileURL(route.wire).href);
206
+ if (wireModule[method]) {
207
+ // Execute the method handler
208
+ const responseData = await wireModule[method]({ req, res, params, scope });
209
+ if (!res.headersSent && responseData) {
210
+ res.writeHead(200, { 'Content-Type': 'application/json' });
211
+ res.end(JSON.stringify(responseData));
212
+ }
213
+ } else {
214
+ res.writeHead(405, { 'Content-Type': 'text/plain' });
215
+ res.end('Method Not Allowed');
216
+ }
217
+ return;
218
+ }
219
+
220
+ // 3. Run flux()
221
+ let data = {};
222
+ if (route.flux) {
223
+ const fluxModule = await import(pathToFileURL(route.flux).href);
224
+ if (fluxModule.flux) {
225
+ data = await fluxModule.flux({ req, params, scope });
226
+ }
227
+ }
228
+
229
+ const isPulse = req.headers['accept']?.includes('application/vnd.inert.pulse+json');
230
+
231
+ // 4. View rendering
232
+ if (!isPulse) {
233
+ const csp = getCspHeader(scope.nonce, config);
234
+ res.writeHead(is404 ? 404 : 200, {
235
+ 'Content-Type': 'text/html; charset=utf-8',
236
+ 'Content-Security-Policy': csp,
237
+ 'X-Content-Type-Options': 'nosniff',
238
+ 'X-Frame-Options': 'DENY'
239
+ });
240
+ }
241
+
242
+ let viewResult = '';
243
+ let isFlashMode = false;
244
+ if (route.view) {
245
+ const viewModule = await import(pathToFileURL(route.view).href);
246
+ if (viewModule.flash === true) isFlashMode = true;
247
+ if (viewModule.render) {
248
+ viewResult = viewModule.render({ data, params, scope });
249
+ }
250
+ }
251
+
252
+ // 5. Wrap in shells (from inside out) if not a pulse navigation
253
+ let finalResult = viewResult;
254
+
255
+ if (isPulse) {
256
+ const { resolveToString } = await import('../../vector/src/index.js');
257
+ const viewHtml = await resolveToString(viewResult);
258
+
259
+ res.writeHead(200, {
260
+ 'Content-Type': 'application/vnd.inert.pulse+json',
261
+ 'X-Content-Type-Options': 'nosniff'
262
+ });
263
+
264
+ res.end(JSON.stringify({
265
+ title: data.title || 'InertJS',
266
+ shellIdsToUpdate: [],
267
+ viewHtml,
268
+ data
269
+ }));
270
+ return;
271
+ }
272
+
273
+ if (route.shells && route.shells.length > 0) {
274
+ for (let i = route.shells.length - 1; i >= 0; i--) {
275
+ const shellModule = await import(pathToFileURL(route.shells[i]).href);
276
+ if (shellModule.render) {
277
+ // Preserve VecStream and RawString objects, only wrap plain strings
278
+ const childrenArg = (finalResult && (finalResult.type === 'VecStream' || finalResult.value !== undefined))
279
+ ? finalResult
280
+ : raw(String(finalResult));
281
+ finalResult = shellModule.render({ children: childrenArg, data, params, scope });
282
+ }
283
+ }
284
+ }
285
+
286
+ // 6. Stream to response
287
+ if (isFlashMode && !isPulse && req.method === 'GET') {
288
+ const { resolveToString } = await import('../../vector/src/index.js');
289
+ const finalHtml = await resolveToString(finalResult);
290
+ // Cache it for the next request
291
+ flashCache.set(url.pathname, finalHtml);
292
+ res.end(finalHtml);
293
+ return;
294
+ }
295
+
296
+ const webStream = renderToStream(finalResult, scope.nonce);
297
+ const nodeStream = Readable.fromWeb(webStream);
298
+
299
+ // Pipe handles backpressure natively
300
+ nodeStream.pipe(res);
301
+
302
+ } catch (err) {
303
+ console.error(`[InertJS] Pipeline Error:`, err);
304
+
305
+ // Fallback handling
306
+ if (route && route.fallback) {
307
+ try {
308
+ const fallbackModule = await import(pathToFileURL(route.fallback).href);
309
+ if (fallbackModule.fallback && !res.headersSent) {
310
+ const fallbackRes = await fallbackModule.fallback(err);
311
+ res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
312
+ res.end(fallbackRes);
313
+ return;
314
+ }
315
+ } catch (fallbackErr) {
316
+ console.error(`[InertJS] Fallback failed:`, fallbackErr);
317
+ }
318
+ }
319
+
320
+ if (!res.headersSent) {
321
+ // Global 500 handler
322
+ const errorMatch = trie.match(['500']);
323
+ if (errorMatch && errorMatch.route.view) {
324
+ try {
325
+ const viewModule = await import(pathToFileURL(errorMatch.route.view).href);
326
+ let finalResult = '';
327
+ if (viewModule.render) {
328
+ finalResult = viewModule.render({ data: { error: err.message, stack: err.stack, title: 'Server Error' }, params: {}, scope });
329
+ }
330
+
331
+ if (errorMatch.route.shells && errorMatch.route.shells.length > 0) {
332
+ for (let i = errorMatch.route.shells.length - 1; i >= 0; i--) {
333
+ const shellModule = await import(pathToFileURL(errorMatch.route.shells[i]).href);
334
+ if (shellModule.render) {
335
+ finalResult = shellModule.render({ children: raw(finalResult instanceof Object && finalResult.value ? finalResult.value : String(finalResult)), data: { error: err.message, stack: err.stack, title: 'Server Error' }, params: {}, scope });
336
+ }
337
+ }
338
+ }
339
+
340
+ const csp = getCspHeader(scope.nonce, config);
341
+ res.writeHead(500, {
342
+ 'Content-Type': 'text/html; charset=utf-8',
343
+ 'Content-Security-Policy': csp,
344
+ 'X-Content-Type-Options': 'nosniff',
345
+ 'X-Frame-Options': 'DENY'
346
+ });
347
+
348
+ const webStream = renderToStream(finalResult, scope.nonce);
349
+ const nodeStream = Readable.fromWeb(webStream);
350
+ nodeStream.pipe(res);
351
+ return;
352
+ } catch (globalErr) {
353
+ console.error(`[InertJS] Global 500 handler failed:`, globalErr);
354
+ }
355
+ }
356
+
357
+ res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
358
+ res.end(`<!DOCTYPE html>
359
+ <html lang="en">
360
+ <head>
361
+ <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>500 Internal Error - InertJS</title>
362
+ <style>
363
+ body { font-family: system-ui, -apple-system, sans-serif; background-color: #0f172a; color: #f8fafc; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; margin: 0; padding: 2rem; box-sizing: border-box; }
364
+ .container { width: 100%; max-width: 800px; }
365
+ .header { text-align: center; margin-bottom: 2rem; }
366
+ h1 { font-size: 3rem; margin: 0; color: #f87171; font-weight: 800; }
367
+ p { color: #94a3b8; font-size: 1.1rem; }
368
+ .error-box { background: rgba(0,0,0,0.3); border: 1px solid rgba(248,113,113,0.3); border-radius: 0.5rem; overflow: hidden; }
369
+ .error-msg { background: rgba(127,29,29,0.4); padding: 1rem; color: #fca5a5; font-family: monospace; font-weight: bold; border-bottom: 1px solid rgba(248,113,113,0.3); word-wrap: break-word; }
370
+ pre { padding: 1rem; margin: 0; color: #cbd5e1; font-family: monospace; font-size: 0.875rem; overflow-x: auto; }
371
+ </style>
372
+ </head>
373
+ <body>
374
+ <div class="container">
375
+ <div class="header">
376
+ <h1>Internal Server Error</h1>
377
+ <p>The application encountered an unexpected condition.</p>
378
+ </div>
379
+ <div class="error-box">
380
+ <div class="error-msg">${err.message.replace(/</g, '&lt;').replace(/>/g, '&gt;')}</div>
381
+ <pre>${err.stack.replace(/</g, '&lt;').replace(/>/g, '&gt;')}</pre>
382
+ </div>
383
+ </div>
384
+ </body>
385
+ </html>`);
386
+ } else {
387
+ res.end(); // just close it
388
+ }
389
+ }
390
+ }
package/src/scope.js ADDED
@@ -0,0 +1,17 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+
3
+ export const requestScope = new AsyncLocalStorage();
4
+
5
+ /**
6
+ * Gets the current request scope.
7
+ *
8
+ * @returns {object} The RequestScope object
9
+ * @throws {Error} If called outside of a request scope
10
+ */
11
+ export function getScope() {
12
+ const scope = requestScope.getStore();
13
+ if (!scope) {
14
+ throw new Error('E_INERT_SCOPE: Accessed outside of request scope');
15
+ }
16
+ return scope;
17
+ }
package/src/server.js ADDED
@@ -0,0 +1,108 @@
1
+ import http from 'node:http';
2
+ import http2 from 'node:http2';
3
+ import crypto from 'node:crypto';
4
+ import { requestScope } from './scope.js';
5
+ import { handleRequest } from './pipeline.js';
6
+ import { RateLimiter } from '../../shield/src/index.js';
7
+
8
+ export class CoreServer {
9
+ /**
10
+ * Initializes the InertJS Core server.
11
+ *
12
+ * @param {object} config Validated config object
13
+ * @param {RouterTrie} trie Loaded router trie
14
+ */
15
+ constructor(config, trie) {
16
+ this.config = config;
17
+ this.trie = trie;
18
+
19
+ // Browsers do not support HTTP/2 without TLS (h2c).
20
+ // To support Chrome in local dev, we fall back to HTTP/1.1 for plain text.
21
+ this.server = http.createServer();
22
+
23
+ const reqPerSec = config?.shield?.rateLimit || 100;
24
+ this.rateLimiter = new RateLimiter(reqPerSec);
25
+
26
+ this.connections = new Set();
27
+
28
+ this.server.on('connection', (conn) => {
29
+ this.connections.add(conn);
30
+ conn.once('close', () => this.connections.delete(conn));
31
+ });
32
+
33
+ this.server.on('request', this._onRequest.bind(this));
34
+ }
35
+
36
+ _onRequest(req, res) {
37
+ const ip = req.socket.remoteAddress || '127.0.0.1';
38
+ if (!this.rateLimiter.check(ip)) {
39
+ res.writeHead(429, { 'Content-Type': 'text/plain', 'Retry-After': '1' });
40
+ res.end('Too Many Requests');
41
+ return;
42
+ }
43
+
44
+ const scope = {
45
+ reqId: crypto.randomUUID(),
46
+ nonce: crypto.randomBytes(16).toString('base64'),
47
+ startedAt: performance.now(),
48
+ vaultHandle: null,
49
+ conduitAgent: null,
50
+ lens: null
51
+ };
52
+
53
+ // Every stage is wrapped in an AsyncLocalStorage RequestScope
54
+ requestScope.run(scope, () => {
55
+ handleRequest(req, res, this.config, this.trie).catch(err => {
56
+ console.error(`[InertJS] Unhandled request error:`, err);
57
+ if (!res.headersSent) {
58
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
59
+ res.end('Internal Server Error');
60
+ } else {
61
+ res.end();
62
+ }
63
+ });
64
+ });
65
+ }
66
+
67
+ async start() {
68
+ let port = this.config.core.port;
69
+ const host = this.config.core.host;
70
+
71
+ return new Promise((resolve, reject) => {
72
+ this.server.on('error', (err) => {
73
+ if (err.code === 'EADDRINUSE') {
74
+ console.warn(`[InertJS] Port ${port} is in use, trying ${port + 1}...`);
75
+ port++;
76
+ this.config.core.port = port; // Update config
77
+ this.server.listen(port, host);
78
+ } else {
79
+ reject(err);
80
+ }
81
+ });
82
+
83
+ this.server.listen(port, host, () => {
84
+ resolve();
85
+ });
86
+ });
87
+ }
88
+
89
+ async stop() {
90
+ return new Promise((resolve, reject) => {
91
+ let timeout;
92
+ if (this.config.core.gracefulShutdownMs) {
93
+ timeout = setTimeout(() => {
94
+ console.warn('[InertJS] Graceful shutdown timeout, forcing close');
95
+ for (const conn of this.connections) {
96
+ conn.destroy();
97
+ }
98
+ }, this.config.core.gracefulShutdownMs);
99
+ }
100
+
101
+ this.server.close((err) => {
102
+ if (timeout) clearTimeout(timeout);
103
+ if (err) reject(err);
104
+ else resolve();
105
+ });
106
+ });
107
+ }
108
+ }
package/src/static.js ADDED
@@ -0,0 +1,50 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
+ import { minifyAsset } from 'inertjs-optimizer';
5
+
6
+ const ETAG_CACHE = new Map();
7
+
8
+ /**
9
+ * Serves a static file with ETag caching and proper cache headers.
10
+ */
11
+ export async function serveStatic(req, res, filePath, contentType = 'application/javascript') {
12
+ try {
13
+ const stat = await fs.stat(filePath);
14
+
15
+ let cacheEntry = ETAG_CACHE.get(filePath);
16
+ if (!cacheEntry || cacheEntry.mtime !== stat.mtimeMs) {
17
+ let content = await fs.readFile(filePath);
18
+
19
+ if (contentType === 'application/javascript' || contentType === 'text/css') {
20
+ const type = contentType === 'text/css' ? 'css' : 'js';
21
+ const minified = await minifyAsset(content.toString('utf8'), type);
22
+ content = Buffer.from(minified, 'utf8');
23
+ }
24
+
25
+ const hash = crypto.createHash('sha1').update(content).digest('hex');
26
+ cacheEntry = {
27
+ etag: `"${hash}"`,
28
+ mtime: stat.mtimeMs,
29
+ content
30
+ };
31
+ ETAG_CACHE.set(filePath, cacheEntry);
32
+ }
33
+
34
+ if (req.headers['if-none-match'] === cacheEntry.etag) {
35
+ res.writeHead(304);
36
+ res.end();
37
+ return true;
38
+ }
39
+
40
+ res.writeHead(200, {
41
+ 'Content-Type': contentType,
42
+ 'Cache-Control': 'public, max-age=31536000, immutable',
43
+ 'ETag': cacheEntry.etag
44
+ });
45
+ res.end(cacheEntry.content);
46
+ return true;
47
+ } catch (err) {
48
+ return false;
49
+ }
50
+ }
@@ -0,0 +1,167 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert';
3
+ import http2 from 'node:http2';
4
+ import fs from 'node:fs/promises';
5
+ import path from 'node:path';
6
+ import os from 'node:os';
7
+ import { pathToFileURL } from 'node:url';
8
+ import { CoreServer } from '../src/server.js';
9
+ import { RouterTrie } from '../../router/src/trie.js';
10
+
11
+ function fetchH2C(url) {
12
+ return new Promise((resolve, reject) => {
13
+ const client = http2.connect(url);
14
+ const u = new URL(url);
15
+ const req = client.request({ ':path': u.pathname });
16
+
17
+ let data = '';
18
+ req.setEncoding('utf8');
19
+
20
+ req.on('response', (headers) => {
21
+ req.on('data', chunk => data += chunk);
22
+ req.on('end', () => {
23
+ client.close();
24
+ resolve({ status: headers[':status'], data, headers });
25
+ });
26
+ });
27
+
28
+ req.on('error', err => {
29
+ client.close();
30
+ reject(err);
31
+ });
32
+
33
+ req.end();
34
+ });
35
+ }
36
+
37
+ test('Core Kernel - Server Pipeline', async (t) => {
38
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'inert-core-test-'));
39
+
40
+ try {
41
+ // Scaffold test routes
42
+ await fs.mkdir(path.join(tmpDir, 'basic'), { recursive: true });
43
+
44
+ const vectorPath = pathToFileURL(path.resolve(process.cwd(), 'packages/vector/src/index.js')).href;
45
+
46
+ // Mock view.js
47
+ await fs.writeFile(
48
+ path.join(tmpDir, 'basic', 'view.js'),
49
+ `import { vec } from '${vectorPath}'; export const render = ({ data }) => vec\`<main>\${data.title}</main>\`;`
50
+ );
51
+
52
+ // Mock shell.js
53
+ await fs.writeFile(
54
+ path.join(tmpDir, 'basic', 'shell.js'),
55
+ `import { vec } from '${vectorPath}'; export const render = ({ children }) => vec\`<html><body>\${children}</body></html>\`;`
56
+ );
57
+
58
+ // Mock flux.js
59
+ await fs.writeFile(
60
+ path.join(tmpDir, 'basic', 'flux.js'),
61
+ `export const flux = async () => { return { title: 'Hello Flux' }; };`
62
+ );
63
+
64
+ // Mock guard.js
65
+ await fs.writeFile(
66
+ path.join(tmpDir, 'basic', 'guard.js'),
67
+ `export const guard = async ({ req }) => req.headers['x-allow'] === 'yes';`
68
+ );
69
+
70
+ const trie = new RouterTrie();
71
+
72
+ // The manifest stores absolute paths
73
+ const toUrl = p => path.join(tmpDir, p).split(path.sep).join('/');
74
+
75
+ trie.insert(['basic'], {
76
+ view: toUrl('basic/view.js'),
77
+ shell: toUrl('basic/shell.js'), // Wait, it's an array of shells
78
+ shells: [toUrl('basic/shell.js')],
79
+ flux: toUrl('basic/flux.js'),
80
+ guard: toUrl('basic/guard.js'),
81
+ });
82
+
83
+ const config = {
84
+ core: { port: 0, host: '127.0.0.1', gracefulShutdownMs: 1000 }
85
+ };
86
+
87
+ const server = new CoreServer(config, trie);
88
+ await server.start();
89
+ const port = server.server.address().port;
90
+
91
+ await t.test('403 when guard fails', async () => {
92
+ const res = await fetchH2C(`http://127.0.0.1:${port}/basic`);
93
+ assert.strictEqual(res.status, 403);
94
+ });
95
+
96
+ await t.test('200 pipeline success (guard -> flux -> view -> shell)', async () => {
97
+ return new Promise((resolve, reject) => {
98
+ const client = http2.connect(`http://127.0.0.1:${port}`);
99
+ const req = client.request({ ':path': '/basic', 'x-allow': 'yes' });
100
+
101
+ let data = '';
102
+ req.setEncoding('utf8');
103
+
104
+ req.on('response', (headers) => {
105
+ assert.strictEqual(headers[':status'], 200);
106
+ assert.strictEqual(headers['content-type'], 'text/html; charset=utf-8');
107
+ assert.ok(headers['content-security-policy'].includes('default-src \'none\''));
108
+
109
+ req.on('data', chunk => data += chunk);
110
+ req.on('end', () => {
111
+ client.close();
112
+ // It should render shell wrapping view wrapping flux data
113
+ assert.strictEqual(data, '<html><body><main>Hello Flux</main></body></html>');
114
+ resolve();
115
+ });
116
+ });
117
+
118
+ req.on('error', err => {
119
+ client.close();
120
+ reject(err);
121
+ });
122
+
123
+ req.end();
124
+ });
125
+ });
126
+
127
+ await t.test('200 Pulse JSON manifest', async () => {
128
+ return new Promise((resolve, reject) => {
129
+ const client = http2.connect(`http://127.0.0.1:${port}`);
130
+ const req = client.request({
131
+ ':path': '/basic',
132
+ 'x-allow': 'yes',
133
+ 'accept': 'application/vnd.inert.pulse+json'
134
+ });
135
+
136
+ let data = '';
137
+ req.setEncoding('utf8');
138
+
139
+ req.on('response', (headers) => {
140
+ assert.strictEqual(headers[':status'], 200);
141
+ assert.strictEqual(headers['content-type'], 'application/vnd.inert.pulse+json');
142
+
143
+ req.on('data', chunk => data += chunk);
144
+ req.on('end', () => {
145
+ client.close();
146
+ const manifest = JSON.parse(data);
147
+ assert.strictEqual(manifest.title, 'Hello Flux');
148
+ assert.strictEqual(manifest.viewHtml, '<main>Hello Flux</main>');
149
+ assert.deepStrictEqual(manifest.data, { title: 'Hello Flux' });
150
+ resolve();
151
+ });
152
+ });
153
+
154
+ req.on('error', err => {
155
+ client.close();
156
+ reject(err);
157
+ });
158
+
159
+ req.end();
160
+ });
161
+ });
162
+
163
+ await server.stop();
164
+ } finally {
165
+ await fs.rm(tmpDir, { recursive: true, force: true });
166
+ }
167
+ });
@@ -0,0 +1,48 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert';
3
+ import http2 from 'node:http2';
4
+ import crypto from 'node:crypto';
5
+ import { CoreServer } from '../src/server.js';
6
+ import { RouterTrie } from '../../router/src/index.js';
7
+
8
+ test('CoreServer Rate Limiter E2E (100 reqs/sec limit)', async (t) => {
9
+ const trie = new RouterTrie();
10
+ trie.insert(['test'], {
11
+ view: 'file://' + process.cwd() + '/packages/core/test/mock/view.js'
12
+ });
13
+
14
+ const server = new CoreServer({
15
+ core: { port: 0, host: '127.0.0.1' },
16
+ shield: { rateLimit: 100 }
17
+ }, trie);
18
+
19
+ await server.start();
20
+ const port = server.server.address().port;
21
+
22
+ const client = http2.connect(`http://127.0.0.1:${port}`);
23
+
24
+ let successCount = 0;
25
+ let limitedCount = 0;
26
+
27
+ // Spam 200 requests concurrently
28
+ const reqs = Array.from({ length: 200 }).map(() => {
29
+ return new Promise((resolve) => {
30
+ const req = client.request({ ':path': '/test' });
31
+ req.on('response', (headers) => {
32
+ if (headers[':status'] === 200) successCount++;
33
+ if (headers[':status'] === 429) limitedCount++;
34
+ req.resume(); // consume data
35
+ });
36
+ req.on('end', resolve);
37
+ req.end();
38
+ });
39
+ });
40
+
41
+ await Promise.all(reqs);
42
+
43
+ assert.ok(successCount > 0 && successCount <= 100, `Expected some success, got ${successCount}`);
44
+ assert.ok(limitedCount >= 100, `Expected at least 100 429s, got ${limitedCount}`);
45
+
46
+ client.close();
47
+ await server.stop();
48
+ });