@taujs/server 0.3.6 → 0.4.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
@@ -187,36 +187,14 @@ var require_picocolors = __commonJS({
187
187
  }
188
188
  });
189
189
 
190
- // src/types.d.ts
190
+ // src/fastify.d.ts
191
191
  import "fastify";
192
192
 
193
193
  // src/SSRServer.ts
194
- var import_fastify_plugin = __toESM(require_plugin(), 1);
195
- var import_picocolors = __toESM(require_picocolors(), 1);
196
- import { readFile } from "fs/promises";
197
- import path2 from "path";
198
-
199
- // src/utils/Logger.ts
200
- var createLogger = (debug) => ({
201
- log: (...args) => {
202
- if (debug) console.log(...args);
203
- },
204
- warn: (...args) => {
205
- if (debug) console.warn(...args);
206
- },
207
- error: (...args) => {
208
- if (debug) console.error(...args);
209
- }
210
- });
211
- var debugLog = (logger, message, req) => {
212
- const prefix = "[\u03C4js]";
213
- const method = req?.method ?? "";
214
- const url = req?.url ?? "";
215
- const tag = method && url ? `${method} ${url}` : "";
216
- logger.log(`${prefix} ${tag} ${message}`);
217
- };
194
+ var import_fastify_plugin3 = __toESM(require_plugin(), 1);
218
195
 
219
196
  // src/constants.ts
197
+ var import_picocolors = __toESM(require_picocolors(), 1);
220
198
  var RENDERTYPE = {
221
199
  ssr: "ssr",
222
200
  streaming: "streaming"
@@ -236,44 +214,649 @@ var DEV_CSP_DIRECTIVES = {
236
214
  "style-src": ["'self'", "'unsafe-inline'"],
237
215
  "img-src": ["'self'", "data:"]
238
216
  };
217
+ var CONTENT = {
218
+ TAG: "\u03C4js"
219
+ };
220
+ var DEBUG = {
221
+ auth: { label: "auth", colour: import_picocolors.default.blue },
222
+ csp: { label: "csp", colour: import_picocolors.default.yellow },
223
+ errors: { label: "errors", colour: import_picocolors.default.red },
224
+ routes: { label: "routes", colour: import_picocolors.default.cyan },
225
+ security: { label: "security", colour: import_picocolors.default.yellow },
226
+ trx: { label: "trx", colour: import_picocolors.default.magenta },
227
+ vite: { label: "vite", colour: import_picocolors.default.yellow }
228
+ };
229
+ var REGEX = {
230
+ BENIGN_NET_ERR: /\b(?:ECONNRESET|EPIPE|ECONNABORTED)\b|socket hang up|aborted|premature(?: close)?/i,
231
+ SAFE_TRACE: /^[a-zA-Z0-9-_:.]{1,128}$/
232
+ };
233
+
234
+ // src/logging/AppError.ts
235
+ var HTTP_STATUS = {
236
+ infra: 500,
237
+ upstream: 502,
238
+ domain: 404,
239
+ validation: 400,
240
+ auth: 403,
241
+ canceled: 499,
242
+ // Client Closed Request (nginx convention)
243
+ timeout: 504
244
+ };
245
+ var AppError = class _AppError extends Error {
246
+ kind;
247
+ httpStatus;
248
+ details;
249
+ safeMessage;
250
+ code;
251
+ constructor(message, kind, options = {}) {
252
+ super(message);
253
+ this.name = "AppError";
254
+ Object.setPrototypeOf(this, new.target.prototype);
255
+ if (options.cause !== void 0) {
256
+ Object.defineProperty(this, "cause", {
257
+ value: options.cause,
258
+ enumerable: false,
259
+ writable: false,
260
+ configurable: true
261
+ });
262
+ }
263
+ this.kind = kind;
264
+ this.httpStatus = options.httpStatus ?? HTTP_STATUS[kind];
265
+ this.details = options.details;
266
+ this.safeMessage = options.safeMessage ?? this.getSafeMessage(kind, message);
267
+ this.code = options.code;
268
+ if (Error.captureStackTrace) Error.captureStackTrace(this, this.constructor);
269
+ }
270
+ getSafeMessage(kind, message) {
271
+ return kind === "domain" || kind === "validation" || kind === "auth" ? message : "Internal Server Error";
272
+ }
273
+ serialiseValue(value, seen = /* @__PURE__ */ new WeakSet()) {
274
+ if (value === null || value === void 0) return value;
275
+ if (typeof value !== "object") return value;
276
+ if (seen.has(value)) return "[circular]";
277
+ seen.add(value);
278
+ if (value instanceof Error) {
279
+ return {
280
+ name: value.name,
281
+ message: value.message,
282
+ stack: value.stack,
283
+ ...value instanceof _AppError && {
284
+ kind: value.kind,
285
+ httpStatus: value.httpStatus,
286
+ code: value.code
287
+ }
288
+ };
289
+ }
290
+ if (Array.isArray(value)) return value.map((item) => this.serialiseValue(item, seen));
291
+ const result = {};
292
+ for (const [key, val] of Object.entries(value)) {
293
+ result[key] = this.serialiseValue(val, seen);
294
+ }
295
+ return result;
296
+ }
297
+ toJSON() {
298
+ return {
299
+ name: this.name,
300
+ kind: this.kind,
301
+ message: this.message,
302
+ safeMessage: this.safeMessage,
303
+ httpStatus: this.httpStatus,
304
+ ...this.code && { code: this.code },
305
+ details: this.serialiseValue(this.details),
306
+ stack: this.stack,
307
+ ...this.cause && {
308
+ cause: this.serialiseValue(this.cause)
309
+ }
310
+ };
311
+ }
312
+ static notFound(message, details, code) {
313
+ return new _AppError(message, "domain", { httpStatus: 404, details, code });
314
+ }
315
+ static forbidden(message, details, code) {
316
+ return new _AppError(message, "auth", { httpStatus: 403, details, code });
317
+ }
318
+ static badRequest(message, details, code) {
319
+ return new _AppError(message, "validation", { httpStatus: 400, details, code });
320
+ }
321
+ static unprocessable(message, details, code) {
322
+ return new _AppError(message, "validation", { httpStatus: 422, details, code });
323
+ }
324
+ static timeout(message, details, code) {
325
+ return new _AppError(message, "timeout", { details, code });
326
+ }
327
+ static canceled(message, details, code) {
328
+ return new _AppError(message, "canceled", { details, code });
329
+ }
330
+ static internal(message, cause, details, code) {
331
+ return new _AppError(message, "infra", { cause, details, code });
332
+ }
333
+ static upstream(message, cause, details, code) {
334
+ return new _AppError(message, "upstream", { cause, details, code });
335
+ }
336
+ static serviceUnavailable(message, cause, details, code) {
337
+ return new _AppError(message, "infra", { httpStatus: 503, cause, details, code });
338
+ }
339
+ static from(err, fallback = "Internal error") {
340
+ return err instanceof _AppError ? err : _AppError.internal(err?.message ?? fallback, err);
341
+ }
342
+ };
343
+ function normaliseError(e) {
344
+ if (e instanceof Error) return { name: e.name, message: e.message, stack: e.stack };
345
+ const hasMessageProp = e != null && typeof e.message !== "undefined";
346
+ const msg = hasMessageProp ? String(e.message) : String(e);
347
+ return { name: "Error", message: msg };
348
+ }
349
+ function toReason(e) {
350
+ if (e instanceof Error) return e;
351
+ if (e === null) return new Error("null");
352
+ if (typeof e === "undefined") return new Error("Unknown render error");
353
+ const maybeMsg = e?.message;
354
+ if (typeof maybeMsg !== "undefined") return new Error(String(maybeMsg));
355
+ return new Error(String(e));
356
+ }
357
+
358
+ // src/logging/utils/index.ts
359
+ var httpStatusFrom = (err, fallback = 500) => err instanceof AppError ? err.httpStatus : fallback;
360
+ var toHttp = (err) => {
361
+ const app = AppError.from(err);
362
+ const status = httpStatusFrom(app);
363
+ const errorMessage = app.safeMessage;
364
+ return {
365
+ status,
366
+ body: {
367
+ error: errorMessage,
368
+ ...app.code && { code: app.code },
369
+ statusText: statusText(status)
370
+ }
371
+ };
372
+ };
373
+ var statusText = (status) => {
374
+ const map = {
375
+ 400: "Bad Request",
376
+ 401: "Unauthorized",
377
+ 403: "Forbidden",
378
+ 404: "Not Found",
379
+ 405: "Method Not Allowed",
380
+ 408: "Request Timeout",
381
+ 422: "Unprocessable Entity",
382
+ 429: "Too Many Requests",
383
+ 499: "Client Closed Request",
384
+ 500: "Internal Server Error",
385
+ 502: "Bad Gateway",
386
+ 503: "Service Unavailable",
387
+ 504: "Gateway Timeout"
388
+ };
389
+ return map[status] ?? "Error";
390
+ };
391
+
392
+ // src/utils/DataRoutes.ts
393
+ import { match } from "path-to-regexp";
394
+
395
+ // src/utils/DataServices.ts
396
+ async function callServiceMethod(registry, serviceName, methodName, params, ctx) {
397
+ if (ctx.signal?.aborted) throw AppError.timeout("Request canceled");
398
+ const service = registry[serviceName];
399
+ if (!service) throw AppError.notFound(`Unknown service: ${serviceName}`);
400
+ const desc = service[methodName];
401
+ if (!desc) throw AppError.notFound(`Unknown method: ${serviceName}.${methodName}`);
402
+ const logger = ctx.logger?.child({
403
+ component: "service-call",
404
+ service: serviceName,
405
+ method: methodName,
406
+ traceId: ctx.traceId
407
+ });
408
+ try {
409
+ const p = desc.parsers?.params ? desc.parsers.params(params) : params;
410
+ const data = await desc.handler(p, ctx);
411
+ const out = desc.parsers?.result ? desc.parsers.result(data) : data;
412
+ if (typeof out !== "object" || out === null) throw AppError.internal(`Non-object result from ${serviceName}.${methodName}`);
413
+ return out;
414
+ } catch (err) {
415
+ logger?.error("Service method failed", {
416
+ params,
417
+ error: err instanceof Error ? { name: err.name, message: err.message, stack: err.stack } : String(err)
418
+ });
419
+ throw err;
420
+ }
421
+ }
422
+ var isServiceDescriptor = (obj) => {
423
+ if (typeof obj !== "object" || obj === null || Array.isArray(obj)) return false;
424
+ const maybe = obj;
425
+ return typeof maybe.serviceName === "string" && typeof maybe.serviceMethod === "string";
426
+ };
427
+
428
+ // src/utils/DataRoutes.ts
429
+ var safeDecode = (value) => {
430
+ try {
431
+ return decodeURIComponent(value);
432
+ } catch {
433
+ return value;
434
+ }
435
+ };
436
+ var cleanPath = (path6) => {
437
+ if (!path6) return "/";
438
+ const basePart = path6.split("?")[0];
439
+ const base = basePart ? basePart.split("#")[0] : "/";
440
+ return base || "/";
441
+ };
442
+ var calculateSpecificity = (path6) => {
443
+ let score = 0;
444
+ const segments = path6.split("/").filter(Boolean);
445
+ for (const segment of segments) {
446
+ if (segment.startsWith(":")) {
447
+ score += 1;
448
+ if (/[?+*]$/.test(segment)) score -= 0.5;
449
+ } else if (segment === "*") {
450
+ score += 0.1;
451
+ } else {
452
+ score += 10;
453
+ }
454
+ }
455
+ score += segments.length * 0.1;
456
+ return score;
457
+ };
458
+ var isPlainObject = (v) => !!v && typeof v === "object" && Object.getPrototypeOf(v) === Object.prototype;
459
+ var createRouteMatchers = (routes) => {
460
+ const sortedRoutes = [...routes].sort((a, b) => calculateSpecificity(b.path) - calculateSpecificity(a.path));
461
+ return sortedRoutes.map((route) => {
462
+ const matcher = match(route.path, { decode: safeDecode });
463
+ const specificity = calculateSpecificity(route.path);
464
+ const keys = [];
465
+ return { route, matcher, keys, specificity };
466
+ });
467
+ };
468
+ var matchRoute = (url, routeMatchers) => {
469
+ const path6 = cleanPath(url);
470
+ for (const { route, matcher, keys } of routeMatchers) {
471
+ const match2 = matcher(path6);
472
+ if (match2) {
473
+ return {
474
+ route,
475
+ params: match2.params,
476
+ keys
477
+ };
478
+ }
479
+ }
480
+ return null;
481
+ };
482
+ var fetchInitialData = async (attr, params, serviceRegistry, ctx, callServiceMethodImpl = callServiceMethod) => {
483
+ const dataHandler = attr?.data;
484
+ if (!dataHandler || typeof dataHandler !== "function") return {};
485
+ try {
486
+ const result = await dataHandler(params, {
487
+ ...ctx,
488
+ headers: ctx.headers ?? {}
489
+ });
490
+ if (isServiceDescriptor(result)) {
491
+ const { serviceName, serviceMethod, args } = result;
492
+ return callServiceMethodImpl(serviceRegistry, serviceName, serviceMethod, args ?? {}, ctx);
493
+ }
494
+ if (isPlainObject(result)) return result;
495
+ throw AppError.badRequest("attr.data must return a plain object or a ServiceDescriptor");
496
+ } catch (err) {
497
+ const e = AppError.from(err);
498
+ const level = e.kind === "domain" || e.kind === "validation" || e.kind === "auth" ? "warn" : "error";
499
+ ctx.logger?.[level](e.message, {
500
+ component: "fetch-initial-data",
501
+ kind: e.kind,
502
+ httpStatus: e.httpStatus,
503
+ ...e.code && { code: e.code },
504
+ details: e.details,
505
+ params,
506
+ traceId: ctx.traceId
507
+ });
508
+ throw e;
509
+ }
510
+ };
239
511
 
240
- // src/security/auth.ts
241
- function createAuthHook(routes, isDebug) {
242
- const logger = createLogger(Boolean(isDebug));
512
+ // src/security/Auth.ts
513
+ var createAuthHook = (routeMatchers, logger) => {
243
514
  return async function authHook(req, reply) {
244
515
  const url = new URL(req.url, `http://${req.headers.host}`).pathname;
245
- const matched = routes.find((r) => r.path === url);
246
- const authConfig = matched?.attr?.middleware?.auth;
247
- if (!authConfig?.required) {
248
- debugLog(logger, "Auth not required for route", req);
516
+ const match2 = matchRoute(url, routeMatchers);
517
+ if (!match2) return;
518
+ const { route } = match2;
519
+ const authConfig = route.attr?.middleware?.auth;
520
+ if (!authConfig) {
521
+ logger.debug("auth", "(none)", { method: req.method, url: req.url });
249
522
  return;
250
523
  }
251
524
  if (typeof req.server.authenticate !== "function") {
252
- req.log.warn('Route requires auth but no "authenticate" decorator is defined on Fastify.');
525
+ logger.warn("Route requires auth but Fastify authenticate decorator is missing", {
526
+ path: url,
527
+ appId: route.appId
528
+ });
253
529
  return reply.status(500).send("Server misconfiguration: auth decorator missing.");
254
530
  }
255
531
  try {
256
- debugLog(logger, "Invoking authenticate(...)", req);
532
+ logger.debug("auth", "Invoking authenticate(...)", { method: req.method, url: req.url });
257
533
  await req.server.authenticate(req, reply);
258
- debugLog(logger, "Authentication successful", req);
534
+ logger.debug("auth", "Authentication successful", { method: req.method, url: req.url });
259
535
  } catch (err) {
260
- debugLog(logger, "Authentication failed", req);
536
+ logger.debug("auth", "Authentication failed", { method: req.method, url: req.url });
261
537
  return reply.send(err);
262
538
  }
263
539
  };
264
- }
540
+ };
265
541
 
266
- // src/security/csp.ts
542
+ // src/security/CSP.ts
543
+ var import_fastify_plugin = __toESM(require_plugin(), 1);
267
544
  import crypto from "crypto";
268
545
 
269
- // src/utils/Utils.ts
546
+ // src/utils/System.ts
270
547
  import { dirname, join } from "path";
271
548
  import "path";
272
549
  import { fileURLToPath } from "url";
273
- import { match } from "path-to-regexp";
274
550
  var isDevelopment = process.env.NODE_ENV === "development";
275
551
  var __filename = fileURLToPath(import.meta.url);
276
552
  var __dirname = join(dirname(__filename), !isDevelopment ? "./" : "..");
553
+
554
+ // src/logging/Logger.ts
555
+ var import_picocolors2 = __toESM(require_picocolors(), 1);
556
+
557
+ // src/logging/Parser.ts
558
+ function parseDebugInput(input) {
559
+ if (input === void 0) return void 0;
560
+ if (typeof input === "boolean") return input;
561
+ if (Array.isArray(input)) {
562
+ const pos = /* @__PURE__ */ new Set();
563
+ const neg = /* @__PURE__ */ new Set();
564
+ for (const raw of input) {
565
+ const s = String(raw);
566
+ const isNeg = s.startsWith("-") || s.startsWith("!");
567
+ const key = isNeg ? s.slice(1) : s;
568
+ const isValid = DEBUG_CATEGORIES.includes(key);
569
+ if (!isValid) {
570
+ console.warn(`[parseDebugInput] Invalid debug category: "${key}". Valid: ${DEBUG_CATEGORIES.join(", ")}`);
571
+ continue;
572
+ }
573
+ (isNeg ? neg : pos).add(key);
574
+ }
575
+ if (neg.size > 0 && pos.size === 0) {
576
+ const o = { all: true };
577
+ for (const k of neg) o[k] = false;
578
+ return o;
579
+ }
580
+ if (pos.size > 0 || neg.size > 0) {
581
+ const o = {};
582
+ for (const k of pos) o[k] = true;
583
+ for (const k of neg) o[k] = false;
584
+ return o;
585
+ }
586
+ return void 0;
587
+ }
588
+ if (typeof input === "string") {
589
+ const raw = input.trim();
590
+ if (!raw) return void 0;
591
+ if (raw === "*" || raw.toLowerCase() === "true" || raw.toLowerCase() === "all") return true;
592
+ const parts = raw.split(",").map((s) => s.trim()).filter(Boolean);
593
+ const flags = {};
594
+ const on = /* @__PURE__ */ new Set();
595
+ const off = /* @__PURE__ */ new Set();
596
+ for (const p of parts) {
597
+ const neg = p.startsWith("-") || p.startsWith("!");
598
+ const key = neg ? p.slice(1) : p;
599
+ const isValid = DEBUG_CATEGORIES.includes(key);
600
+ if (!isValid) {
601
+ console.warn(`[parseDebugInput] Invalid debug category: "${key}". Valid: ${DEBUG_CATEGORIES.join(", ")}`);
602
+ continue;
603
+ }
604
+ (neg ? off : on).add(key);
605
+ }
606
+ if (off.size > 0 && on.size === 0) {
607
+ flags.all = true;
608
+ for (const k of off) flags[k] = false;
609
+ return flags;
610
+ }
611
+ for (const k of on) flags[k] = true;
612
+ for (const k of off) flags[k] = false;
613
+ return flags;
614
+ }
615
+ return input;
616
+ }
617
+
618
+ // src/logging/Logger.ts
619
+ var DEBUG_CATEGORIES = ["auth", "routes", "errors", "vite", "network", "ssr"];
620
+ var Logger = class _Logger {
621
+ constructor(config = {}) {
622
+ this.config = config;
623
+ if (config.context) this.context = { ...config.context };
624
+ }
625
+ debugEnabled = /* @__PURE__ */ new Set();
626
+ context = {};
627
+ child(context) {
628
+ const child = new _Logger({
629
+ ...this.config,
630
+ context: { ...this.context, ...context }
631
+ });
632
+ child.debugEnabled = new Set(this.debugEnabled);
633
+ return child;
634
+ }
635
+ configure(debug) {
636
+ this.debugEnabled.clear();
637
+ if (debug === true) {
638
+ this.debugEnabled = new Set(DEBUG_CATEGORIES);
639
+ } else if (Array.isArray(debug)) {
640
+ this.debugEnabled = new Set(debug);
641
+ } else if (typeof debug === "object" && debug) {
642
+ if (debug.all) this.debugEnabled = new Set(DEBUG_CATEGORIES);
643
+ Object.entries(debug).forEach(([key, value]) => {
644
+ if (key !== "all" && typeof value === "boolean") {
645
+ if (value) this.debugEnabled.add(key);
646
+ else this.debugEnabled.delete(key);
647
+ }
648
+ });
649
+ }
650
+ }
651
+ isDebugEnabled(category) {
652
+ return this.debugEnabled.has(category);
653
+ }
654
+ shouldEmit(level) {
655
+ const order = { debug: 0, info: 1, warn: 2, error: 3 };
656
+ const minLevel = this.config.minLevel ?? "info";
657
+ return order[level] >= order[minLevel];
658
+ }
659
+ shouldIncludeStack(level) {
660
+ const include = this.config.includeStack;
661
+ if (include === void 0) return level === "error" || level === "warn" && process.env.NODE_ENV !== "production";
662
+ if (typeof include === "boolean") return include;
663
+ return include(level);
664
+ }
665
+ stripStacks(meta, seen = /* @__PURE__ */ new WeakSet()) {
666
+ if (!meta || typeof meta !== "object") return meta;
667
+ if (seen.has(meta)) return "[circular]";
668
+ seen.add(meta);
669
+ if (Array.isArray(meta)) return meta.map((v) => this.stripStacks(v, seen));
670
+ const copy = { ...meta };
671
+ for (const k of Object.keys(copy)) {
672
+ if (k === "stack" || k.endsWith("Stack")) {
673
+ delete copy[k];
674
+ } else {
675
+ copy[k] = this.stripStacks(copy[k], seen);
676
+ }
677
+ }
678
+ return copy;
679
+ }
680
+ formatTimestamp() {
681
+ const now = /* @__PURE__ */ new Date();
682
+ if (process.env.NODE_ENV === "production") return now.toISOString();
683
+ const hours = String(now.getHours()).padStart(2, "0");
684
+ const minutes = String(now.getMinutes()).padStart(2, "0");
685
+ const seconds = String(now.getSeconds()).padStart(2, "0");
686
+ const millis = String(now.getMilliseconds()).padStart(3, "0");
687
+ return `${hours}:${minutes}:${seconds}.${millis}`;
688
+ }
689
+ emit(level, message, meta, category) {
690
+ if (!this.shouldEmit(level)) return;
691
+ const timestamp = this.formatTimestamp();
692
+ const wantCtx = this.config.includeContext === void 0 ? false : typeof this.config.includeContext === "function" ? this.config.includeContext(level) : this.config.includeContext;
693
+ const customSink = this.config.custom?.[level] ?? (level === "debug" ? this.config.custom?.info : void 0) ?? this.config.custom?.log;
694
+ const consoleFallback = level === "error" ? console.error : level === "warn" ? console.warn : console.log;
695
+ const sink = customSink ?? consoleFallback;
696
+ const merged = meta ?? {};
697
+ const withCtx = wantCtx && Object.keys(this.context).length > 0 ? { context: this.context, ...merged } : merged;
698
+ const finalMeta = this.shouldIncludeStack(level) ? withCtx : this.stripStacks(withCtx);
699
+ const hasMeta = finalMeta && typeof finalMeta === "object" ? Object.keys(finalMeta).length > 0 : false;
700
+ const coloredLevel = (() => {
701
+ const levelText = level.toLowerCase() + (category ? `:${category.toLowerCase()}` : "");
702
+ switch (level) {
703
+ case "debug":
704
+ return import_picocolors2.default.gray(`[${levelText}]`);
705
+ case "info":
706
+ return import_picocolors2.default.cyan(`[${levelText}]`);
707
+ case "warn":
708
+ return import_picocolors2.default.yellow(`[${levelText}]`);
709
+ case "error":
710
+ return import_picocolors2.default.red(`[${levelText}]`);
711
+ default:
712
+ return `[${levelText}]`;
713
+ }
714
+ })();
715
+ const formatted = `${timestamp} ${coloredLevel} ${message}`;
716
+ if (this.config.singleLine && hasMeta) {
717
+ const metaStr = JSON.stringify(finalMeta).replace(/\n/g, "\\n");
718
+ sink(`${formatted} ${metaStr}`);
719
+ return;
720
+ }
721
+ if (customSink) {
722
+ if (hasMeta) sink(formatted, finalMeta);
723
+ else sink(formatted);
724
+ } else {
725
+ if (hasMeta) consoleFallback(formatted, finalMeta);
726
+ else consoleFallback(formatted);
727
+ }
728
+ }
729
+ info(message, meta) {
730
+ this.emit("info", message, meta);
731
+ }
732
+ warn(message, meta) {
733
+ this.emit("warn", message, meta);
734
+ }
735
+ error(message, meta) {
736
+ this.emit("error", message, meta);
737
+ }
738
+ debug(category, message, meta) {
739
+ if (!this.debugEnabled.has(category)) return;
740
+ this.emit("debug", message, meta, category);
741
+ }
742
+ };
743
+ function createLogger(opts) {
744
+ const logger = new Logger({
745
+ custom: opts?.custom,
746
+ context: opts?.context,
747
+ minLevel: opts?.minLevel,
748
+ includeStack: opts?.includeStack,
749
+ includeContext: opts?.includeContext,
750
+ singleLine: opts?.singleLine
751
+ });
752
+ const parsed = parseDebugInput(opts?.debug);
753
+ if (parsed !== void 0) logger.configure(parsed);
754
+ return logger;
755
+ }
756
+
757
+ // src/security/CSP.ts
758
+ var defaultGenerateCSP = (directives, nonce, req) => {
759
+ const merged = { ...directives };
760
+ merged["script-src"] = merged["script-src"] || ["'self'"];
761
+ if (!merged["script-src"].some((v) => v.startsWith("'nonce-"))) {
762
+ merged["script-src"].push(`'nonce-${nonce}'`);
763
+ }
764
+ if (isDevelopment) {
765
+ const connect = merged["connect-src"] || ["'self'"];
766
+ if (!connect.includes("ws:")) connect.push("ws:");
767
+ if (!connect.includes("http:")) connect.push("http:");
768
+ merged["connect-src"] = connect;
769
+ const style = merged["style-src"] || ["'self'"];
770
+ if (!style.includes("'unsafe-inline'")) style.push("'unsafe-inline'");
771
+ merged["style-src"] = style;
772
+ }
773
+ return Object.entries(merged).map(([key, values]) => `${key} ${values.join(" ")}`).join("; ");
774
+ };
775
+ var generateNonce = () => crypto.randomBytes(16).toString("base64");
776
+ var mergeDirectives = (base, override) => {
777
+ const merged = { ...base };
778
+ for (const [directive, values] of Object.entries(override)) {
779
+ if (merged[directive]) {
780
+ merged[directive] = [.../* @__PURE__ */ new Set([...merged[directive], ...values])];
781
+ } else {
782
+ merged[directive] = [...values];
783
+ }
784
+ }
785
+ return merged;
786
+ };
787
+ var findMatchingRoute = (routeMatchers, path6) => {
788
+ if (!routeMatchers) return null;
789
+ const match2 = matchRoute(path6, routeMatchers);
790
+ return match2 ? { route: match2.route, params: match2.params } : null;
791
+ };
792
+ var cspPlugin = (0, import_fastify_plugin.default)(
793
+ async (fastify, opts) => {
794
+ const { generateCSP = defaultGenerateCSP, routes = [], routeMatchers, debug } = opts;
795
+ const globalDirectives = opts.directives || DEV_CSP_DIRECTIVES;
796
+ const matchers = routeMatchers || (routes.length > 0 ? createRouteMatchers(routes) : null);
797
+ const logger = createLogger({
798
+ debug,
799
+ context: { component: "csp-plugin" }
800
+ });
801
+ fastify.addHook("onRequest", (req, reply, done) => {
802
+ const nonce = generateNonce();
803
+ req.cspNonce = nonce;
804
+ try {
805
+ const routeMatch = findMatchingRoute(matchers, req.url);
806
+ const routeCSP = routeMatch?.route.attr?.middleware?.csp;
807
+ if (routeCSP === false) {
808
+ done();
809
+ return;
810
+ }
811
+ let finalDirectives = globalDirectives;
812
+ if (routeCSP && typeof routeCSP === "object") {
813
+ if (!routeCSP.disabled) {
814
+ let routeDirectives;
815
+ if (typeof routeCSP.directives === "function") {
816
+ const params = routeMatch?.params || {};
817
+ routeDirectives = routeCSP.directives({
818
+ url: req.url,
819
+ params,
820
+ headers: req.headers,
821
+ req
822
+ });
823
+ } else {
824
+ routeDirectives = routeCSP.directives || {};
825
+ }
826
+ if (routeCSP.mode === "replace") {
827
+ finalDirectives = routeDirectives;
828
+ } else {
829
+ finalDirectives = mergeDirectives(globalDirectives, routeDirectives);
830
+ }
831
+ }
832
+ }
833
+ let cspHeader;
834
+ if (routeCSP?.generateCSP) {
835
+ cspHeader = routeCSP.generateCSP(finalDirectives, nonce, req);
836
+ } else {
837
+ cspHeader = generateCSP(finalDirectives, nonce, req);
838
+ }
839
+ reply.header("Content-Security-Policy", cspHeader);
840
+ } catch (error) {
841
+ logger.error("CSP plugin error", {
842
+ url: req.url,
843
+ error: error instanceof Error ? { name: error.name, message: error.message, stack: error.stack } : String(error)
844
+ });
845
+ const fallbackHeader = generateCSP(globalDirectives, nonce, req);
846
+ reply.header("Content-Security-Policy", fallbackHeader);
847
+ }
848
+ done();
849
+ });
850
+ },
851
+ { name: "taujs-csp-plugin" }
852
+ );
853
+
854
+ // src/utils/AssetManager.ts
855
+ import { readFile } from "fs/promises";
856
+ import path2 from "path";
857
+ import { pathToFileURL } from "url";
858
+
859
+ // src/utils/Templates.ts
277
860
  var CSS_LANGS_RE = /\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\?)/;
278
861
  async function collectStyle(server, entries) {
279
862
  const urls = await collectStyleUrls(server, entries);
@@ -336,44 +919,6 @@ function renderPreloadLink(file) {
336
919
  return "";
337
920
  }
338
921
  }
339
- var callServiceMethod = async (registry, serviceName, methodName, params) => {
340
- const service = registry[serviceName];
341
- if (!service) throw new Error(`Service ${String(serviceName)} does not exist in the registry`);
342
- const method = service[methodName];
343
- if (typeof method !== "function") throw new Error(`Service method ${String(methodName)} does not exist on ${String(serviceName)}`);
344
- const data = await method(params);
345
- if (typeof data !== "object" || data === null)
346
- throw new Error(`Expected object response from ${String(serviceName)}.${String(methodName)}, but got ${typeof data}`);
347
- return data;
348
- };
349
- var isServiceDescriptor = (obj) => {
350
- if (typeof obj !== "object" || obj === null || Array.isArray(obj)) return false;
351
- const maybe = obj;
352
- return typeof maybe.serviceName === "string" && typeof maybe.serviceMethod === "string";
353
- };
354
- var fetchInitialData = async (attr, params, serviceRegistry, ctx = { headers: {} }, callServiceMethodImpl = callServiceMethod) => {
355
- const dataHandler = attr?.data;
356
- if (!dataHandler || typeof dataHandler !== "function") return Promise.resolve({});
357
- return dataHandler(params, ctx).then(async (result) => {
358
- if (isServiceDescriptor(result)) {
359
- const { serviceName, serviceMethod, args } = result;
360
- if (serviceRegistry[serviceName]?.[serviceMethod]) return callServiceMethodImpl(serviceRegistry, serviceName, serviceMethod, args ?? {});
361
- throw new Error(`Invalid service: serviceName=${String(serviceName)}, method=${String(serviceMethod)}`);
362
- }
363
- if (typeof result === "object" && result !== null) return result;
364
- throw new Error("Invalid result from attr.data");
365
- });
366
- };
367
- var matchRoute = (url, renderRoutes) => {
368
- for (const route of renderRoutes) {
369
- const matcher = match(route.path, {
370
- decode: decodeURIComponent
371
- });
372
- const matched = matcher(url);
373
- if (matched) return { route, params: matched.params };
374
- }
375
- return null;
376
- };
377
922
  function getCssLinks(manifest, basePath = "") {
378
923
  const seen = /* @__PURE__ */ new Set();
379
924
  const styles = [];
@@ -401,79 +946,32 @@ var ensureNonNull = (value, errorMessage) => {
401
946
  if (value === void 0 || value === null) throw new Error(errorMessage);
402
947
  return value;
403
948
  };
404
-
405
- // src/security/csp.ts
406
- var defaultGenerateCSP = (directives, nonce) => {
407
- const merged = { ...directives };
408
- merged["script-src"] = merged["script-src"] || ["'self'"];
409
- if (!merged["script-src"].some((v) => v.startsWith("'nonce-"))) merged["script-src"].push(`'nonce-${nonce}'`);
410
- if (isDevelopment) {
411
- const connect = merged["connect-src"] || ["'self'"];
412
- if (!connect.includes("ws:")) connect.push("ws:");
413
- if (!connect.includes("http:")) connect.push("http:");
414
- merged["connect-src"] = connect;
415
- const style = merged["style-src"] || ["'self'"];
416
- if (!style.includes("'unsafe-inline'")) style.push("'unsafe-inline'");
417
- merged["style-src"] = style;
418
- }
419
- return Object.entries(merged).map(([key, values]) => `${key} ${values.join(" ")}`).join("; ");
420
- };
421
- var generateNonce = () => crypto.randomBytes(16).toString("base64");
422
- var createCSPHook = (options = {}) => (req, reply, done) => {
423
- const nonce = generateNonce();
424
- const directives = options.directives ?? DEV_CSP_DIRECTIVES;
425
- const generate = options.generateCSP ?? defaultGenerateCSP;
426
- const cspHeader = generate(directives, nonce);
427
- reply.header("Content-Security-Policy", cspHeader);
428
- if (typeof options.exposeNonce === "function") {
429
- options.exposeNonce(req, nonce);
430
- } else {
431
- req.nonce = nonce;
432
- }
433
- done();
434
- };
435
- var applyCSP = (security, reply) => {
436
- const nonce = generateNonce();
437
- const directives = security?.csp?.directives ?? DEV_CSP_DIRECTIVES;
438
- const generate = security?.csp?.generateCSP ?? defaultGenerateCSP;
439
- const header = generate(directives, nonce);
440
- reply.header("Content-Security-Policy", header);
441
- reply.request.nonce = nonce;
442
- return nonce;
443
- };
444
-
445
- // src/security/verifyMiddleware.ts
446
- var isAuthRequired = (r) => r.attr?.middleware?.auth?.required === true;
447
- var hasAuthenticate = (app) => typeof app.authenticate === "function";
448
- var verifyContracts = (app, routes, contracts, isDebug) => {
449
- const logger = createLogger(Boolean(isDebug));
450
- for (const contract of contracts) {
451
- const isUsed = routes.some(contract.required);
452
- if (!isUsed) {
453
- debugLog(logger, `Middleware "${contract.key}" not used in any routes`);
454
- continue;
455
- }
456
- if (!contract.verify(app)) {
457
- const error = new Error(`[\u03C4js] ${contract.errorMessage}`);
458
- logger.error(error.message);
459
- throw error;
460
- }
461
- debugLog(logger, `Middleware "${contract.key}" verified \u2713`);
462
- }
463
- };
464
-
465
- // src/SSRServer.ts
466
- var createMaps = () => {
949
+ function processTemplate(template) {
950
+ const [headSplit, bodySplit] = template.split(SSRTAG.ssrHead);
951
+ if (typeof bodySplit === "undefined") throw new Error(`Template is missing ${SSRTAG.ssrHead} marker.`);
952
+ const [beforeBody, afterBody] = bodySplit.split(SSRTAG.ssrHtml);
953
+ if (typeof beforeBody === "undefined" || typeof afterBody === "undefined") throw new Error(`Template is missing ${SSRTAG.ssrHtml} marker.`);
467
954
  return {
468
- bootstrapModules: /* @__PURE__ */ new Map(),
469
- cssLinks: /* @__PURE__ */ new Map(),
470
- manifests: /* @__PURE__ */ new Map(),
471
- preloadLinks: /* @__PURE__ */ new Map(),
472
- renderModules: /* @__PURE__ */ new Map(),
473
- ssrManifests: /* @__PURE__ */ new Map(),
474
- templates: /* @__PURE__ */ new Map()
955
+ beforeHead: headSplit,
956
+ afterHead: "",
957
+ beforeBody: beforeBody.replace(/\s*$/, ""),
958
+ afterBody: afterBody.replace(/^\s*/, "")
475
959
  };
960
+ }
961
+ var rebuildTemplate = (parts, headContent, bodyContent) => {
962
+ return `${parts.beforeHead}${headContent}${parts.afterHead}${parts.beforeBody}${bodyContent}${parts.afterBody}`;
476
963
  };
964
+
965
+ // src/utils/AssetManager.ts
966
+ var createMaps = () => ({
967
+ bootstrapModules: /* @__PURE__ */ new Map(),
968
+ cssLinks: /* @__PURE__ */ new Map(),
969
+ manifests: /* @__PURE__ */ new Map(),
970
+ preloadLinks: /* @__PURE__ */ new Map(),
971
+ renderModules: /* @__PURE__ */ new Map(),
972
+ ssrManifests: /* @__PURE__ */ new Map(),
973
+ templates: /* @__PURE__ */ new Map()
974
+ });
477
975
  var processConfigs = (configs, baseClientRoot, templateDefaults) => {
478
976
  return configs.map((config) => {
479
977
  const clientRoot = path2.resolve(baseClientRoot, config.entryPoint);
@@ -487,54 +985,605 @@ var processConfigs = (configs, baseClientRoot, templateDefaults) => {
487
985
  };
488
986
  });
489
987
  };
490
- var SSRServer = (0, import_fastify_plugin.default)(
491
- async (app, opts) => {
492
- const logger = createLogger(opts.isDebug ?? false);
493
- const { alias, configs, routes, serviceRegistry, isDebug, clientRoot: baseClientRoot } = opts;
494
- const { bootstrapModules, cssLinks, manifests, preloadLinks, renderModules, ssrManifests, templates } = createMaps();
495
- const processedConfigs = processConfigs(configs, baseClientRoot, TEMPLATE);
496
- for (const config of processedConfigs) {
497
- const { clientRoot, entryClient, htmlTemplate } = config;
988
+ var loadAssets = async (processedConfigs, baseClientRoot, bootstrapModules, cssLinks, manifests, preloadLinks, renderModules, ssrManifests, templates, opts = {}) => {
989
+ const logger = opts.logger ?? createLogger({
990
+ debug: opts.debug,
991
+ includeContext: true
992
+ });
993
+ for (const config of processedConfigs) {
994
+ const { clientRoot, entryClient, entryServer, htmlTemplate } = config;
995
+ try {
498
996
  const templateHtmlPath = path2.join(clientRoot, htmlTemplate);
499
997
  const templateHtml = await readFile(templateHtmlPath, "utf-8");
500
998
  templates.set(clientRoot, templateHtml);
501
999
  const relativeBasePath = path2.relative(baseClientRoot, clientRoot).replace(/\\/g, "/");
502
1000
  const adjustedRelativePath = relativeBasePath ? `/${relativeBasePath}` : "";
503
1001
  if (!isDevelopment) {
504
- const manifestPath = path2.join(clientRoot, ".vite/manifest.json");
505
- const manifestContent = await readFile(manifestPath, "utf-8");
506
- const manifest = JSON.parse(manifestContent);
507
- manifests.set(clientRoot, manifest);
508
- const ssrManifestPath = path2.join(clientRoot, ".vite/ssr-manifest.json");
509
- const ssrManifestContent = await readFile(ssrManifestPath, "utf-8");
510
- const ssrManifest = JSON.parse(ssrManifestContent);
511
- ssrManifests.set(clientRoot, ssrManifest);
512
- const entryClientFile = manifest[`${entryClient}.tsx`]?.file;
513
- if (!entryClientFile) throw new Error(`Entry client file not found in manifest for ${entryClient}.tsx`);
514
- const bootstrapModule = `/${adjustedRelativePath}/${entryClientFile}`.replace(/\/{2,}/g, "/");
515
- bootstrapModules.set(clientRoot, bootstrapModule);
516
- const preloadLink = renderPreloadLinks(ssrManifest, adjustedRelativePath);
517
- preloadLinks.set(clientRoot, preloadLink);
518
- const cssLink = getCssLinks(manifest, adjustedRelativePath);
519
- cssLinks.set(clientRoot, cssLink);
1002
+ try {
1003
+ const manifestPath = path2.join(clientRoot, ".vite/manifest.json");
1004
+ const manifestContent = await readFile(manifestPath, "utf-8");
1005
+ const manifest = JSON.parse(manifestContent);
1006
+ manifests.set(clientRoot, manifest);
1007
+ const ssrManifestPath = path2.join(clientRoot, ".vite/ssr-manifest.json");
1008
+ const ssrManifestContent = await readFile(ssrManifestPath, "utf-8");
1009
+ const ssrManifest = JSON.parse(ssrManifestContent);
1010
+ ssrManifests.set(clientRoot, ssrManifest);
1011
+ const entryClientFile = manifest[`${entryClient}.tsx`]?.file;
1012
+ if (!entryClientFile) {
1013
+ throw AppError.internal(`Entry client file not found in manifest for ${entryClient}.tsx`, {
1014
+ details: {
1015
+ clientRoot,
1016
+ entryClient,
1017
+ availableKeys: Object.keys(manifest)
1018
+ }
1019
+ });
1020
+ }
1021
+ const bootstrapModule = `/${adjustedRelativePath}/${entryClientFile}`.replace(/\/{2,}/g, "/");
1022
+ bootstrapModules.set(clientRoot, bootstrapModule);
1023
+ const preloadLink = renderPreloadLinks(ssrManifest, adjustedRelativePath);
1024
+ preloadLinks.set(clientRoot, preloadLink);
1025
+ const cssLink = getCssLinks(manifest, adjustedRelativePath);
1026
+ cssLinks.set(clientRoot, cssLink);
1027
+ const renderModulePath = path2.join(clientRoot, `${entryServer}.js`);
1028
+ const moduleUrl = pathToFileURL(renderModulePath).href;
1029
+ try {
1030
+ const importedModule = await import(moduleUrl);
1031
+ renderModules.set(clientRoot, importedModule);
1032
+ } catch (err) {
1033
+ throw AppError.internal(`Failed to load render module ${renderModulePath}`, {
1034
+ cause: err,
1035
+ details: { moduleUrl, clientRoot, entryServer }
1036
+ });
1037
+ }
1038
+ } catch (err) {
1039
+ if (err instanceof AppError) {
1040
+ logger.error("Asset load failed", {
1041
+ error: { name: err.name, message: err.message, stack: err.stack, code: err.code },
1042
+ stage: "loadAssets:production"
1043
+ });
1044
+ } else {
1045
+ logger.error("Asset load failed", {
1046
+ error: err instanceof Error ? { name: err.name, message: err.message, stack: err.stack } : String(err),
1047
+ stage: "loadAssets:production"
1048
+ });
1049
+ }
1050
+ }
520
1051
  } else {
521
1052
  const bootstrapModule = `/${adjustedRelativePath}/${entryClient}`.replace(/\/{2,}/g, "/");
522
1053
  bootstrapModules.set(clientRoot, bootstrapModule);
523
1054
  }
1055
+ } catch (err) {
1056
+ logger.error("Failed to process config", {
1057
+ error: err instanceof Error ? { name: err.name, message: err.message, stack: err.stack } : String(err),
1058
+ stage: "loadAssets:config"
1059
+ });
524
1060
  }
525
- let viteDevServer;
526
- verifyContracts(
527
- app,
528
- routes,
529
- [
1061
+ }
1062
+ };
1063
+
1064
+ // src/utils/DevServer.ts
1065
+ import path3 from "path";
1066
+ var setupDevServer = async (app, baseClientRoot, alias, debug, devNet) => {
1067
+ const logger = createLogger({
1068
+ context: { service: "setupDevServer" },
1069
+ debug,
1070
+ minLevel: "debug"
1071
+ });
1072
+ const host = devNet?.host ?? process.env.HOST?.trim() ?? process.env.FASTIFY_ADDRESS?.trim() ?? "localhost";
1073
+ const hmrPort = devNet?.hmrPort ?? (Number(process.env.HMR_PORT) || 5174);
1074
+ const { createServer: createServer2 } = await import("vite");
1075
+ const viteDevServer = await createServer2({
1076
+ appType: "custom",
1077
+ css: {
1078
+ preprocessorOptions: {
1079
+ scss: {
1080
+ api: "modern-compiler"
1081
+ }
1082
+ }
1083
+ },
1084
+ mode: "development",
1085
+ plugins: [
1086
+ ...debug ? [
1087
+ {
1088
+ name: "\u03C4js-development-server-debug-logging",
1089
+ configureServer(server) {
1090
+ logger.debug("vite", `${CONTENT.TAG} Development server debug started`);
1091
+ server.middlewares.use((req, res, next) => {
1092
+ logger.debug("vite", "\u2190 rx", {
1093
+ method: req.method,
1094
+ url: req.url,
1095
+ host: req.headers.host,
1096
+ ua: req.headers["user-agent"]
1097
+ });
1098
+ res.on("finish", () => {
1099
+ logger.debug("vite", "\u2192 tx", {
1100
+ method: req.method,
1101
+ url: req.url,
1102
+ statusCode: res.statusCode
1103
+ });
1104
+ });
1105
+ next();
1106
+ });
1107
+ }
1108
+ }
1109
+ ] : []
1110
+ ],
1111
+ resolve: {
1112
+ alias: {
1113
+ "@client": path3.resolve(baseClientRoot),
1114
+ "@server": path3.resolve(__dirname),
1115
+ "@shared": path3.resolve(__dirname, "../shared"),
1116
+ ...alias
1117
+ }
1118
+ },
1119
+ root: baseClientRoot,
1120
+ server: {
1121
+ middlewareMode: true,
1122
+ hmr: {
1123
+ clientPort: hmrPort,
1124
+ host: host !== "localhost" ? host : void 0,
1125
+ port: hmrPort,
1126
+ protocol: "ws"
1127
+ }
1128
+ }
1129
+ });
1130
+ overrideCSSHMRConsoleError();
1131
+ app.addHook("onRequest", async (request, reply) => {
1132
+ await new Promise((resolve) => {
1133
+ viteDevServer.middlewares(request.raw, reply.raw, () => {
1134
+ if (!reply.sent) resolve();
1135
+ });
1136
+ });
1137
+ });
1138
+ return viteDevServer;
1139
+ };
1140
+
1141
+ // src/utils/HandleRender.ts
1142
+ import path4 from "path";
1143
+ import { PassThrough } from "stream";
1144
+
1145
+ // src/utils/Telemetry.ts
1146
+ import crypto2 from "crypto";
1147
+ function createRequestContext(req, reply, baseLogger) {
1148
+ const raw = typeof req.headers["x-trace-id"] === "string" ? req.headers["x-trace-id"] : "";
1149
+ const traceId = raw && REGEX.SAFE_TRACE.test(raw) ? raw : typeof req.id === "string" ? req.id : crypto2.randomUUID();
1150
+ reply.header("x-trace-id", traceId);
1151
+ const anyLogger = baseLogger;
1152
+ const child = anyLogger.child;
1153
+ const logger = typeof child === "function" ? child.call(baseLogger, { traceId, url: req.url, method: req.method }) : baseLogger;
1154
+ const headers = Object.fromEntries(
1155
+ Object.entries(req.headers).map(([headerName, headerValue]) => {
1156
+ const normalisedValue = Array.isArray(headerValue) ? headerValue.join(",") : headerValue ?? "";
1157
+ return [headerName, normalisedValue];
1158
+ })
1159
+ );
1160
+ return { traceId, logger, headers };
1161
+ }
1162
+
1163
+ // src/utils/HandleRender.ts
1164
+ var handleRender = async (req, reply, routeMatchers, processedConfigs, serviceRegistry, maps, opts = {}) => {
1165
+ const { viteDevServer } = opts;
1166
+ const logger = opts.logger ?? createLogger({
1167
+ debug: opts.debug,
1168
+ minLevel: isDevelopment ? "debug" : "info",
1169
+ includeContext: true,
1170
+ includeStack: (lvl) => lvl === "error" || isDevelopment
1171
+ });
1172
+ try {
1173
+ if (/\.\w+$/.test(req.raw.url ?? "")) return reply.callNotFound();
1174
+ const url = req.url ? new URL(req.url, `http://${req.headers.host}`).pathname : "/";
1175
+ const matchedRoute = matchRoute(url, routeMatchers);
1176
+ const rawNonce = req.cspNonce;
1177
+ const cspNonce = rawNonce && rawNonce.length > 0 ? rawNonce : void 0;
1178
+ if (!matchedRoute) {
1179
+ reply.callNotFound();
1180
+ return;
1181
+ }
1182
+ const { route, params } = matchedRoute;
1183
+ const { attr, appId } = route;
1184
+ const config = processedConfigs.find((c) => c.appId === appId);
1185
+ if (!config) {
1186
+ throw AppError.internal("No configuration found for the request", {
1187
+ details: {
1188
+ appId,
1189
+ availableAppIds: processedConfigs.map((c) => c.appId),
1190
+ url
1191
+ }
1192
+ });
1193
+ }
1194
+ const { clientRoot, entryServer } = config;
1195
+ let template = ensureNonNull(maps.templates.get(clientRoot), `Template not found for clientRoot: ${clientRoot}`);
1196
+ const bootstrapModule = maps.bootstrapModules.get(clientRoot);
1197
+ const cssLink = maps.cssLinks.get(clientRoot);
1198
+ const manifest = maps.manifests.get(clientRoot);
1199
+ const preloadLink = maps.preloadLinks.get(clientRoot);
1200
+ const ssrManifest = maps.ssrManifests.get(clientRoot);
1201
+ let renderModule;
1202
+ if (isDevelopment && viteDevServer) {
1203
+ try {
1204
+ template = template.replace(/<script type="module" src="\/@vite\/client"><\/script>/g, "");
1205
+ template = template.replace(/<style type="text\/css">[\s\S]*?<\/style>/g, "");
1206
+ const entryServerPath = path4.join(clientRoot, `${entryServer}.tsx`);
1207
+ const executedModule = await viteDevServer.ssrLoadModule(entryServerPath);
1208
+ renderModule = executedModule;
1209
+ const styles = await collectStyle(viteDevServer, [entryServerPath]);
1210
+ const styleNonce = cspNonce ? ` nonce="${cspNonce}"` : "";
1211
+ template = template?.replace("</head>", `<style type="text/css"${styleNonce}>${styles}</style></head>`);
1212
+ template = await viteDevServer.transformIndexHtml(url, template);
1213
+ } catch (error) {
1214
+ throw AppError.internal("Failed to load dev assets", { cause: error, details: { clientRoot, entryServer, url } });
1215
+ }
1216
+ } else {
1217
+ renderModule = maps.renderModules.get(clientRoot);
1218
+ if (!renderModule) throw AppError.internal(`Render module not found for clientRoot: ${clientRoot}. Module should have been preloaded.`);
1219
+ }
1220
+ const renderType = attr?.render ?? RENDERTYPE.ssr;
1221
+ const templateParts = processTemplate(template);
1222
+ const baseLogger = opts.logger ?? logger;
1223
+ const { traceId, logger: reqLogger, headers } = createRequestContext(req, reply, baseLogger);
1224
+ const ctx = { traceId, logger: reqLogger, headers };
1225
+ const initialDataInput = () => fetchInitialData(attr, params, serviceRegistry, ctx);
1226
+ if (renderType === RENDERTYPE.ssr) {
1227
+ const { renderSSR } = renderModule;
1228
+ if (!renderSSR) {
1229
+ throw AppError.internal("renderSSR function not found in module", {
1230
+ details: { clientRoot, availableFunctions: Object.keys(renderModule) }
1231
+ });
1232
+ }
1233
+ const ac = new AbortController();
1234
+ const onAborted = () => ac.abort("client_aborted");
1235
+ req.raw.on("aborted", onAborted);
1236
+ reply.raw.on("close", () => {
1237
+ if (!reply.raw.writableEnded) ac.abort("socket_closed");
1238
+ });
1239
+ reply.raw.on("finish", () => req.raw.off("aborted", onAborted));
1240
+ if (ac.signal.aborted) {
1241
+ logger.warn("SSR skipped; already aborted", { url: req.url });
1242
+ return;
1243
+ }
1244
+ const initialDataResolved = await initialDataInput();
1245
+ let headContent = "";
1246
+ let appHtml = "";
1247
+ try {
1248
+ const res = await renderSSR(initialDataResolved, req.url, attr?.meta, ac.signal, { logger: reqLogger });
1249
+ headContent = res.headContent;
1250
+ appHtml = res.appHtml;
1251
+ } catch (err) {
1252
+ const msg = String(err?.message ?? err ?? "");
1253
+ const benign = REGEX.BENIGN_NET_ERR.test(msg);
1254
+ if (ac.signal.aborted || benign) {
1255
+ logger.warn("SSR aborted mid-render (benign)", { url: req.url, reason: msg });
1256
+ return;
1257
+ }
1258
+ logger.error("SSR render failed", { url: req.url, error: normaliseError(err) });
1259
+ throw err;
1260
+ }
1261
+ let aggregateHeadContent = headContent;
1262
+ if (ssrManifest && preloadLink) aggregateHeadContent += preloadLink;
1263
+ if (manifest && cssLink) aggregateHeadContent += cssLink;
1264
+ const shouldHydrate = attr?.hydrate !== false;
1265
+ const nonceAttr = cspNonce ? ` nonce="${cspNonce}"` : "";
1266
+ const initialDataScript = `<script${nonceAttr}>window.__INITIAL_DATA__ = ${JSON.stringify(initialDataResolved).replace(/</g, "\\u003c")};</script>`;
1267
+ const bootstrapScriptTag = shouldHydrate && bootstrapModule ? `<script${nonceAttr} type="module" src="${bootstrapModule}" defer></script>` : "";
1268
+ const safeAppHtml = appHtml.trim();
1269
+ const fullHtml = rebuildTemplate(templateParts, aggregateHeadContent, `${safeAppHtml}${initialDataScript}${bootstrapScriptTag}`);
1270
+ try {
1271
+ return reply.status(200).header("Content-Type", "text/html").send(fullHtml);
1272
+ } catch (err) {
1273
+ const msg = String(err?.message ?? err ?? "");
1274
+ const benign = REGEX.BENIGN_NET_ERR.test(msg);
1275
+ if (!benign) logger.error("SSR send failed", { url: req.url, error: normaliseError(err) });
1276
+ else logger.warn("SSR send aborted (benign)", { url: req.url, reason: msg });
1277
+ return;
1278
+ }
1279
+ } else {
1280
+ const { renderStream } = renderModule;
1281
+ if (!renderStream) {
1282
+ throw AppError.internal("renderStream function not found in module", {
1283
+ details: { clientRoot, availableFunctions: Object.keys(renderModule) }
1284
+ });
1285
+ }
1286
+ const cspHeader = reply.getHeader("Content-Security-Policy");
1287
+ reply.raw.writeHead(200, {
1288
+ "Content-Security-Policy": cspHeader,
1289
+ "Content-Type": "text/html; charset=utf-8"
1290
+ });
1291
+ const ac = new AbortController();
1292
+ const onAborted = () => ac.abort();
1293
+ req.raw.on("aborted", onAborted);
1294
+ reply.raw.on("close", () => {
1295
+ if (!reply.raw.writableEnded) ac.abort();
1296
+ });
1297
+ reply.raw.on("finish", () => req.raw.off("aborted", onAborted));
1298
+ const shouldHydrate = attr?.hydrate !== false;
1299
+ const abortedState = { aborted: false };
1300
+ const isBenignSocketAbort = (e) => {
1301
+ const msg = String(e?.message ?? e ?? "");
1302
+ return REGEX.BENIGN_NET_ERR.test(msg);
1303
+ };
1304
+ const writable = new PassThrough();
1305
+ writable.on("error", (err) => {
1306
+ if (!isBenignSocketAbort(err)) logger.error("PassThrough error:", { error: err });
1307
+ });
1308
+ reply.raw.on("error", (err) => {
1309
+ if (!isBenignSocketAbort(err)) logger.error("HTTP socket error:", { error: err });
1310
+ });
1311
+ writable.pipe(reply.raw, { end: false });
1312
+ let finalData = void 0;
1313
+ renderStream(
1314
+ writable,
530
1315
  {
531
- key: "auth",
532
- required: isAuthRequired,
533
- verify: hasAuthenticate,
534
- errorMessage: "Routes require auth but Fastify instance is missing `.authenticate` decorator."
1316
+ onHead: (headContent) => {
1317
+ let aggregateHeadContent = headContent;
1318
+ if (ssrManifest && preloadLink) aggregateHeadContent += preloadLink;
1319
+ if (manifest && cssLink) aggregateHeadContent += cssLink;
1320
+ return reply.raw.write(`${templateParts.beforeHead}${aggregateHeadContent}${templateParts.afterHead}${templateParts.beforeBody}`);
1321
+ },
1322
+ onShellReady: () => {
1323
+ },
1324
+ onAllReady: (data) => {
1325
+ if (!abortedState.aborted) finalData = data;
1326
+ },
1327
+ onError: (err) => {
1328
+ if (abortedState.aborted || isBenignSocketAbort(err)) {
1329
+ logger.warn("Client disconnected before stream finished");
1330
+ try {
1331
+ if (!reply.raw.writableEnded && !reply.raw.destroyed) reply.raw.destroy();
1332
+ } catch (e) {
1333
+ logger.debug?.("stream teardown: destroy() failed", { error: normaliseError(e) });
1334
+ }
1335
+ return;
1336
+ }
1337
+ abortedState.aborted = true;
1338
+ logger.error("Critical rendering error during stream", {
1339
+ error: normaliseError(err),
1340
+ clientRoot,
1341
+ url: req.url
1342
+ });
1343
+ try {
1344
+ ac?.abort?.();
1345
+ } catch (e) {
1346
+ logger.debug?.("stream teardown: abort() failed", { error: normaliseError(e) });
1347
+ }
1348
+ const reason = toReason(err);
1349
+ try {
1350
+ if (!reply.raw.writableEnded && !reply.raw.destroyed) reply.raw.destroy(reason);
1351
+ } catch (e) {
1352
+ logger.debug?.("stream teardown: destroy() failed", { error: normaliseError(e) });
1353
+ }
1354
+ }
1355
+ },
1356
+ initialDataInput,
1357
+ req.url,
1358
+ shouldHydrate ? bootstrapModule : void 0,
1359
+ attr?.meta,
1360
+ cspNonce,
1361
+ ac.signal,
1362
+ { logger: reqLogger }
1363
+ );
1364
+ writable.on("finish", () => {
1365
+ if (abortedState.aborted || reply.raw.writableEnded) return;
1366
+ const data = finalData ?? {};
1367
+ const initialDataScript = `<script${cspNonce ? ` nonce="${cspNonce}"` : ""}>window.__INITIAL_DATA__ = ${JSON.stringify(data).replace(
1368
+ /</g,
1369
+ "\\u003c"
1370
+ )}; window.dispatchEvent(new Event('taujs:data-ready'));</script>`;
1371
+ reply.raw.write(initialDataScript);
1372
+ reply.raw.write(templateParts.afterBody);
1373
+ reply.raw.end();
1374
+ });
1375
+ }
1376
+ } catch (err) {
1377
+ if (err instanceof AppError) throw err;
1378
+ throw AppError.internal("handleRender failed", err, {
1379
+ url: req.url,
1380
+ route: req.routeOptions?.url
1381
+ });
1382
+ }
1383
+ };
1384
+
1385
+ // src/utils/HandleNotFound.ts
1386
+ var handleNotFound = async (req, reply, processedConfigs, maps, opts = {}) => {
1387
+ const logger = opts.logger ?? createLogger({
1388
+ debug: opts.debug,
1389
+ context: { component: "handle-not-found", url: req.url, method: req.method, traceId: req.id }
1390
+ });
1391
+ try {
1392
+ if (/\.\w+$/.test(req.raw.url ?? "")) {
1393
+ logger.debug?.("ssr", "Delegating asset-like request to Fastify notFound handler", { url: req.raw.url });
1394
+ return reply.callNotFound();
1395
+ }
1396
+ const defaultConfig = processedConfigs[0];
1397
+ if (!defaultConfig) {
1398
+ logger.error?.("No default configuration found", { configCount: processedConfigs.length, url: req.raw.url });
1399
+ throw AppError.internal("No default configuration found", {
1400
+ details: { configCount: processedConfigs.length, url: req.raw.url }
1401
+ });
1402
+ }
1403
+ const { clientRoot } = defaultConfig;
1404
+ const cspNonce = req.cspNonce ?? void 0;
1405
+ const template = ensureNonNull(maps.templates.get(clientRoot), `Template not found for clientRoot: ${clientRoot}`);
1406
+ const cssLink = maps.cssLinks.get(clientRoot);
1407
+ const bootstrapModule = maps.bootstrapModules.get(clientRoot);
1408
+ logger.debug?.("ssr", "Preparing not-found fallback HTML", {
1409
+ clientRoot,
1410
+ hasCssLink: Boolean(cssLink),
1411
+ hasBootstrapModule: Boolean(bootstrapModule),
1412
+ isDevelopment,
1413
+ hasCspNonce: Boolean(cspNonce)
1414
+ });
1415
+ let processedTemplate = template.replace(SSRTAG.ssrHead, "").replace(SSRTAG.ssrHtml, "");
1416
+ if (!isDevelopment && cssLink) {
1417
+ processedTemplate = processedTemplate.replace("</head>", `${cssLink}</head>`);
1418
+ }
1419
+ if (bootstrapModule) {
1420
+ const nonceAttr = cspNonce ? ` nonce="${cspNonce}"` : "";
1421
+ processedTemplate = processedTemplate.replace("</body>", `<script${nonceAttr} type="module" src="${bootstrapModule}" defer></script></body>`);
1422
+ }
1423
+ logger.debug?.("ssr", "Sending not-found fallback HTML", { status: 200 });
1424
+ return reply.status(200).type("text/html").send(processedTemplate);
1425
+ } catch (err) {
1426
+ logger.error?.("handleNotFound failed", { error: err, url: req.url, clientRoot: processedConfigs[0]?.clientRoot });
1427
+ throw AppError.internal("handleNotFound failed", err, {
1428
+ stage: "handleNotFound",
1429
+ url: req.url,
1430
+ clientRoot: processedConfigs[0]?.clientRoot
1431
+ });
1432
+ }
1433
+ };
1434
+
1435
+ // src/security/CSPReporting.ts
1436
+ var import_fastify_plugin2 = __toESM(require_plugin(), 1);
1437
+ function sanitiseContext(ctx) {
1438
+ return {
1439
+ userAgent: ctx.userAgent,
1440
+ ip: ctx.ip,
1441
+ referer: ctx.referer,
1442
+ timestamp: ctx.timestamp
1443
+ // headers: ctx.headers,
1444
+ };
1445
+ }
1446
+ function logCspViolation(logger, report, context) {
1447
+ logger.warn("CSP Violation", {
1448
+ violation: {
1449
+ documentUri: report["document-uri"],
1450
+ violatedDirective: report["violated-directive"],
1451
+ blockedUri: report["blocked-uri"],
1452
+ sourceFile: report["source-file"],
1453
+ line: report["line-number"],
1454
+ column: report["column-number"],
1455
+ scriptSample: report["script-sample"],
1456
+ originalPolicy: report["original-policy"],
1457
+ disposition: report.disposition
1458
+ },
1459
+ context: {
1460
+ userAgent: context.userAgent,
1461
+ ip: context.ip,
1462
+ referer: context.referer,
1463
+ timestamp: context.timestamp
1464
+ }
1465
+ });
1466
+ }
1467
+ var processCSPReport = (body, context, logger) => {
1468
+ try {
1469
+ const reportData = body?.["csp-report"] || body;
1470
+ if (!reportData || typeof reportData !== "object") {
1471
+ logger.warn("Ignoring malformed CSP report", {
1472
+ bodyType: typeof body,
1473
+ context: sanitiseContext(context)
1474
+ });
1475
+ return;
1476
+ }
1477
+ const documentUri = reportData["document-uri"] ?? reportData["documentURL"];
1478
+ const violatedDirective = reportData["violated-directive"] ?? reportData["violatedDirective"];
1479
+ if (!documentUri || !violatedDirective) {
1480
+ logger.warn("Ignoring incomplete CSP report", {
1481
+ hasDocumentUri: !!documentUri,
1482
+ hasViolatedDirective: !!violatedDirective,
1483
+ context: sanitiseContext(context)
1484
+ });
1485
+ return;
1486
+ }
1487
+ const violation = {
1488
+ "document-uri": String(documentUri),
1489
+ "violated-directive": String(violatedDirective),
1490
+ "blocked-uri": reportData["blocked-uri"] ?? reportData["blockedURL"] ?? "",
1491
+ "source-file": reportData["source-file"] ?? reportData["sourceFile"],
1492
+ "line-number": reportData["line-number"] ?? reportData["lineNumber"],
1493
+ "column-number": reportData["column-number"] ?? reportData["columnNumber"],
1494
+ "script-sample": reportData["script-sample"] ?? reportData["sample"],
1495
+ "original-policy": reportData["original-policy"] ?? reportData["originalPolicy"] ?? "",
1496
+ disposition: reportData.disposition ?? "enforce"
1497
+ };
1498
+ logCspViolation(logger, violation, context);
1499
+ } catch (processingError) {
1500
+ logger.warn("CSP report processing failed", {
1501
+ error: processingError instanceof Error ? processingError.message : String(processingError),
1502
+ bodyType: typeof body,
1503
+ context: sanitiseContext(context)
1504
+ });
1505
+ }
1506
+ };
1507
+ var cspReportPlugin = (0, import_fastify_plugin2.default)(
1508
+ async (fastify, opts) => {
1509
+ const { onViolation } = opts;
1510
+ if (!opts.path || typeof opts.path !== "string") throw AppError.badRequest("CSP report path is required and must be a string");
1511
+ const logger = createLogger({
1512
+ debug: opts.debug,
1513
+ context: { service: "csp-reporting" },
1514
+ minLevel: "info"
1515
+ });
1516
+ fastify.post(opts.path, async (req, reply) => {
1517
+ const context = {
1518
+ userAgent: req.headers["user-agent"],
1519
+ ip: req.ip,
1520
+ referer: req.headers.referer,
1521
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1522
+ headers: req.headers,
1523
+ __fastifyRequest: req
1524
+ // onViolation callback
1525
+ };
1526
+ try {
1527
+ processCSPReport(req.body, context, logger);
1528
+ const reportData = req.body?.["csp-report"] || req.body;
1529
+ if (onViolation && reportData && typeof reportData === "object") {
1530
+ const documentUri = reportData["document-uri"] ?? reportData["documentURL"];
1531
+ const violatedDirective = reportData["violated-directive"] ?? reportData["violatedDirective"];
1532
+ if (documentUri && violatedDirective) {
1533
+ const violation = {
1534
+ "document-uri": String(documentUri),
1535
+ "violated-directive": String(violatedDirective),
1536
+ "blocked-uri": reportData["blocked-uri"] ?? reportData["blockedURL"] ?? "",
1537
+ "source-file": reportData["source-file"] ?? reportData["sourceFile"],
1538
+ "line-number": reportData["line-number"] ?? reportData["lineNumber"],
1539
+ "column-number": reportData["column-number"] ?? reportData["columnNumber"],
1540
+ "script-sample": reportData["script-sample"] ?? reportData["sample"],
1541
+ "original-policy": reportData["original-policy"] ?? reportData["originalPolicy"] ?? "",
1542
+ disposition: reportData.disposition ?? "enforce"
1543
+ };
1544
+ onViolation(violation, req);
1545
+ }
535
1546
  }
536
- ],
537
- opts.isDebug
1547
+ } catch (err) {
1548
+ logger.warn("CSP reporting route failed", {
1549
+ error: err instanceof Error ? err.message : String(err)
1550
+ });
1551
+ }
1552
+ reply.code(204).send();
1553
+ });
1554
+ },
1555
+ { name: "taujs-csp-report-plugin" }
1556
+ );
1557
+
1558
+ // src/SSRServer.ts
1559
+ var SSRServer = (0, import_fastify_plugin3.default)(
1560
+ async (app, opts) => {
1561
+ const { alias, configs, routes, serviceRegistry, clientRoot: baseClientRoot, security } = opts;
1562
+ const logger = createLogger({
1563
+ debug: opts.debug,
1564
+ context: { component: "ssr-server" },
1565
+ minLevel: process.env.NODE_ENV === "production" ? "info" : "debug",
1566
+ includeContext: true,
1567
+ singleLine: true
1568
+ });
1569
+ const maps = createMaps();
1570
+ const processedConfigs = processConfigs(configs, baseClientRoot, TEMPLATE);
1571
+ const routeMatchers = createRouteMatchers(routes);
1572
+ let viteDevServer;
1573
+ await loadAssets(
1574
+ processedConfigs,
1575
+ baseClientRoot,
1576
+ maps.bootstrapModules,
1577
+ maps.cssLinks,
1578
+ maps.manifests,
1579
+ maps.preloadLinks,
1580
+ maps.renderModules,
1581
+ maps.ssrManifests,
1582
+ maps.templates,
1583
+ {
1584
+ debug: opts.debug,
1585
+ logger
1586
+ }
538
1587
  );
539
1588
  if (opts.registerStaticAssets && typeof opts.registerStaticAssets === "object") {
540
1589
  const { plugin, options } = opts.registerStaticAssets;
@@ -546,181 +1595,420 @@ var SSRServer = (0, import_fastify_plugin.default)(
546
1595
  ...options ?? {}
547
1596
  });
548
1597
  }
549
- app.addHook(
550
- "onRequest",
551
- createCSPHook({
552
- directives: opts.security?.csp?.directives,
553
- generateCSP: opts.security?.csp?.generateCSP
554
- })
555
- );
556
- app.addHook("onRequest", createAuthHook(routes));
557
- if (isDevelopment) {
558
- const { createServer } = await import("vite");
559
- viteDevServer = await createServer({
560
- appType: "custom",
561
- css: {
562
- preprocessorOptions: {
563
- scss: {
564
- api: "modern-compiler"
565
- }
566
- }
567
- },
568
- mode: "development",
569
- plugins: [
570
- ...isDebug ? [
571
- {
572
- name: "taujs-development-server-debug-logging",
573
- configureServer(server) {
574
- logger.log(import_picocolors.default.green("\u03C4js development server debug started."));
575
- server.middlewares.use((req, res, next) => {
576
- logger.log(import_picocolors.default.cyan(`\u2190 rx: ${req.url}`));
577
- res.on("finish", () => logger.log(import_picocolors.default.yellow(`\u2192 tx: ${req.url}`)));
578
- next();
579
- });
580
- }
581
- }
582
- ] : []
583
- ],
584
- resolve: {
585
- alias: {
586
- "@client": path2.resolve(baseClientRoot),
587
- "@server": path2.resolve(__dirname),
588
- "@shared": path2.resolve(__dirname, "../shared"),
589
- ...alias
590
- }
591
- },
592
- root: baseClientRoot,
593
- server: {
594
- middlewareMode: true,
595
- hmr: {
596
- port: 5174
597
- }
598
- }
599
- });
600
- overrideCSSHMRConsoleError();
601
- app.addHook("onRequest", async (request, reply) => {
602
- await new Promise((resolve) => {
603
- viteDevServer.middlewares(request.raw, reply.raw, () => {
604
- if (!reply.sent) resolve();
605
- });
606
- });
1598
+ if (security?.csp?.reporting) {
1599
+ app.register(cspReportPlugin, {
1600
+ path: security.csp.reporting.endpoint,
1601
+ debug: opts.debug,
1602
+ logger,
1603
+ onViolation: security.csp.reporting.onViolation
607
1604
  });
608
1605
  }
1606
+ app.register(cspPlugin, {
1607
+ directives: opts.security?.csp?.directives,
1608
+ generateCSP: opts.security?.csp?.generateCSP,
1609
+ routeMatchers,
1610
+ debug: opts.debug
1611
+ });
1612
+ if (isDevelopment) viteDevServer = await setupDevServer(app, baseClientRoot, alias, opts.debug, opts.devNet);
1613
+ app.addHook("onRequest", createAuthHook(routeMatchers, logger));
609
1614
  app.get("/*", async (req, reply) => {
610
- try {
611
- if (/\.\w+$/.test(req.raw.url ?? "")) return reply.callNotFound();
612
- const url = req.url ? new URL(req.url, `http://${req.headers.host}`).pathname : "/";
613
- const matchedRoute = matchRoute(url, routes);
614
- const nonce = applyCSP(opts.security, reply);
615
- if (!matchedRoute) {
616
- reply.callNotFound();
617
- return;
618
- }
619
- const { route, params } = matchedRoute;
620
- const { attr, appId } = route;
621
- const config = processedConfigs.find((config2) => config2.appId === appId) || processedConfigs[0];
622
- if (!config) throw new Error("No configuration found for the request.");
623
- const { clientRoot, entryServer } = config;
624
- let template = ensureNonNull(templates.get(clientRoot), `Template not found for clientRoot: ${clientRoot}`);
625
- const bootstrapModule = bootstrapModules.get(clientRoot);
626
- const cssLink = cssLinks.get(clientRoot);
627
- const manifest = manifests.get(clientRoot);
628
- const preloadLink = preloadLinks.get(clientRoot);
629
- const ssrManifest = ssrManifests.get(clientRoot);
630
- let renderModule;
631
- if (isDevelopment) {
632
- template = template.replace(/<script type="module" src="\/@vite\/client"><\/script>/g, "");
633
- template = template.replace(/<style type="text\/css">[\s\S]*?<\/style>/g, "");
634
- const entryServerPath = path2.join(clientRoot, `${entryServer}.tsx`);
635
- const executedModule = await viteDevServer.ssrLoadModule(entryServerPath);
636
- renderModule = executedModule;
637
- const styles = await collectStyle(viteDevServer, [entryServerPath]);
638
- template = template?.replace("</head>", `<style type="text/css">${styles}</style></head>`);
639
- template = await viteDevServer.transformIndexHtml(url, template);
640
- } else {
641
- renderModule = renderModules.get(clientRoot);
642
- if (!renderModule) {
643
- const renderModulePath = path2.join(clientRoot, `${entryServer}.js`);
644
- const importedModule = await import(renderModulePath);
645
- renderModule = importedModule;
646
- renderModules.set(clientRoot, renderModule);
647
- }
648
- }
649
- const renderType = attr?.render || RENDERTYPE.ssr;
650
- const [beforeBody = "", afterBody = ""] = template.split(SSRTAG.ssrHtml);
651
- const [beforeHead = "", afterHead = ""] = beforeBody.split(SSRTAG.ssrHead);
652
- const initialDataPromise = fetchInitialData(attr, params, serviceRegistry);
653
- if (renderType === RENDERTYPE.ssr) {
654
- const { renderSSR } = renderModule;
655
- const initialDataResolved = await initialDataPromise;
656
- const initialDataScript = `<script nonce="${nonce}">window.__INITIAL_DATA__ = ${JSON.stringify(initialDataResolved).replace(/</g, "\\u003c")}</script>`;
657
- const { headContent, appHtml } = await renderSSR(initialDataResolved, req.url, attr?.meta);
658
- let aggregateHeadContent = headContent;
659
- if (ssrManifest && preloadLink) aggregateHeadContent += preloadLink;
660
- if (manifest && cssLink) aggregateHeadContent += cssLink;
661
- const shouldHydrate = attr?.hydrate !== false;
662
- const bootstrapScriptTag = shouldHydrate ? `<script nonce="${nonce}" type="module" src="${bootstrapModule}" defer></script>` : "";
663
- const fullHtml = template.replace(SSRTAG.ssrHead, aggregateHeadContent).replace(SSRTAG.ssrHtml, `${appHtml}${initialDataScript}${bootstrapScriptTag}`);
664
- return reply.status(200).header("Content-Type", "text/html").send(fullHtml);
665
- } else {
666
- const { renderStream } = renderModule;
667
- reply.raw.writeHead(200, { "Content-Type": "text/html" });
668
- renderStream(
669
- reply.raw,
670
- {
671
- onHead: (headContent) => {
672
- let aggregateHeadContent = headContent;
673
- if (ssrManifest && preloadLink) aggregateHeadContent += preloadLink;
674
- if (manifest && cssLink) aggregateHeadContent += cssLink;
675
- reply.raw.write(`${beforeHead}${aggregateHeadContent}${afterHead}`);
676
- },
677
- onFinish: async (initialDataResolved) => {
678
- reply.raw.write(`<script nonce="${nonce}">window.__INITIAL_DATA__ = ${JSON.stringify(initialDataResolved).replace(/</g, "\\u003c")}</script>`);
679
- reply.raw.write(afterBody);
680
- reply.raw.end();
681
- },
682
- onError: (error) => {
683
- console.error("Critical rendering onError:", error);
684
- reply.raw.end("Internal Server Error");
685
- }
686
- },
687
- initialDataPromise,
688
- req.url,
689
- bootstrapModule,
690
- attr?.meta
691
- );
692
- }
693
- } catch (error) {
694
- console.error("Error setting up SSR stream:", error);
695
- if (!reply.raw.headersSent) reply.raw.writeHead(500, { "Content-Type": "text/plain" });
696
- reply.raw.end("Internal Server Error");
697
- }
1615
+ await handleRender(req, reply, routeMatchers, processedConfigs, serviceRegistry, maps, {
1616
+ debug: opts.debug,
1617
+ logger,
1618
+ viteDevServer
1619
+ });
698
1620
  });
699
1621
  app.setNotFoundHandler(async (req, reply) => {
700
- if (/\.\w+$/.test(req.raw.url ?? "")) return reply.callNotFound();
701
- try {
702
- const defaultConfig = processedConfigs[0];
703
- if (!defaultConfig) throw new Error("No default configuration found.");
704
- const { clientRoot } = defaultConfig;
705
- const nonce = applyCSP(opts.security, reply);
706
- let template = ensureNonNull(templates.get(clientRoot), `Template not found for clientRoot: ${clientRoot}`);
707
- const cssLink = cssLinks.get(clientRoot);
708
- const bootstrapModule = bootstrapModules.get(clientRoot);
709
- template = template.replace(SSRTAG.ssrHead, "").replace(SSRTAG.ssrHtml, "");
710
- if (!isDevelopment && cssLink) template = template.replace("</head>", `${cssLink}</head>`);
711
- if (bootstrapModule) template = template.replace("</body>", `<script nonce="${nonce}" type="module" src="${bootstrapModule}" defer></script></body>`);
712
- reply.status(200).type("text/html").send(template);
713
- } catch (error) {
714
- console.error("Failed to serve clientHtmlTemplate:", error);
715
- reply.status(500).send("Internal Server Error");
1622
+ await handleNotFound(
1623
+ req,
1624
+ reply,
1625
+ processedConfigs,
1626
+ {
1627
+ cssLinks: maps.cssLinks,
1628
+ bootstrapModules: maps.bootstrapModules,
1629
+ templates: maps.templates
1630
+ },
1631
+ {
1632
+ debug: opts.debug,
1633
+ logger
1634
+ }
1635
+ );
1636
+ });
1637
+ app.setErrorHandler((err, req, reply) => {
1638
+ const e = AppError.from(err);
1639
+ logger.error(e.message, {
1640
+ kind: e.kind,
1641
+ httpStatus: e.httpStatus,
1642
+ ...e.code && { code: e.code },
1643
+ details: e.details,
1644
+ method: req.method,
1645
+ url: req.url,
1646
+ route: req.routeOptions?.url,
1647
+ stack: e.stack
1648
+ });
1649
+ if (!reply.raw.headersSent) {
1650
+ const { status, body } = toHttp(e);
1651
+ reply.status(status).send(body);
1652
+ } else {
1653
+ reply.raw.end();
716
1654
  }
717
1655
  });
718
1656
  },
719
- { name: "taujs-ssr-server" }
1657
+ { name: "\u03C4js-ssr-server" }
720
1658
  );
1659
+
1660
+ // src/CreateServer.ts
1661
+ var import_picocolors4 = __toESM(require_picocolors(), 1);
1662
+ import path5 from "path";
1663
+ import { performance as performance2 } from "perf_hooks";
1664
+ import fastifyStatic from "@fastify/static";
1665
+ import Fastify from "fastify";
1666
+
1667
+ // src/Setup.ts
1668
+ import { performance } from "perf_hooks";
1669
+ var extractBuildConfigs = (config) => {
1670
+ return config.apps.map(({ appId, entryPoint, plugins }) => ({
1671
+ appId,
1672
+ entryPoint,
1673
+ plugins
1674
+ }));
1675
+ };
1676
+ var extractRoutes = (taujsConfig) => {
1677
+ const t0 = performance.now();
1678
+ const allRoutes = [];
1679
+ const apps = [];
1680
+ const warnings = [];
1681
+ const pathTracker = /* @__PURE__ */ new Map();
1682
+ for (const app of taujsConfig.apps) {
1683
+ const appRoutes = (app.routes ?? []).map((route) => {
1684
+ const fullRoute = { ...route, appId: app.appId };
1685
+ if (!pathTracker.has(route.path)) pathTracker.set(route.path, []);
1686
+ pathTracker.get(route.path).push(app.appId);
1687
+ return fullRoute;
1688
+ });
1689
+ apps.push({ appId: app.appId, routeCount: appRoutes.length });
1690
+ allRoutes.push(...appRoutes);
1691
+ }
1692
+ for (const [path6, appIds] of pathTracker.entries()) {
1693
+ if (appIds.length > 1) warnings.push(`Route path "${path6}" is declared in multiple apps: ${appIds.join(", ")}`);
1694
+ }
1695
+ const sortedRoutes = allRoutes.sort((a, b) => computeScore(b.path) - computeScore(a.path));
1696
+ const durationMs = performance.now() - t0;
1697
+ return {
1698
+ routes: sortedRoutes,
1699
+ apps,
1700
+ totalRoutes: allRoutes.length,
1701
+ durationMs,
1702
+ warnings
1703
+ };
1704
+ };
1705
+ var extractSecurity = (taujsConfig) => {
1706
+ const t0 = performance.now();
1707
+ const user = taujsConfig.security ?? {};
1708
+ const userCsp = user.csp;
1709
+ const hasExplicitCSP = !!userCsp;
1710
+ const normalisedCsp = userCsp ? {
1711
+ defaultMode: userCsp.defaultMode ?? "merge",
1712
+ directives: userCsp.directives,
1713
+ generateCSP: userCsp.generateCSP,
1714
+ reporting: userCsp.reporting ? {
1715
+ endpoint: userCsp.reporting.endpoint,
1716
+ onViolation: userCsp.reporting.onViolation,
1717
+ reportOnly: userCsp.reporting.reportOnly ?? false
1718
+ } : void 0
1719
+ } : void 0;
1720
+ const security = { csp: normalisedCsp };
1721
+ const summary = {
1722
+ mode: hasExplicitCSP ? "explicit" : "dev-defaults",
1723
+ defaultMode: normalisedCsp?.defaultMode ?? "merge",
1724
+ hasReporting: !!normalisedCsp?.reporting?.endpoint,
1725
+ reportOnly: !!normalisedCsp?.reporting?.reportOnly
1726
+ };
1727
+ const durationMs = performance.now() - t0;
1728
+ return {
1729
+ security,
1730
+ durationMs,
1731
+ hasExplicitCSP,
1732
+ summary
1733
+ };
1734
+ };
1735
+ function printConfigSummary(logger, apps, configsCount, totalRoutes, durationMs, warnings) {
1736
+ logger.info(`${CONTENT.TAG} [config] Loaded ${configsCount} app(s), ${totalRoutes} route(s) in ${durationMs.toFixed(1)}ms`);
1737
+ apps.forEach((a) => logger.debug("routes", `\u2022 ${a.appId}: ${a.routeCount} route(s)`));
1738
+ warnings.forEach((w) => logger.warn(`${CONTENT.TAG} [warn] ${w}`));
1739
+ }
1740
+ function printSecuritySummary(logger, routes, security, hasExplicitCSP, securityDurationMs) {
1741
+ const total = routes.length;
1742
+ const disabled = routes.filter((r) => r.attr?.middleware?.csp === false).length;
1743
+ const custom = routes.filter((r) => {
1744
+ const v = r.attr?.middleware?.csp;
1745
+ return v !== void 0 && v !== false;
1746
+ }).length;
1747
+ const enabled = total - disabled;
1748
+ const hasReporting = !!security.csp?.reporting?.endpoint;
1749
+ const mode = security.csp?.defaultMode ?? "merge";
1750
+ let status = "configured";
1751
+ let detail = "";
1752
+ if (hasExplicitCSP) {
1753
+ detail = `explicit, mode=${mode}`;
1754
+ if (hasReporting) detail += ", reporting";
1755
+ if (custom > 0) detail += `, ${custom} route override(s)`;
1756
+ } else {
1757
+ if (process.env.NODE_ENV === "production") {
1758
+ logger.warn("(consider explicit config for production)");
1759
+ }
1760
+ }
1761
+ logger.info(`${CONTENT.TAG} [security] CSP ${status} (${enabled}/${total} routes) in ${securityDurationMs.toFixed(1)}ms`);
1762
+ }
1763
+ function printContractReport(logger, report) {
1764
+ for (const r of report.items) {
1765
+ const line = `${CONTENT.TAG} [security][${r.key}] ${r.message}`;
1766
+ if (r.status === "error") {
1767
+ logger.error(line);
1768
+ } else if (r.status === "warning") {
1769
+ logger.warn(line);
1770
+ } else if (r.status === "skipped") {
1771
+ logger.debug(r.key, line);
1772
+ } else {
1773
+ logger.info(line);
1774
+ }
1775
+ }
1776
+ }
1777
+ var computeScore = (path6) => {
1778
+ return path6.split("/").filter(Boolean).reduce((score, segment) => score + (segment.startsWith(":") ? 1 : 10), 0);
1779
+ };
1780
+
1781
+ // src/network/Network.ts
1782
+ var import_picocolors3 = __toESM(require_picocolors(), 1);
1783
+ import { networkInterfaces } from "os";
1784
+ var isPrivateIPv4 = (addr) => {
1785
+ if (!/^\d+\.\d+\.\d+\.\d+$/.test(addr)) return false;
1786
+ const [a, b, _c, _d] = addr.split(".").map(Number);
1787
+ if (a === 10) return true;
1788
+ if (a === 192 && b === 168) return true;
1789
+ if (a === 172 && b >= 16 && b <= 31) return true;
1790
+ return false;
1791
+ };
1792
+ var bannerPlugin = async (fastify, options) => {
1793
+ const logger = createLogger({ debug: options.debug });
1794
+ const dbgNetwork = logger.isDebugEnabled("network");
1795
+ fastify.decorate("showBanner", function showBanner() {
1796
+ const addr = this.server.address();
1797
+ if (!addr || typeof addr === "string") return;
1798
+ const { address, port } = addr;
1799
+ const boundHost = address === "::1" ? "localhost" : address === "::" ? "::" : address === "0.0.0.0" ? "0.0.0.0" : address;
1800
+ console.log(`\u2503 Local ${import_picocolors3.default.bold(`http://localhost:${port}/`)}`);
1801
+ if (boundHost === "localhost" || boundHost === "127.0.0.1") {
1802
+ console.log("\u2503 Network use --host to expose\n");
1803
+ return;
1804
+ }
1805
+ const nets = networkInterfaces();
1806
+ let networkAddress = null;
1807
+ for (const ifaces of Object.values(nets)) {
1808
+ if (!ifaces) continue;
1809
+ for (const iface of ifaces) {
1810
+ if (iface.internal || iface.family !== "IPv4") continue;
1811
+ if (isPrivateIPv4(iface.address)) {
1812
+ networkAddress = iface.address;
1813
+ break;
1814
+ }
1815
+ if (!networkAddress) networkAddress = iface.address;
1816
+ }
1817
+ if (networkAddress && isPrivateIPv4(networkAddress)) break;
1818
+ }
1819
+ if (networkAddress) {
1820
+ console.log(`\u2503 Network http://${networkAddress}:${port}/
1821
+ `);
1822
+ if (dbgNetwork) logger.warn(import_picocolors3.default.yellow(`${CONTENT.TAG} [network] Dev server exposed on network - for local testing only.`));
1823
+ }
1824
+ logger.info(import_picocolors3.default.green(`${CONTENT.TAG} [network] Bound to host: ${boundHost}`));
1825
+ });
1826
+ fastify.addHook("onReady", async function() {
1827
+ if (this.server.listening) {
1828
+ this.showBanner();
1829
+ return;
1830
+ }
1831
+ this.server.once("listening", () => this.showBanner());
1832
+ });
1833
+ };
1834
+
1835
+ // src/network/CLI.ts
1836
+ function readFlag(argv, keys, bareValue) {
1837
+ const end = argv.indexOf("--");
1838
+ const limit = end === -1 ? argv.length : end;
1839
+ for (let i = 0; i < limit; i++) {
1840
+ const arg = argv[i];
1841
+ for (const key of keys) {
1842
+ if (arg === key) {
1843
+ const next = argv[i + 1];
1844
+ if (!next || next.startsWith("-")) return bareValue;
1845
+ return next.trim();
1846
+ }
1847
+ const pref = `${key}=`;
1848
+ if (arg && arg.startsWith(pref)) {
1849
+ const v = arg.slice(pref.length).trim();
1850
+ return v || bareValue;
1851
+ }
1852
+ }
1853
+ }
1854
+ return void 0;
1855
+ }
1856
+ function resolveNet(input) {
1857
+ const env = process.env;
1858
+ const argv = process.argv;
1859
+ let host = "localhost";
1860
+ let port = 5173;
1861
+ let hmrPort = 5174;
1862
+ if (input?.host) host = input.host;
1863
+ if (Number.isFinite(input?.port)) port = Number(input.port);
1864
+ if (Number.isFinite(input?.hmrPort)) hmrPort = Number(input.hmrPort);
1865
+ if (env.HOST?.trim()) host = env.HOST.trim();
1866
+ else if (env.FASTIFY_ADDRESS?.trim()) host = env.FASTIFY_ADDRESS.trim();
1867
+ if (env.PORT) port = Number(env.PORT) || port;
1868
+ if (env.FASTIFY_PORT) port = Number(env.FASTIFY_PORT) || port;
1869
+ if (env.HMR_PORT) hmrPort = Number(env.HMR_PORT) || hmrPort;
1870
+ const cliHost = readFlag(argv, ["--host", "--hostname", "-H"], "0.0.0.0");
1871
+ const cliPort = readFlag(argv, ["--port", "-p"]);
1872
+ const cliHMR = readFlag(argv, ["--hmr-port"]);
1873
+ if (cliHost) host = cliHost;
1874
+ if (cliPort) port = Number(cliPort) || port;
1875
+ if (cliHMR) hmrPort = Number(cliHMR) || hmrPort;
1876
+ if (host === "true" || host === "") host = "0.0.0.0";
1877
+ return { host, port, hmrPort };
1878
+ }
1879
+
1880
+ // src/security/VerifyMiddleware.ts
1881
+ var isAuthRequired = (route) => Boolean(route.attr?.middleware?.auth);
1882
+ var hasAuthenticate = (app) => typeof app.authenticate === "function";
1883
+ function formatCspLoadedMsg(hasGlobal, custom) {
1884
+ if (hasGlobal) {
1885
+ return custom > 0 ? `Loaded global config with ${custom} route override(s)` : "Loaded global config";
1886
+ }
1887
+ return custom > 0 ? `Loaded development defaults with ${custom} route override(s)` : "Loaded development defaults";
1888
+ }
1889
+ var verifyContracts = (app, routes, contracts, security) => {
1890
+ const items = [];
1891
+ for (const contract of contracts) {
1892
+ const isRequired = contract.required(routes, security);
1893
+ if (!isRequired) {
1894
+ items.push({
1895
+ key: contract.key,
1896
+ status: "skipped",
1897
+ message: `No routes require "${contract.key}"`
1898
+ });
1899
+ continue;
1900
+ }
1901
+ if (!contract.verify(app)) {
1902
+ const msg = `[\u03C4js] ${contract.errorMessage}`;
1903
+ items.push({ key: contract.key, status: "error", message: msg });
1904
+ throw new Error(msg);
1905
+ }
1906
+ if (contract.key === "csp") {
1907
+ const total = routes.length;
1908
+ const disabled = routes.filter((r) => r.attr?.middleware?.csp === false).length;
1909
+ const custom = routes.filter((r) => {
1910
+ const v = r.attr?.middleware?.csp;
1911
+ return v !== void 0 && v !== false;
1912
+ }).length;
1913
+ const enabled = total - disabled;
1914
+ const hasGlobal = !!security?.csp;
1915
+ let status = "verified";
1916
+ let tail = "";
1917
+ if (!hasGlobal && process.env.NODE_ENV === "production") {
1918
+ status = "warning";
1919
+ tail = " (consider adding global CSP for production)";
1920
+ }
1921
+ const baseMsg = formatCspLoadedMsg(hasGlobal, custom);
1922
+ items.push({
1923
+ key: "csp",
1924
+ status,
1925
+ message: baseMsg + tail
1926
+ });
1927
+ items.push({
1928
+ key: "csp",
1929
+ status,
1930
+ message: `\u2713 Verified (${enabled} enabled, ${disabled} disabled, ${total} total). ` + tail
1931
+ });
1932
+ } else {
1933
+ const count = routes.filter((r) => contract.required([r], security)).length;
1934
+ items.push({
1935
+ key: contract.key,
1936
+ status: "verified",
1937
+ message: `\u2713 ${count} route(s)`
1938
+ });
1939
+ }
1940
+ }
1941
+ return { items };
1942
+ };
1943
+
1944
+ // src/CreateServer.ts
1945
+ var createServer = async (opts) => {
1946
+ const t0 = performance2.now();
1947
+ const clientRoot = opts.clientRoot ?? path5.resolve(process.cwd(), "client");
1948
+ const app = opts.fastify ?? Fastify({ logger: false });
1949
+ const net = resolveNet(opts.config.server);
1950
+ await app.register(bannerPlugin, {
1951
+ debug: opts.debug,
1952
+ hmr: { host: net.host, port: net.hmrPort }
1953
+ });
1954
+ const logger = createLogger({
1955
+ debug: opts.debug,
1956
+ custom: opts.logger,
1957
+ minLevel: process.env.NODE_ENV === "production" ? "info" : "debug",
1958
+ includeContext: true
1959
+ });
1960
+ const configs = extractBuildConfigs(opts.config);
1961
+ const { routes, apps, totalRoutes, durationMs, warnings } = extractRoutes(opts.config);
1962
+ const { security, durationMs: securityDuration, hasExplicitCSP } = extractSecurity(opts.config);
1963
+ printConfigSummary(logger, apps, configs.length, totalRoutes, durationMs, warnings);
1964
+ printSecuritySummary(logger, routes, security, hasExplicitCSP, securityDuration);
1965
+ const report = verifyContracts(
1966
+ app,
1967
+ routes,
1968
+ [
1969
+ {
1970
+ key: "auth",
1971
+ required: (rts) => rts.some(isAuthRequired),
1972
+ verify: hasAuthenticate,
1973
+ errorMessage: "Routes require auth but Fastify is missing .authenticate decorator."
1974
+ },
1975
+ {
1976
+ key: "csp",
1977
+ required: () => true,
1978
+ verify: () => true,
1979
+ errorMessage: "CSP plugin failed to register."
1980
+ }
1981
+ ],
1982
+ security
1983
+ );
1984
+ printContractReport(logger, report);
1985
+ try {
1986
+ await app.register(SSRServer, {
1987
+ clientRoot,
1988
+ configs,
1989
+ routes,
1990
+ serviceRegistry: opts.serviceRegistry,
1991
+ registerStaticAssets: opts.registerStaticAssets !== void 0 ? opts.registerStaticAssets : { plugin: fastifyStatic },
1992
+ debug: opts.debug,
1993
+ alias: opts.alias,
1994
+ security,
1995
+ devNet: { host: net.host, hmrPort: net.hmrPort }
1996
+ });
1997
+ } catch (err) {
1998
+ logger.error("Failed to register SSRServer", {
1999
+ step: "register:SSRServer",
2000
+ error: normaliseError(err)
2001
+ });
2002
+ }
2003
+ const t1 = performance2.now();
2004
+ console.log(`
2005
+ ${import_picocolors4.default.bgGreen(import_picocolors4.default.black(` ${CONTENT.TAG} `))} configured in ${(t1 - t0).toFixed(0)}ms
2006
+ `);
2007
+ if (opts.fastify) return { net };
2008
+ return { app, net };
2009
+ };
721
2010
  export {
722
2011
  SSRServer,
723
2012
  TEMPLATE,
724
- createMaps,
725
- processConfigs
2013
+ createServer
726
2014
  };