@zenithbuild/cli 0.5.0-beta.2.6 → 0.6.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/dist/index.js CHANGED
@@ -10,11 +10,62 @@
10
10
  // Minimal arg parsing. No heavy dependencies.
11
11
  // ---------------------------------------------------------------------------
12
12
 
13
- import { resolve, join } from 'node:path';
14
- import { existsSync } from 'node:fs';
13
+ import { resolve, join, dirname } from 'node:path';
14
+ import { existsSync, readFileSync } from 'node:fs';
15
+ import { fileURLToPath } from 'node:url';
15
16
  import { createLogger } from './ui/logger.js';
16
17
 
17
18
  const COMMANDS = ['dev', 'build', 'preview'];
19
+ const DEFAULT_VERSION = '0.0.0';
20
+ const __filename = fileURLToPath(import.meta.url);
21
+ const __dirname = dirname(__filename);
22
+
23
+ function getCliVersion() {
24
+ try {
25
+ const pkgPath = join(__dirname, '..', 'package.json');
26
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
27
+ return typeof pkg.version === 'string' ? pkg.version : DEFAULT_VERSION;
28
+ } catch {
29
+ return DEFAULT_VERSION;
30
+ }
31
+ }
32
+
33
+ function printUsage(logger) {
34
+ logger.heading('V0');
35
+ logger.print('Usage:');
36
+ logger.print(' zenith dev [port|--port <port>] Start development server');
37
+ logger.print(' zenith build Build static site to /dist');
38
+ logger.print(' zenith preview [port|--port <port>] Preview /dist statically');
39
+ logger.print('');
40
+ logger.print('Options:');
41
+ logger.print(' -h, --help Show this help message');
42
+ logger.print(' -v, --version Print Zenith CLI version');
43
+ logger.print('');
44
+ }
45
+
46
+ function resolvePort(args, fallback) {
47
+ if (!Array.isArray(args) || args.length === 0) {
48
+ return fallback;
49
+ }
50
+
51
+ const flagIndex = args.findIndex((arg) => arg === '--port' || arg === '-p');
52
+ if (flagIndex >= 0 && args[flagIndex + 1]) {
53
+ const parsed = Number.parseInt(args[flagIndex + 1], 10);
54
+ if (Number.isFinite(parsed)) {
55
+ return parsed;
56
+ }
57
+ }
58
+
59
+ const positional = args.find((arg) => /^[0-9]+$/.test(arg));
60
+ if (positional) {
61
+ const parsed = Number.parseInt(positional, 10);
62
+ if (Number.isFinite(parsed)) {
63
+ return parsed;
64
+ }
65
+ }
66
+
67
+ return fallback;
68
+ }
18
69
 
19
70
  /**
20
71
  * Load zenith.config.js from project root.
@@ -41,14 +92,20 @@ async function loadConfig(projectRoot) {
41
92
  export async function cli(args, cwd) {
42
93
  const logger = createLogger(process);
43
94
  const command = args[0];
95
+ const cliVersion = getCliVersion();
96
+
97
+ if (args.includes('--version') || args.includes('-v')) {
98
+ logger.print(`zenith ${cliVersion}`);
99
+ process.exit(0);
100
+ }
101
+
102
+ if (args.includes('--help') || args.includes('-h')) {
103
+ printUsage(logger);
104
+ process.exit(0);
105
+ }
44
106
 
45
107
  if (!command || !COMMANDS.includes(command)) {
46
- logger.heading('V0');
47
- logger.print('Usage:');
48
- logger.print(' zenith dev Start development server');
49
- logger.print(' zenith build Build static site to /dist');
50
- logger.print(' zenith preview Preview /dist statically');
51
- logger.print('');
108
+ printUsage(logger);
52
109
  process.exit(command ? 1 : 0);
53
110
  }
54
111
 
@@ -69,10 +126,13 @@ export async function cli(args, cwd) {
69
126
 
70
127
  if (command === 'dev') {
71
128
  const { createDevServer } = await import('./dev-server.js');
72
- const port = parseInt(args[1]) || 3000;
129
+ const port = process.env.ZENITH_DEV_PORT
130
+ ? Number.parseInt(process.env.ZENITH_DEV_PORT, 10)
131
+ : resolvePort(args.slice(1), 3000);
132
+ const host = process.env.ZENITH_DEV_HOST || '127.0.0.1';
73
133
  logger.info('Starting dev server...');
74
- const dev = await createDevServer({ pagesDir, outDir, port, config });
75
- logger.success(`Dev server running at http://localhost:${dev.port}`);
134
+ const dev = await createDevServer({ pagesDir, outDir, port, host, config });
135
+ logger.success(`Dev server running at http://${host === '0.0.0.0' ? '127.0.0.1' : host}:${dev.port}`);
76
136
 
77
137
  // Graceful shutdown
78
138
  process.on('SIGINT', () => {
@@ -87,10 +147,11 @@ export async function cli(args, cwd) {
87
147
 
88
148
  if (command === 'preview') {
89
149
  const { createPreviewServer } = await import('./preview.js');
90
- const port = parseInt(args[1]) || 4000;
150
+ const port = resolvePort(args.slice(1), 4000);
151
+ const host = process.env.ZENITH_PREVIEW_HOST || '127.0.0.1';
91
152
  logger.info('Starting preview server...');
92
- const preview = await createPreviewServer({ distDir: outDir, port });
93
- logger.success(`Preview server running at http://localhost:${preview.port}`);
153
+ const preview = await createPreviewServer({ distDir: outDir, port, host });
154
+ logger.success(`Preview server running at http://${host === '0.0.0.0' ? '127.0.0.1' : host}:${preview.port}`);
94
155
 
95
156
  process.on('SIGINT', () => {
96
157
  preview.close();
package/dist/preview.js CHANGED
@@ -47,6 +47,7 @@ const requestHeaders = JSON.parse(process.env.ZENITH_SERVER_REQUEST_HEADERS || '
47
47
  const routePattern = process.env.ZENITH_SERVER_ROUTE_PATTERN || '';
48
48
  const routeFile = process.env.ZENITH_SERVER_ROUTE_FILE || sourcePath || '';
49
49
  const routeId = process.env.ZENITH_SERVER_ROUTE_ID || routePattern || '';
50
+ const guardOnly = process.env.ZENITH_SERVER_GUARD_ONLY === '1';
50
51
 
51
52
  if (!source.trim()) {
52
53
  process.stdout.write('null');
@@ -158,6 +159,55 @@ const safeRequestHeaders =
158
159
  requestHeaders && typeof requestHeaders === 'object'
159
160
  ? { ...requestHeaders }
160
161
  : {};
162
+ function parseCookies(rawCookieHeader) {
163
+ const out = Object.create(null);
164
+ const raw = String(rawCookieHeader || '');
165
+ if (!raw) return out;
166
+ const pairs = raw.split(';');
167
+ for (let i = 0; i < pairs.length; i++) {
168
+ const part = pairs[i];
169
+ const eq = part.indexOf('=');
170
+ if (eq <= 0) continue;
171
+ const key = part.slice(0, eq).trim();
172
+ if (!key) continue;
173
+ const value = part.slice(eq + 1).trim();
174
+ try {
175
+ out[key] = decodeURIComponent(value);
176
+ } catch {
177
+ out[key] = value;
178
+ }
179
+ }
180
+ return out;
181
+ }
182
+ const cookieHeader = typeof safeRequestHeaders.cookie === 'string'
183
+ ? safeRequestHeaders.cookie
184
+ : '';
185
+ const requestCookies = parseCookies(cookieHeader);
186
+
187
+ function ctxAllow() {
188
+ return { kind: 'allow' };
189
+ }
190
+ function ctxRedirect(location, status = 302) {
191
+ return {
192
+ kind: 'redirect',
193
+ location: String(location || ''),
194
+ status: Number.isInteger(status) ? status : 302
195
+ };
196
+ }
197
+ function ctxDeny(status = 403, message = undefined) {
198
+ return {
199
+ kind: 'deny',
200
+ status: Number.isInteger(status) ? status : 403,
201
+ message: typeof message === 'string' ? message : undefined
202
+ };
203
+ }
204
+ function ctxData(payload) {
205
+ return {
206
+ kind: 'data',
207
+ data: payload
208
+ };
209
+ }
210
+
161
211
  const requestSnapshot = new Request(requestUrl, {
162
212
  method: requestMethod,
163
213
  headers: new Headers(safeRequestHeaders)
@@ -168,15 +218,32 @@ const routeMeta = {
168
218
  pattern: routePattern,
169
219
  file: routeFile ? path.relative(process.cwd(), routeFile) : ''
170
220
  };
221
+ const routeContext = {
222
+ params: routeParams,
223
+ url: new URL(requestUrl),
224
+ headers: { ...safeRequestHeaders },
225
+ cookies: requestCookies,
226
+ request: requestSnapshot,
227
+ method: requestMethod,
228
+ route: routeMeta,
229
+ env: {},
230
+ auth: {
231
+ async getSession(_ctx) {
232
+ return null;
233
+ },
234
+ async requireSession(_ctx) {
235
+ throw ctxRedirect('/login', 302);
236
+ }
237
+ },
238
+ allow: ctxAllow,
239
+ redirect: ctxRedirect,
240
+ deny: ctxDeny,
241
+ data: ctxData
242
+ };
171
243
 
172
244
  const context = vm.createContext({
173
245
  params: routeParams,
174
- ctx: {
175
- params: routeParams,
176
- url: new URL(requestUrl),
177
- request: requestSnapshot,
178
- route: routeMeta
179
- },
246
+ ctx: routeContext,
180
247
  fetch: globalThis.fetch,
181
248
  Headers: globalThis.Headers,
182
249
  Request: globalThis.Request,
@@ -251,11 +318,11 @@ async function linkModule(specifier, parentIdentifier) {
251
318
  return loadFileModule(resolvedUrl);
252
319
  }
253
320
 
254
- const allowed = new Set(['data', 'load', 'ssr_data', 'props', 'ssr', 'prerender']);
321
+ const allowed = new Set(['data', 'load', 'guard', 'ssr_data', 'props', 'ssr', 'prerender']);
255
322
  const prelude = "const params = globalThis.params;\n" +
256
323
  "const ctx = globalThis.ctx;\n" +
257
- "import { resolveServerPayload } from 'zenith:server-contract';\n" +
258
- "globalThis.resolveServerPayload = resolveServerPayload;\n";
324
+ "import { resolveRouteResult } from 'zenith:server-contract';\n" +
325
+ "globalThis.resolveRouteResult = resolveRouteResult;\n";
259
326
  const entryIdentifier = sourcePath
260
327
  ? pathToFileURL(sourcePath).href
261
328
  : 'zenith:server-script';
@@ -294,13 +361,14 @@ for (const key of namespaceKeys) {
294
361
 
295
362
  const exported = entryModule.namespace;
296
363
  try {
297
- const payload = await context.resolveServerPayload({
364
+ const resolved = await context.resolveRouteResult({
298
365
  exports: exported,
299
366
  ctx: context.ctx,
300
- filePath: sourcePath || 'server_script'
367
+ filePath: sourcePath || 'server_script',
368
+ guardOnly: guardOnly
301
369
  });
302
370
 
303
- process.stdout.write(JSON.stringify(payload === undefined ? null : payload));
371
+ process.stdout.write(JSON.stringify(resolved || null));
304
372
  } catch (error) {
305
373
  const message = error instanceof Error ? error.message : String(error);
306
374
  process.stdout.write(
@@ -318,17 +386,106 @@ try {
318
386
  /**
319
387
  * Create and start a preview server.
320
388
  *
321
- * @param {{ distDir: string, port?: number }} options
389
+ * @param {{ distDir: string, port?: number, host?: string }} options
322
390
  * @returns {Promise<{ server: import('http').Server, port: number, close: () => void }>}
323
391
  */
324
392
  export async function createPreviewServer(options) {
325
- const { distDir, port = 4000 } = options;
393
+ const { distDir, port = 4000, host = '127.0.0.1' } = options;
394
+ let actualPort = port;
395
+
396
+ function publicHost() {
397
+ if (host === '0.0.0.0' || host === '::') {
398
+ return '127.0.0.1';
399
+ }
400
+ return host;
401
+ }
402
+
403
+ function serverOrigin() {
404
+ return `http://${publicHost()}:${actualPort}`;
405
+ }
326
406
 
327
407
  const server = createServer(async (req, res) => {
328
- const url = new URL(req.url, `http://localhost:${port}`);
408
+ const requestBase = typeof req.headers.host === 'string' && req.headers.host.length > 0
409
+ ? `http://${req.headers.host}`
410
+ : serverOrigin();
411
+ const url = new URL(req.url, requestBase);
329
412
 
330
413
  try {
331
- if (extname(url.pathname)) {
414
+ if (url.pathname === '/__zenith/route-check') {
415
+ // Security: Require explicitly designated header to prevent public oracle probing
416
+ if (req.headers['x-zenith-route-check'] !== '1') {
417
+ res.writeHead(403, { 'Content-Type': 'application/json' });
418
+ res.end(JSON.stringify({ error: 'forbidden', message: 'invalid request context' }));
419
+ return;
420
+ }
421
+
422
+ const targetPath = String(url.searchParams.get('path') || '/');
423
+
424
+ // Security: Prevent protocol/domain injection in path
425
+ if (targetPath.includes('://') || targetPath.startsWith('//') || /[\r\n]/.test(targetPath)) {
426
+ res.writeHead(400, { 'Content-Type': 'application/json' });
427
+ res.end(JSON.stringify({ error: 'invalid_path_format' }));
428
+ return;
429
+ }
430
+
431
+ const targetUrl = new URL(targetPath, url.origin);
432
+ if (targetUrl.origin !== url.origin) {
433
+ res.writeHead(400, { 'Content-Type': 'application/json' });
434
+ res.end(JSON.stringify({ error: 'external_route_evaluation_forbidden' }));
435
+ return;
436
+ }
437
+
438
+ const routes = await loadRouteManifest(distDir);
439
+ const resolvedCheck = resolveRequestRoute(targetUrl, routes);
440
+ if (!resolvedCheck.matched || !resolvedCheck.route) {
441
+ res.writeHead(404, { 'Content-Type': 'application/json' });
442
+ res.end(JSON.stringify({ error: 'route_not_found' }));
443
+ return;
444
+ }
445
+
446
+ const checkResult = await executeServerRoute({
447
+ source: resolvedCheck.route.server_script || '',
448
+ sourcePath: resolvedCheck.route.server_script_path || '',
449
+ params: resolvedCheck.params,
450
+ requestUrl: targetUrl.toString(),
451
+ requestMethod: req.method || 'GET',
452
+ requestHeaders: req.headers,
453
+ routePattern: resolvedCheck.route.path,
454
+ routeFile: resolvedCheck.route.server_script_path || '',
455
+ routeId: resolvedCheck.route.route_id || routeIdFromSourcePath(resolvedCheck.route.server_script_path || ''),
456
+ guardOnly: true
457
+ });
458
+ // Security: Enforce relative or same-origin redirects
459
+ if (checkResult && checkResult.result && checkResult.result.kind === 'redirect') {
460
+ const loc = String(checkResult.result.location || '/');
461
+ if (loc.includes('://') || loc.startsWith('//')) {
462
+ try {
463
+ const parsedLoc = new URL(loc);
464
+ if (parsedLoc.origin !== targetUrl.origin) {
465
+ checkResult.result.location = '/'; // Fallback to root for open redirect attempt
466
+ }
467
+ } catch {
468
+ checkResult.result.location = '/';
469
+ }
470
+ }
471
+ }
472
+
473
+ res.writeHead(200, {
474
+ 'Content-Type': 'application/json',
475
+ 'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
476
+ 'Pragma': 'no-cache',
477
+ 'Expires': '0',
478
+ 'Vary': 'Cookie'
479
+ });
480
+ res.end(JSON.stringify({
481
+ result: checkResult?.result || checkResult,
482
+ routeId: resolvedCheck.route.route_id || '',
483
+ to: targetUrl.toString()
484
+ }));
485
+ return;
486
+ }
487
+
488
+ if (extname(url.pathname) && extname(url.pathname) !== '.html') {
332
489
  const staticPath = resolveWithinDist(distDir, url.pathname);
333
490
  if (!staticPath || !(await fileExists(staticPath))) {
334
491
  throw new Error('not found');
@@ -358,11 +515,11 @@ export async function createPreviewServer(options) {
358
515
  throw new Error('not found');
359
516
  }
360
517
 
361
- let html = await readFile(htmlPath, 'utf8');
518
+ let ssrPayload = null;
362
519
  if (resolved.matched && resolved.route?.server_script && resolved.route.prerender !== true) {
363
- let payload = null;
520
+ let routeExecution = null;
364
521
  try {
365
- payload = await executeServerScript({
522
+ routeExecution = await executeServerRoute({
366
523
  source: resolved.route.server_script,
367
524
  sourcePath: resolved.route.server_script_path || '',
368
525
  params: resolved.params,
@@ -371,22 +528,48 @@ export async function createPreviewServer(options) {
371
528
  requestHeaders: req.headers,
372
529
  routePattern: resolved.route.path,
373
530
  routeFile: resolved.route.server_script_path || '',
374
- routeId: routeIdFromSourcePath(resolved.route.server_script_path || '')
531
+ routeId: resolved.route.route_id || routeIdFromSourcePath(resolved.route.server_script_path || '')
375
532
  });
376
533
  } catch (error) {
377
- payload = {
534
+ ssrPayload = {
378
535
  __zenith_error: {
379
- status: 500,
380
536
  code: 'LOAD_FAILED',
381
537
  message: error instanceof Error ? error.message : String(error)
382
538
  }
383
539
  };
384
540
  }
385
- if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
386
- html = injectSsrPayload(html, payload);
541
+
542
+ const trace = routeExecution?.trace || { guard: 'none', load: 'none' };
543
+ const routeId = resolved.route.route_id || routeIdFromSourcePath(resolved.route.server_script_path || '');
544
+ console.log(`[Zenith] guard(${routeId}) -> ${trace.guard}`);
545
+ console.log(`[Zenith] load(${routeId}) -> ${trace.load}`);
546
+
547
+ const result = routeExecution?.result;
548
+ if (result && result.kind === 'redirect') {
549
+ const status = Number.isInteger(result.status) ? result.status : 302;
550
+ res.writeHead(status, {
551
+ Location: result.location,
552
+ 'Cache-Control': 'no-store'
553
+ });
554
+ res.end('');
555
+ return;
556
+ }
557
+ if (result && result.kind === 'deny') {
558
+ const status = Number.isInteger(result.status) ? result.status : 403;
559
+ res.writeHead(status, { 'Content-Type': 'text/plain; charset=utf-8' });
560
+ res.end(result.message || (status === 401 ? 'Unauthorized' : 'Forbidden'));
561
+ return;
562
+ }
563
+ if (result && result.kind === 'data' && result.data && typeof result.data === 'object' && !Array.isArray(result.data)) {
564
+ ssrPayload = result.data;
387
565
  }
388
566
  }
389
567
 
568
+ let html = await readFile(htmlPath, 'utf8');
569
+ if (ssrPayload) {
570
+ html = injectSsrPayload(html, ssrPayload);
571
+ }
572
+
390
573
  res.writeHead(200, { 'Content-Type': 'text/html' });
391
574
  res.end(html);
392
575
  } catch {
@@ -396,8 +579,8 @@ export async function createPreviewServer(options) {
396
579
  });
397
580
 
398
581
  return new Promise((resolveServer) => {
399
- server.listen(port, () => {
400
- const actualPort = server.address().port;
582
+ server.listen(port, host, () => {
583
+ actualPort = server.address().port;
401
584
  resolveServer({
402
585
  server,
403
586
  port: actualPort,
@@ -416,6 +599,13 @@ export async function createPreviewServer(options) {
416
599
  * server_script?: string | null;
417
600
  * server_script_path?: string | null;
418
601
  * prerender?: boolean;
602
+ * route_id?: string;
603
+ * pattern?: string;
604
+ * params_shape?: Record<string, string>;
605
+ * has_guard?: boolean;
606
+ * has_load?: boolean;
607
+ * guard_module_ref?: string | null;
608
+ * load_module_ref?: string | null;
419
609
  * }} PreviewRoute
420
610
  */
421
611
 
@@ -446,28 +636,120 @@ export const matchRoute = matchManifestRoute;
446
636
 
447
637
  /**
448
638
  * @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
449
- * @returns {Promise<Record<string, unknown> | null>}
639
+ * @returns {Promise<{ result: { kind: string, [key: string]: unknown }, trace: { guard: string, load: string } }>}
450
640
  */
451
- export async function executeServerScript(input) {
641
+ export async function executeServerRoute({
642
+ source,
643
+ sourcePath,
644
+ params,
645
+ requestUrl,
646
+ requestMethod,
647
+ requestHeaders,
648
+ routePattern,
649
+ routeFile,
650
+ routeId,
651
+ guardOnly = false
652
+ }) {
653
+ if (!source || !String(source).trim()) {
654
+ return {
655
+ result: { kind: 'data', data: {} },
656
+ trace: { guard: 'none', load: 'none' }
657
+ };
658
+ }
659
+
452
660
  const payload = await spawnNodeServerRunner({
453
- source: input.source,
454
- sourcePath: input.sourcePath,
455
- params: input.params,
456
- requestUrl: input.requestUrl || 'http://localhost/',
457
- requestMethod: input.requestMethod || 'GET',
458
- requestHeaders: sanitizeRequestHeaders(input.requestHeaders || {}),
459
- routePattern: input.routePattern || '',
460
- routeFile: input.routeFile || input.sourcePath || '',
461
- routeId: input.routeId || routeIdFromSourcePath(input.sourcePath || '')
661
+ source,
662
+ sourcePath,
663
+ params,
664
+ requestUrl: requestUrl || 'http://localhost/',
665
+ requestMethod: requestMethod || 'GET',
666
+ requestHeaders: sanitizeRequestHeaders(requestHeaders || {}),
667
+ routePattern: routePattern || '',
668
+ routeFile: routeFile || sourcePath || '',
669
+ routeId: routeId || routeIdFromSourcePath(sourcePath || ''),
670
+ guardOnly
462
671
  });
463
672
 
464
673
  if (payload === null || payload === undefined) {
465
- return null;
674
+ return {
675
+ result: { kind: 'data', data: {} },
676
+ trace: { guard: 'none', load: 'none' }
677
+ };
466
678
  }
467
679
  if (typeof payload !== 'object' || Array.isArray(payload)) {
468
680
  throw new Error('[zenith-preview] server script payload must be an object');
469
681
  }
470
- return payload;
682
+
683
+ const errorEnvelope = payload.__zenith_error;
684
+ if (errorEnvelope && typeof errorEnvelope === 'object') {
685
+ return {
686
+ result: {
687
+ kind: 'deny',
688
+ status: 500,
689
+ message: String(errorEnvelope.message || 'Server route execution failed')
690
+ },
691
+ trace: { guard: 'none', load: 'deny' }
692
+ };
693
+ }
694
+
695
+ const result = payload.result;
696
+ const trace = payload.trace;
697
+ if (result && typeof result === 'object' && !Array.isArray(result) && typeof result.kind === 'string') {
698
+ return {
699
+ result,
700
+ trace: trace && typeof trace === 'object'
701
+ ? {
702
+ guard: String(trace.guard || 'none'),
703
+ load: String(trace.load || 'none')
704
+ }
705
+ : { guard: 'none', load: 'none' }
706
+ };
707
+ }
708
+
709
+ return {
710
+ result: {
711
+ kind: 'data',
712
+ data: payload
713
+ },
714
+ trace: { guard: 'none', load: 'data' }
715
+ };
716
+ }
717
+
718
+ /**
719
+ * @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
720
+ * @returns {Promise<Record<string, unknown> | null>}
721
+ */
722
+ export async function executeServerScript(input) {
723
+ const execution = await executeServerRoute(input);
724
+ const result = execution?.result;
725
+ if (!result || typeof result !== 'object') {
726
+ return null;
727
+ }
728
+ if (result.kind === 'data' && result.data && typeof result.data === 'object' && !Array.isArray(result.data)) {
729
+ return result.data;
730
+ }
731
+
732
+ if (result.kind === 'redirect') {
733
+ return {
734
+ __zenith_error: {
735
+ status: Number.isInteger(result.status) ? result.status : 302,
736
+ code: 'REDIRECT',
737
+ message: `Redirect to ${String(result.location || '')}`
738
+ }
739
+ };
740
+ }
741
+
742
+ if (result.kind === 'deny') {
743
+ return {
744
+ __zenith_error: {
745
+ status: Number.isInteger(result.status) ? result.status : 403,
746
+ code: 'ACCESS_DENIED',
747
+ message: String(result.message || 'Access denied')
748
+ }
749
+ };
750
+ }
751
+
752
+ return {};
471
753
  }
472
754
 
473
755
  /**
@@ -491,6 +773,7 @@ function spawnNodeServerRunner(input) {
491
773
  ZENITH_SERVER_ROUTE_PATTERN: input.routePattern || '',
492
774
  ZENITH_SERVER_ROUTE_FILE: input.routeFile || input.sourcePath || '',
493
775
  ZENITH_SERVER_ROUTE_ID: input.routeId || '',
776
+ ZENITH_SERVER_GUARD_ONLY: input.guardOnly ? '1' : '',
494
777
  ZENITH_SERVER_CONTRACT_PATH: join(__dirname, 'server-contract.js')
495
778
  },
496
779
  stdio: ['ignore', 'pipe', 'pipe']
@@ -609,7 +892,7 @@ export function resolveWithinDist(distDir, requestPath) {
609
892
  */
610
893
  function sanitizeRequestHeaders(headers) {
611
894
  const out = Object.create(null);
612
- const denyExact = new Set(['authorization', 'cookie', 'proxy-authorization', 'set-cookie']);
895
+ const denyExact = new Set(['proxy-authorization', 'set-cookie']);
613
896
  const denyPrefixes = ['x-forwarded-', 'cf-'];
614
897
  for (const [rawKey, rawValue] of Object.entries(headers || {})) {
615
898
  const key = String(rawKey || '').toLowerCase();