@zenithbuild/cli 0.7.3 → 0.7.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 +14 -11
- package/dist/adapters/adapter-netlify.js +1 -0
- package/dist/adapters/adapter-node.js +8 -0
- package/dist/adapters/adapter-vercel.js +1 -0
- package/dist/build/compiler-runtime.d.ts +10 -9
- package/dist/build/compiler-runtime.js +51 -1
- package/dist/build/compiler-signal-expression.d.ts +1 -0
- package/dist/build/compiler-signal-expression.js +155 -0
- package/dist/build/expression-rewrites.d.ts +1 -6
- package/dist/build/expression-rewrites.js +61 -65
- package/dist/build/page-component-loop.d.ts +3 -13
- package/dist/build/page-component-loop.js +21 -46
- package/dist/build/page-ir-normalization.d.ts +0 -8
- package/dist/build/page-ir-normalization.js +13 -234
- package/dist/build/page-loop-state.d.ts +6 -9
- package/dist/build/page-loop-state.js +9 -8
- package/dist/build/page-loop.js +27 -22
- package/dist/build/scoped-identifier-rewrite.d.ts +37 -44
- package/dist/build/scoped-identifier-rewrite.js +28 -128
- package/dist/build/server-script.d.ts +2 -1
- package/dist/build/server-script.js +29 -3
- package/dist/build.js +5 -3
- package/dist/component-instance-ir.js +158 -52
- package/dist/dev-build-session.js +20 -6
- package/dist/dev-server.js +82 -39
- package/dist/framework-components/Image.zen +1 -1
- package/dist/images/materialization-plan.d.ts +1 -0
- package/dist/images/materialization-plan.js +6 -0
- package/dist/images/materialize.d.ts +5 -3
- package/dist/images/materialize.js +24 -109
- package/dist/images/router-manifest.d.ts +1 -0
- package/dist/images/router-manifest.js +49 -0
- package/dist/index.js +8 -2
- package/dist/manifest.js +3 -2
- package/dist/preview.d.ts +4 -3
- package/dist/preview.js +87 -53
- package/dist/request-body.d.ts +2 -0
- package/dist/request-body.js +13 -0
- package/dist/request-origin.d.ts +2 -0
- package/dist/request-origin.js +45 -0
- package/dist/route-check-support.d.ts +1 -0
- package/dist/route-check-support.js +4 -0
- package/dist/server-contract.d.ts +15 -0
- package/dist/server-contract.js +102 -32
- package/dist/server-error.d.ts +4 -0
- package/dist/server-error.js +34 -0
- package/dist/server-output.d.ts +2 -0
- package/dist/server-output.js +13 -0
- package/dist/server-runtime/node-server.js +33 -27
- package/dist/server-runtime/route-render.d.ts +3 -3
- package/dist/server-runtime/route-render.js +20 -31
- package/dist/server-script-composition.d.ts +11 -5
- package/dist/server-script-composition.js +25 -10
- package/package.json +6 -3
package/dist/preview.js
CHANGED
|
@@ -14,10 +14,15 @@ import { access, readFile } from 'node:fs/promises';
|
|
|
14
14
|
import { extname, join, normalize, resolve, sep, dirname } from 'node:path';
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
16
|
import { appLocalRedirectLocation, imageEndpointPath, normalizeBasePath, routeCheckPath, stripBasePath } from './base-path.js';
|
|
17
|
-
import {
|
|
17
|
+
import { resolveBuildAdapter } from './adapters/resolve-adapter.js';
|
|
18
|
+
import { isConfigKeyExplicit, isLoadedConfig, loadConfig, validateConfig } from './config.js';
|
|
18
19
|
import { materializeImageMarkup } from './images/materialize.js';
|
|
19
20
|
import { createImageRuntimePayload, injectImageRuntimePayload } from './images/payload.js';
|
|
20
21
|
import { handleImageRequest } from './images/service.js';
|
|
22
|
+
import { encodeRequestBodyBase64, readRequestBodyBuffer } from './request-body.js';
|
|
23
|
+
import { createTrustedOriginResolver } from './request-origin.js';
|
|
24
|
+
import { supportsTargetRouteCheck } from './route-check-support.js';
|
|
25
|
+
import { clientFacingRouteMessage, defaultRouteDenyMessage, logServerException, sanitizeRouteResult } from './server-error.js';
|
|
21
26
|
import { createSilentLogger } from './ui/logger.js';
|
|
22
27
|
import { compareRouteSpecificity, matchRoute as matchManifestRoute, resolveRequestRoute } from './server/resolve-request-route.js';
|
|
23
28
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -34,6 +39,7 @@ const MIME_TYPES = {
|
|
|
34
39
|
'.avif': 'image/avif',
|
|
35
40
|
'.gif': 'image/gif'
|
|
36
41
|
};
|
|
42
|
+
const IMAGE_RUNTIME_TAG_RE = /<script\b[^>]*\bid=(["'])zenith-image-runtime\1[^>]*>[\s\S]*?<\/script>/i;
|
|
37
43
|
const SERVER_SCRIPT_RUNNER = String.raw `
|
|
38
44
|
import vm from 'node:vm';
|
|
39
45
|
import fs from 'node:fs/promises';
|
|
@@ -203,6 +209,13 @@ function ctxDeny(status = 403, message = undefined) {
|
|
|
203
209
|
message: typeof message === 'string' ? message : undefined
|
|
204
210
|
};
|
|
205
211
|
}
|
|
212
|
+
function ctxInvalid(payload, status = 400) {
|
|
213
|
+
return {
|
|
214
|
+
kind: 'invalid',
|
|
215
|
+
data: payload,
|
|
216
|
+
status: Number.isInteger(status) ? status : 400
|
|
217
|
+
};
|
|
218
|
+
}
|
|
206
219
|
function ctxData(payload) {
|
|
207
220
|
return {
|
|
208
221
|
kind: 'data',
|
|
@@ -210,10 +223,16 @@ function ctxData(payload) {
|
|
|
210
223
|
};
|
|
211
224
|
}
|
|
212
225
|
|
|
213
|
-
const
|
|
226
|
+
const requestInit = {
|
|
214
227
|
method: requestMethod,
|
|
215
228
|
headers: new Headers(safeRequestHeaders)
|
|
216
|
-
}
|
|
229
|
+
};
|
|
230
|
+
const requestBodyBase64 = String(process.env.ZENITH_SERVER_REQUEST_BODY_BASE64 || '');
|
|
231
|
+
if (requestMethod !== 'GET' && requestMethod !== 'HEAD' && requestBodyBase64.length > 0) {
|
|
232
|
+
requestInit.body = Buffer.from(requestBodyBase64, 'base64');
|
|
233
|
+
requestInit.duplex = 'half';
|
|
234
|
+
}
|
|
235
|
+
const requestSnapshot = new Request(requestUrl, requestInit);
|
|
217
236
|
const routeParams = { ...params };
|
|
218
237
|
const routeMeta = {
|
|
219
238
|
id: routeId,
|
|
@@ -229,6 +248,7 @@ const routeContext = {
|
|
|
229
248
|
method: requestMethod,
|
|
230
249
|
route: routeMeta,
|
|
231
250
|
env: {},
|
|
251
|
+
action: null,
|
|
232
252
|
auth: {
|
|
233
253
|
async getSession(_ctx) {
|
|
234
254
|
return null;
|
|
@@ -240,6 +260,7 @@ const routeContext = {
|
|
|
240
260
|
allow: ctxAllow,
|
|
241
261
|
redirect: ctxRedirect,
|
|
242
262
|
deny: ctxDeny,
|
|
263
|
+
invalid: ctxInvalid,
|
|
243
264
|
data: ctxData
|
|
244
265
|
};
|
|
245
266
|
|
|
@@ -320,7 +341,7 @@ async function linkModule(specifier, parentIdentifier) {
|
|
|
320
341
|
return loadFileModule(resolvedUrl);
|
|
321
342
|
}
|
|
322
343
|
|
|
323
|
-
const allowed = new Set(['data', 'load', 'guard', 'ssr_data', 'props', 'ssr', 'prerender']);
|
|
344
|
+
const allowed = new Set(['data', 'load', 'guard', 'action', 'ssr_data', 'props', 'ssr', 'prerender']);
|
|
324
345
|
const prelude = "const params = globalThis.params;\n" +
|
|
325
346
|
"const ctx = globalThis.ctx;\n" +
|
|
326
347
|
"import { resolveRouteResult } from 'zenith:server-contract';\n" +
|
|
@@ -372,13 +393,15 @@ try {
|
|
|
372
393
|
|
|
373
394
|
process.stdout.write(JSON.stringify(resolved || null));
|
|
374
395
|
} catch (error) {
|
|
375
|
-
const message = error instanceof Error
|
|
396
|
+
const message = error instanceof Error
|
|
397
|
+
? (typeof error.stack === 'string' && error.stack.length > 0 ? error.stack : error.message)
|
|
398
|
+
: String(error);
|
|
399
|
+
process.stderr.write('[Zenith:Server] preview route execution failed\\n' + message + '\\n');
|
|
376
400
|
process.stdout.write(
|
|
377
401
|
JSON.stringify({
|
|
378
402
|
__zenith_error: {
|
|
379
403
|
status: 500,
|
|
380
|
-
code: 'LOAD_FAILED'
|
|
381
|
-
message
|
|
404
|
+
code: 'LOAD_FAILED'
|
|
382
405
|
}
|
|
383
406
|
})
|
|
384
407
|
);
|
|
@@ -395,7 +418,9 @@ export async function createPreviewServer(options) {
|
|
|
395
418
|
const loadedConfig = await loadConfig(resolvedProjectRoot);
|
|
396
419
|
const resolvedConfig = options?.config && typeof options.config === 'object'
|
|
397
420
|
? (() => {
|
|
398
|
-
const overrideConfig =
|
|
421
|
+
const overrideConfig = isLoadedConfig(options.config)
|
|
422
|
+
? options.config
|
|
423
|
+
: validateConfig(options.config);
|
|
399
424
|
const mergedConfig = { ...loadedConfig };
|
|
400
425
|
for (const key of Object.keys(overrideConfig)) {
|
|
401
426
|
if (isConfigKeyExplicit(overrideConfig, key)) {
|
|
@@ -411,7 +436,13 @@ export async function createPreviewServer(options) {
|
|
|
411
436
|
const logger = providedLogger || createSilentLogger();
|
|
412
437
|
const verboseLogging = logger.mode?.logLevel === 'verbose';
|
|
413
438
|
const configuredBasePath = normalizeBasePath(config.basePath || '/');
|
|
439
|
+
const routeCheckEnabled = supportsTargetRouteCheck(resolveBuildAdapter(config).target);
|
|
414
440
|
let actualPort = port;
|
|
441
|
+
const resolveServerOrigin = createTrustedOriginResolver({
|
|
442
|
+
host,
|
|
443
|
+
getPort: () => actualPort,
|
|
444
|
+
label: 'preview server'
|
|
445
|
+
});
|
|
415
446
|
async function loadImageManifest() {
|
|
416
447
|
try {
|
|
417
448
|
const manifestRaw = await readFile(join(distDir, '_zenith', 'image', 'manifest.json'), 'utf8');
|
|
@@ -422,24 +453,17 @@ export async function createPreviewServer(options) {
|
|
|
422
453
|
return {};
|
|
423
454
|
}
|
|
424
455
|
}
|
|
425
|
-
function publicHost() {
|
|
426
|
-
if (host === '0.0.0.0' || host === '::') {
|
|
427
|
-
return '127.0.0.1';
|
|
428
|
-
}
|
|
429
|
-
return host;
|
|
430
|
-
}
|
|
431
|
-
function serverOrigin() {
|
|
432
|
-
return `http://${publicHost()}:${actualPort}`;
|
|
433
|
-
}
|
|
434
456
|
const server = createServer(async (req, res) => {
|
|
435
|
-
const
|
|
436
|
-
? `http://${req.headers.host}`
|
|
437
|
-
: serverOrigin();
|
|
438
|
-
const url = new URL(req.url, requestBase);
|
|
457
|
+
const url = new URL(req.url, resolveServerOrigin());
|
|
439
458
|
const { basePath, routes } = await loadRouteManifestState(distDir, configuredBasePath);
|
|
440
459
|
const canonicalPath = stripBasePath(url.pathname, basePath);
|
|
441
460
|
try {
|
|
442
461
|
if (url.pathname === routeCheckPath(basePath)) {
|
|
462
|
+
if (!routeCheckEnabled) {
|
|
463
|
+
res.writeHead(501, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
|
|
464
|
+
res.end(JSON.stringify({ error: 'route_check_unsupported' }));
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
443
467
|
// Security: Require explicitly designated header to prevent public oracle probing
|
|
444
468
|
if (req.headers['x-zenith-route-check'] !== '1') {
|
|
445
469
|
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
@@ -509,7 +533,7 @@ export async function createPreviewServer(options) {
|
|
|
509
533
|
'Vary': 'Cookie'
|
|
510
534
|
});
|
|
511
535
|
res.end(JSON.stringify({
|
|
512
|
-
result: checkResult?.result || checkResult,
|
|
536
|
+
result: sanitizeRouteResult(checkResult?.result || checkResult),
|
|
513
537
|
routeId: resolvedCheck.route.route_id || '',
|
|
514
538
|
to: targetUrl.toString()
|
|
515
539
|
}));
|
|
@@ -557,33 +581,40 @@ export async function createPreviewServer(options) {
|
|
|
557
581
|
throw new Error('not found');
|
|
558
582
|
}
|
|
559
583
|
let ssrPayload = null;
|
|
584
|
+
let routeExecution = null;
|
|
560
585
|
if (resolved.matched && resolved.route?.server_script && resolved.route.prerender !== true) {
|
|
561
|
-
let routeExecution = null;
|
|
562
586
|
try {
|
|
587
|
+
const requestMethod = req.method || 'GET';
|
|
588
|
+
const requestBodyBuffer = requestMethod === 'GET' || requestMethod === 'HEAD'
|
|
589
|
+
? null
|
|
590
|
+
: await readRequestBodyBuffer(req);
|
|
563
591
|
routeExecution = await executeServerRoute({
|
|
564
592
|
source: resolved.route.server_script,
|
|
565
593
|
sourcePath: resolved.route.server_script_path || '',
|
|
566
594
|
params: resolved.params,
|
|
567
595
|
requestUrl: url.toString(),
|
|
568
|
-
requestMethod
|
|
596
|
+
requestMethod,
|
|
569
597
|
requestHeaders: req.headers,
|
|
598
|
+
requestBodyBase64: encodeRequestBodyBase64(requestBodyBuffer),
|
|
570
599
|
routePattern: resolved.route.path,
|
|
571
600
|
routeFile: resolved.route.server_script_path || '',
|
|
572
601
|
routeId: resolved.route.route_id || routeIdFromSourcePath(resolved.route.server_script_path || '')
|
|
573
602
|
});
|
|
574
603
|
}
|
|
575
604
|
catch (error) {
|
|
605
|
+
logServerException('preview server route execution failed', error);
|
|
576
606
|
ssrPayload = {
|
|
577
607
|
__zenith_error: {
|
|
608
|
+
status: 500,
|
|
578
609
|
code: 'LOAD_FAILED',
|
|
579
|
-
message: error instanceof Error ? error.message : String(error)
|
|
610
|
+
message: error instanceof Error ? error.message : String(error || '')
|
|
580
611
|
}
|
|
581
612
|
};
|
|
582
613
|
}
|
|
583
|
-
const trace = routeExecution?.trace || { guard: 'none', load: 'none' };
|
|
614
|
+
const trace = routeExecution?.trace || { guard: 'none', action: 'none', load: 'none' };
|
|
584
615
|
const routeId = resolved.route.route_id || routeIdFromSourcePath(resolved.route.server_script_path || '');
|
|
585
616
|
if (verboseLogging) {
|
|
586
|
-
logger.router(`${routeId} guard=${trace.guard} load=${trace.load}`);
|
|
617
|
+
logger.router(`${routeId} guard=${trace.guard} action=${trace.action} load=${trace.load}`);
|
|
587
618
|
}
|
|
588
619
|
const result = routeExecution?.result;
|
|
589
620
|
if (result && result.kind === 'redirect') {
|
|
@@ -598,7 +629,7 @@ export async function createPreviewServer(options) {
|
|
|
598
629
|
if (result && result.kind === 'deny') {
|
|
599
630
|
const status = Number.isInteger(result.status) ? result.status : 403;
|
|
600
631
|
res.writeHead(status, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
601
|
-
res.end(result.message
|
|
632
|
+
res.end(clientFacingRouteMessage(status, result.message));
|
|
602
633
|
return;
|
|
603
634
|
}
|
|
604
635
|
if (result && result.kind === 'data' && result.data && typeof result.data === 'object' && !Array.isArray(result.data)) {
|
|
@@ -606,21 +637,24 @@ export async function createPreviewServer(options) {
|
|
|
606
637
|
}
|
|
607
638
|
}
|
|
608
639
|
let html = await readFile(htmlPath, 'utf8');
|
|
609
|
-
if (resolved.matched
|
|
610
|
-
const pageAssetPath = resolveWithinDist(distDir, resolved.route.page_asset);
|
|
640
|
+
if (resolved.matched) {
|
|
611
641
|
html = await materializeImageMarkup({
|
|
612
642
|
html,
|
|
613
|
-
pageAssetPath,
|
|
614
643
|
payload: createImageRuntimePayload(config.images, await loadImageManifest(), 'endpoint', basePath),
|
|
615
|
-
|
|
616
|
-
|
|
644
|
+
imageMaterialization: Array.isArray(resolved.route?.image_materialization)
|
|
645
|
+
? resolved.route.image_materialization
|
|
646
|
+
: []
|
|
617
647
|
});
|
|
618
648
|
}
|
|
619
649
|
if (ssrPayload) {
|
|
620
650
|
html = injectSsrPayload(html, ssrPayload);
|
|
621
651
|
}
|
|
622
|
-
|
|
623
|
-
|
|
652
|
+
if (!IMAGE_RUNTIME_TAG_RE.test(html)) {
|
|
653
|
+
html = injectImageRuntimePayload(html, createImageRuntimePayload(config.images, await loadImageManifest(), 'endpoint', basePath));
|
|
654
|
+
}
|
|
655
|
+
res.writeHead(Number.isInteger(routeExecution?.status) ? routeExecution.status : 200, {
|
|
656
|
+
'Content-Type': 'text/html'
|
|
657
|
+
});
|
|
624
658
|
res.end(html);
|
|
625
659
|
}
|
|
626
660
|
catch {
|
|
@@ -691,13 +725,13 @@ async function loadRouteManifestState(distDir, fallbackBasePath = '/') {
|
|
|
691
725
|
export const matchRoute = matchManifestRoute;
|
|
692
726
|
/**
|
|
693
727
|
* @param {{ source: string, sourcePath: string, params: Record<string, string>, requestUrl?: string, requestMethod?: string, requestHeaders?: Record<string, string | string[] | undefined>, routePattern?: string, routeFile?: string, routeId?: string }} input
|
|
694
|
-
* @returns {Promise<{ result: { kind: string, [key: string]: unknown }, trace: { guard: string, load: string } }>}
|
|
728
|
+
* @returns {Promise<{ result: { kind: string, [key: string]: unknown }, trace: { guard: string, action: string, load: string }, status?: number }>}
|
|
695
729
|
*/
|
|
696
|
-
export async function executeServerRoute({ source, sourcePath, params, requestUrl, requestMethod, requestHeaders, routePattern, routeFile, routeId, guardOnly = false }) {
|
|
730
|
+
export async function executeServerRoute({ source, sourcePath, params, requestUrl, requestMethod, requestHeaders, requestBodyBase64, routePattern, routeFile, routeId, guardOnly = false }) {
|
|
697
731
|
if (!source || !String(source).trim()) {
|
|
698
732
|
return {
|
|
699
733
|
result: { kind: 'data', data: {} },
|
|
700
|
-
trace: { guard: 'none', load: 'none' }
|
|
734
|
+
trace: { guard: 'none', action: 'none', load: 'none' }
|
|
701
735
|
};
|
|
702
736
|
}
|
|
703
737
|
const payload = await spawnNodeServerRunner({
|
|
@@ -707,6 +741,7 @@ export async function executeServerRoute({ source, sourcePath, params, requestUr
|
|
|
707
741
|
requestUrl: requestUrl || 'http://localhost/',
|
|
708
742
|
requestMethod: requestMethod || 'GET',
|
|
709
743
|
requestHeaders: sanitizeRequestHeaders(requestHeaders || {}),
|
|
744
|
+
requestBodyBase64: requestBodyBase64 || '',
|
|
710
745
|
routePattern: routePattern || '',
|
|
711
746
|
routeFile: routeFile || sourcePath || '',
|
|
712
747
|
routeId: routeId || routeIdFromSourcePath(sourcePath || ''),
|
|
@@ -715,7 +750,7 @@ export async function executeServerRoute({ source, sourcePath, params, requestUr
|
|
|
715
750
|
if (payload === null || payload === undefined) {
|
|
716
751
|
return {
|
|
717
752
|
result: { kind: 'data', data: {} },
|
|
718
|
-
trace: { guard: 'none', load: 'none' }
|
|
753
|
+
trace: { guard: 'none', action: 'none', load: 'none' }
|
|
719
754
|
};
|
|
720
755
|
}
|
|
721
756
|
if (typeof payload !== 'object' || Array.isArray(payload)) {
|
|
@@ -727,9 +762,9 @@ export async function executeServerRoute({ source, sourcePath, params, requestUr
|
|
|
727
762
|
result: {
|
|
728
763
|
kind: 'deny',
|
|
729
764
|
status: 500,
|
|
730
|
-
message:
|
|
765
|
+
message: defaultRouteDenyMessage(500)
|
|
731
766
|
},
|
|
732
|
-
trace: { guard: 'none', load: 'deny' }
|
|
767
|
+
trace: { guard: 'none', action: 'none', load: 'deny' }
|
|
733
768
|
};
|
|
734
769
|
}
|
|
735
770
|
const result = payload.result;
|
|
@@ -740,9 +775,11 @@ export async function executeServerRoute({ source, sourcePath, params, requestUr
|
|
|
740
775
|
trace: trace && typeof trace === 'object'
|
|
741
776
|
? {
|
|
742
777
|
guard: String(trace.guard || 'none'),
|
|
778
|
+
action: String(trace.action || 'none'),
|
|
743
779
|
load: String(trace.load || 'none')
|
|
744
780
|
}
|
|
745
|
-
: { guard: 'none', load: 'none' }
|
|
781
|
+
: { guard: 'none', action: 'none', load: 'none' },
|
|
782
|
+
status: Number.isInteger(payload.status) ? payload.status : undefined
|
|
746
783
|
};
|
|
747
784
|
}
|
|
748
785
|
return {
|
|
@@ -750,18 +787,9 @@ export async function executeServerRoute({ source, sourcePath, params, requestUr
|
|
|
750
787
|
kind: 'data',
|
|
751
788
|
data: payload
|
|
752
789
|
},
|
|
753
|
-
trace: { guard: 'none', load: 'data' }
|
|
790
|
+
trace: { guard: 'none', action: 'none', load: 'data' }
|
|
754
791
|
};
|
|
755
792
|
}
|
|
756
|
-
export function defaultRouteDenyMessage(status) {
|
|
757
|
-
if (status === 401)
|
|
758
|
-
return 'Unauthorized';
|
|
759
|
-
if (status === 403)
|
|
760
|
-
return 'Forbidden';
|
|
761
|
-
if (status === 404)
|
|
762
|
-
return 'Not Found';
|
|
763
|
-
return 'Internal Server Error';
|
|
764
|
-
}
|
|
765
793
|
/**
|
|
766
794
|
* @param {{ source: string, sourcePath: string, params: Record<string, string>, requestUrl?: string, requestMethod?: string, requestHeaders?: Record<string, string | string[] | undefined>, routePattern?: string, routeFile?: string, routeId?: string }} input
|
|
767
795
|
* @returns {Promise<Record<string, unknown> | null>}
|
|
@@ -790,7 +818,7 @@ export async function executeServerScript(input) {
|
|
|
790
818
|
__zenith_error: {
|
|
791
819
|
status,
|
|
792
820
|
code: status >= 500 ? 'LOAD_FAILED' : (status === 404 ? 'NOT_FOUND' : 'ACCESS_DENIED'),
|
|
793
|
-
message:
|
|
821
|
+
message: clientFacingRouteMessage(status, result.message)
|
|
794
822
|
}
|
|
795
823
|
};
|
|
796
824
|
}
|
|
@@ -811,6 +839,7 @@ function spawnNodeServerRunner(input) {
|
|
|
811
839
|
ZENITH_SERVER_REQUEST_URL: input.requestUrl || 'http://localhost/',
|
|
812
840
|
ZENITH_SERVER_REQUEST_METHOD: input.requestMethod || 'GET',
|
|
813
841
|
ZENITH_SERVER_REQUEST_HEADERS: JSON.stringify(input.requestHeaders || {}),
|
|
842
|
+
ZENITH_SERVER_REQUEST_BODY_BASE64: input.requestBodyBase64 || '',
|
|
814
843
|
ZENITH_SERVER_ROUTE_PATTERN: input.routePattern || '',
|
|
815
844
|
ZENITH_SERVER_ROUTE_FILE: input.routeFile || input.sourcePath || '',
|
|
816
845
|
ZENITH_SERVER_ROUTE_ID: input.routeId || '',
|
|
@@ -835,6 +864,11 @@ function spawnNodeServerRunner(input) {
|
|
|
835
864
|
rejectPromise(new Error(`[zenith-preview] server script execution failed (${code}): ${stderr.trim() || stdout.trim()}`));
|
|
836
865
|
return;
|
|
837
866
|
}
|
|
867
|
+
const stderrOutput = stderr.trim();
|
|
868
|
+
const internalErrorIndex = stderrOutput.indexOf('[Zenith:Server]');
|
|
869
|
+
if (internalErrorIndex >= 0) {
|
|
870
|
+
console.error(stderrOutput.slice(internalErrorIndex).trim());
|
|
871
|
+
}
|
|
838
872
|
const raw = stdout.trim();
|
|
839
873
|
if (!raw || raw === 'null') {
|
|
840
874
|
resolvePromise(null);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export async function readRequestBodyBuffer(req) {
|
|
2
|
+
const chunks = [];
|
|
3
|
+
for await (const chunk of req) {
|
|
4
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
5
|
+
}
|
|
6
|
+
return Buffer.concat(chunks);
|
|
7
|
+
}
|
|
8
|
+
export function encodeRequestBodyBase64(bodyBuffer) {
|
|
9
|
+
if (!Buffer.isBuffer(bodyBuffer) || bodyBuffer.length === 0) {
|
|
10
|
+
return '';
|
|
11
|
+
}
|
|
12
|
+
return bodyBuffer.toString('base64');
|
|
13
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const DEFAULT_HOST = '127.0.0.1';
|
|
2
|
+
export function publicHost(host) {
|
|
3
|
+
const normalized = String(host || DEFAULT_HOST).trim() || DEFAULT_HOST;
|
|
4
|
+
if (normalized === '0.0.0.0' || normalized === '::') {
|
|
5
|
+
return DEFAULT_HOST;
|
|
6
|
+
}
|
|
7
|
+
return normalized;
|
|
8
|
+
}
|
|
9
|
+
function normalizePublicOrigin(value, label) {
|
|
10
|
+
const raw = String(value || '').trim();
|
|
11
|
+
if (!raw) {
|
|
12
|
+
throw new Error(`[Zenith:Server] ${label} must be a non-empty absolute origin`);
|
|
13
|
+
}
|
|
14
|
+
let parsed;
|
|
15
|
+
try {
|
|
16
|
+
parsed = new URL(raw);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
throw new Error(`[Zenith:Server] ${label} must be an absolute origin`);
|
|
20
|
+
}
|
|
21
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
22
|
+
throw new Error(`[Zenith:Server] ${label} must use http or https`);
|
|
23
|
+
}
|
|
24
|
+
if (parsed.username || parsed.password) {
|
|
25
|
+
throw new Error(`[Zenith:Server] ${label} must not include credentials`);
|
|
26
|
+
}
|
|
27
|
+
if (parsed.pathname !== '/' || parsed.search || parsed.hash) {
|
|
28
|
+
throw new Error(`[Zenith:Server] ${label} must not include a path, query, or hash`);
|
|
29
|
+
}
|
|
30
|
+
return parsed.origin;
|
|
31
|
+
}
|
|
32
|
+
export function createTrustedOriginResolver(options = {}) {
|
|
33
|
+
const { publicOrigin = undefined, host = DEFAULT_HOST, port = undefined, getPort = undefined, label = 'server' } = options;
|
|
34
|
+
if (publicOrigin !== undefined && publicOrigin !== null && String(publicOrigin).trim().length > 0) {
|
|
35
|
+
const origin = normalizePublicOrigin(publicOrigin, `${label} publicOrigin`);
|
|
36
|
+
return () => origin;
|
|
37
|
+
}
|
|
38
|
+
return () => {
|
|
39
|
+
const resolvedPort = typeof getPort === 'function' ? getPort() : port;
|
|
40
|
+
if (!Number.isInteger(resolvedPort) || resolvedPort <= 0) {
|
|
41
|
+
throw new Error(`[Zenith:Server] ${label} requires "publicOrigin" when a trusted port is unavailable; raw Host headers are not trusted`);
|
|
42
|
+
}
|
|
43
|
+
return `http://${publicHost(host)}:${resolvedPort}`;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function supportsTargetRouteCheck(target: any): boolean;
|
|
@@ -15,6 +15,11 @@ export function data(payload: any): {
|
|
|
15
15
|
kind: string;
|
|
16
16
|
data: any;
|
|
17
17
|
};
|
|
18
|
+
export function invalid(payload: any, status?: number): {
|
|
19
|
+
kind: string;
|
|
20
|
+
data: any;
|
|
21
|
+
status: number;
|
|
22
|
+
};
|
|
18
23
|
export function validateServerExports({ exports, filePath }: {
|
|
19
24
|
exports: any;
|
|
20
25
|
filePath: any;
|
|
@@ -29,8 +34,18 @@ export function resolveRouteResult({ exports, ctx, filePath, guardOnly }: {
|
|
|
29
34
|
result: any;
|
|
30
35
|
trace: {
|
|
31
36
|
guard: string;
|
|
37
|
+
action: string;
|
|
38
|
+
load: string;
|
|
39
|
+
};
|
|
40
|
+
status?: undefined;
|
|
41
|
+
} | {
|
|
42
|
+
result: any;
|
|
43
|
+
trace: {
|
|
44
|
+
guard: string;
|
|
45
|
+
action: string;
|
|
32
46
|
load: string;
|
|
33
47
|
};
|
|
48
|
+
status: number | undefined;
|
|
34
49
|
}>;
|
|
35
50
|
export function resolveServerPayload({ exports, ctx, filePath }: {
|
|
36
51
|
exports: any;
|