@taujs/server 0.4.0 → 0.4.1
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/{Config-CjwAJCfZ.d.ts → Config-LCDjtT9m.d.ts} +12 -82
- package/dist/Config.d.ts +1 -1
- package/dist/index.d.ts +28 -48
- package/dist/index.js +1706 -1540
- package/package.json +2 -2
- package/dist/Build.d.ts +0 -22
- package/dist/Build.js +0 -228
- package/dist/types.d.ts +0 -3
- package/dist/types.js +0 -0
package/dist/index.js
CHANGED
|
@@ -24,6 +24,79 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
mod
|
|
25
25
|
));
|
|
26
26
|
|
|
27
|
+
// node_modules/picocolors/picocolors.js
|
|
28
|
+
var require_picocolors = __commonJS({
|
|
29
|
+
"node_modules/picocolors/picocolors.js"(exports, module) {
|
|
30
|
+
"use strict";
|
|
31
|
+
var p = process || {};
|
|
32
|
+
var argv = p.argv || [];
|
|
33
|
+
var env = p.env || {};
|
|
34
|
+
var isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI);
|
|
35
|
+
var formatter = (open, close, replace = open) => (input) => {
|
|
36
|
+
let string = "" + input, index = string.indexOf(close, open.length);
|
|
37
|
+
return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close;
|
|
38
|
+
};
|
|
39
|
+
var replaceClose = (string, close, replace, index) => {
|
|
40
|
+
let result = "", cursor = 0;
|
|
41
|
+
do {
|
|
42
|
+
result += string.substring(cursor, index) + replace;
|
|
43
|
+
cursor = index + close.length;
|
|
44
|
+
index = string.indexOf(close, cursor);
|
|
45
|
+
} while (~index);
|
|
46
|
+
return result + string.substring(cursor);
|
|
47
|
+
};
|
|
48
|
+
var createColors = (enabled = isColorSupported) => {
|
|
49
|
+
let f = enabled ? formatter : () => String;
|
|
50
|
+
return {
|
|
51
|
+
isColorSupported: enabled,
|
|
52
|
+
reset: f("\x1B[0m", "\x1B[0m"),
|
|
53
|
+
bold: f("\x1B[1m", "\x1B[22m", "\x1B[22m\x1B[1m"),
|
|
54
|
+
dim: f("\x1B[2m", "\x1B[22m", "\x1B[22m\x1B[2m"),
|
|
55
|
+
italic: f("\x1B[3m", "\x1B[23m"),
|
|
56
|
+
underline: f("\x1B[4m", "\x1B[24m"),
|
|
57
|
+
inverse: f("\x1B[7m", "\x1B[27m"),
|
|
58
|
+
hidden: f("\x1B[8m", "\x1B[28m"),
|
|
59
|
+
strikethrough: f("\x1B[9m", "\x1B[29m"),
|
|
60
|
+
black: f("\x1B[30m", "\x1B[39m"),
|
|
61
|
+
red: f("\x1B[31m", "\x1B[39m"),
|
|
62
|
+
green: f("\x1B[32m", "\x1B[39m"),
|
|
63
|
+
yellow: f("\x1B[33m", "\x1B[39m"),
|
|
64
|
+
blue: f("\x1B[34m", "\x1B[39m"),
|
|
65
|
+
magenta: f("\x1B[35m", "\x1B[39m"),
|
|
66
|
+
cyan: f("\x1B[36m", "\x1B[39m"),
|
|
67
|
+
white: f("\x1B[37m", "\x1B[39m"),
|
|
68
|
+
gray: f("\x1B[90m", "\x1B[39m"),
|
|
69
|
+
bgBlack: f("\x1B[40m", "\x1B[49m"),
|
|
70
|
+
bgRed: f("\x1B[41m", "\x1B[49m"),
|
|
71
|
+
bgGreen: f("\x1B[42m", "\x1B[49m"),
|
|
72
|
+
bgYellow: f("\x1B[43m", "\x1B[49m"),
|
|
73
|
+
bgBlue: f("\x1B[44m", "\x1B[49m"),
|
|
74
|
+
bgMagenta: f("\x1B[45m", "\x1B[49m"),
|
|
75
|
+
bgCyan: f("\x1B[46m", "\x1B[49m"),
|
|
76
|
+
bgWhite: f("\x1B[47m", "\x1B[49m"),
|
|
77
|
+
blackBright: f("\x1B[90m", "\x1B[39m"),
|
|
78
|
+
redBright: f("\x1B[91m", "\x1B[39m"),
|
|
79
|
+
greenBright: f("\x1B[92m", "\x1B[39m"),
|
|
80
|
+
yellowBright: f("\x1B[93m", "\x1B[39m"),
|
|
81
|
+
blueBright: f("\x1B[94m", "\x1B[39m"),
|
|
82
|
+
magentaBright: f("\x1B[95m", "\x1B[39m"),
|
|
83
|
+
cyanBright: f("\x1B[96m", "\x1B[39m"),
|
|
84
|
+
whiteBright: f("\x1B[97m", "\x1B[39m"),
|
|
85
|
+
bgBlackBright: f("\x1B[100m", "\x1B[49m"),
|
|
86
|
+
bgRedBright: f("\x1B[101m", "\x1B[49m"),
|
|
87
|
+
bgGreenBright: f("\x1B[102m", "\x1B[49m"),
|
|
88
|
+
bgYellowBright: f("\x1B[103m", "\x1B[49m"),
|
|
89
|
+
bgBlueBright: f("\x1B[104m", "\x1B[49m"),
|
|
90
|
+
bgMagentaBright: f("\x1B[105m", "\x1B[49m"),
|
|
91
|
+
bgCyanBright: f("\x1B[106m", "\x1B[49m"),
|
|
92
|
+
bgWhiteBright: f("\x1B[107m", "\x1B[49m")
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
module.exports = createColors();
|
|
96
|
+
module.exports.createColors = createColors;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
27
100
|
// node_modules/fastify-plugin/lib/getPluginName.js
|
|
28
101
|
var require_getPluginName = __commonJS({
|
|
29
102
|
"node_modules/fastify-plugin/lib/getPluginName.js"(exports, module) {
|
|
@@ -114,84 +187,15 @@ var require_plugin = __commonJS({
|
|
|
114
187
|
}
|
|
115
188
|
});
|
|
116
189
|
|
|
117
|
-
//
|
|
118
|
-
var
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
var env = p.env || {};
|
|
124
|
-
var isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI);
|
|
125
|
-
var formatter = (open, close, replace = open) => (input) => {
|
|
126
|
-
let string = "" + input, index = string.indexOf(close, open.length);
|
|
127
|
-
return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close;
|
|
128
|
-
};
|
|
129
|
-
var replaceClose = (string, close, replace, index) => {
|
|
130
|
-
let result = "", cursor = 0;
|
|
131
|
-
do {
|
|
132
|
-
result += string.substring(cursor, index) + replace;
|
|
133
|
-
cursor = index + close.length;
|
|
134
|
-
index = string.indexOf(close, cursor);
|
|
135
|
-
} while (~index);
|
|
136
|
-
return result + string.substring(cursor);
|
|
137
|
-
};
|
|
138
|
-
var createColors = (enabled = isColorSupported) => {
|
|
139
|
-
let f = enabled ? formatter : () => String;
|
|
140
|
-
return {
|
|
141
|
-
isColorSupported: enabled,
|
|
142
|
-
reset: f("\x1B[0m", "\x1B[0m"),
|
|
143
|
-
bold: f("\x1B[1m", "\x1B[22m", "\x1B[22m\x1B[1m"),
|
|
144
|
-
dim: f("\x1B[2m", "\x1B[22m", "\x1B[22m\x1B[2m"),
|
|
145
|
-
italic: f("\x1B[3m", "\x1B[23m"),
|
|
146
|
-
underline: f("\x1B[4m", "\x1B[24m"),
|
|
147
|
-
inverse: f("\x1B[7m", "\x1B[27m"),
|
|
148
|
-
hidden: f("\x1B[8m", "\x1B[28m"),
|
|
149
|
-
strikethrough: f("\x1B[9m", "\x1B[29m"),
|
|
150
|
-
black: f("\x1B[30m", "\x1B[39m"),
|
|
151
|
-
red: f("\x1B[31m", "\x1B[39m"),
|
|
152
|
-
green: f("\x1B[32m", "\x1B[39m"),
|
|
153
|
-
yellow: f("\x1B[33m", "\x1B[39m"),
|
|
154
|
-
blue: f("\x1B[34m", "\x1B[39m"),
|
|
155
|
-
magenta: f("\x1B[35m", "\x1B[39m"),
|
|
156
|
-
cyan: f("\x1B[36m", "\x1B[39m"),
|
|
157
|
-
white: f("\x1B[37m", "\x1B[39m"),
|
|
158
|
-
gray: f("\x1B[90m", "\x1B[39m"),
|
|
159
|
-
bgBlack: f("\x1B[40m", "\x1B[49m"),
|
|
160
|
-
bgRed: f("\x1B[41m", "\x1B[49m"),
|
|
161
|
-
bgGreen: f("\x1B[42m", "\x1B[49m"),
|
|
162
|
-
bgYellow: f("\x1B[43m", "\x1B[49m"),
|
|
163
|
-
bgBlue: f("\x1B[44m", "\x1B[49m"),
|
|
164
|
-
bgMagenta: f("\x1B[45m", "\x1B[49m"),
|
|
165
|
-
bgCyan: f("\x1B[46m", "\x1B[49m"),
|
|
166
|
-
bgWhite: f("\x1B[47m", "\x1B[49m"),
|
|
167
|
-
blackBright: f("\x1B[90m", "\x1B[39m"),
|
|
168
|
-
redBright: f("\x1B[91m", "\x1B[39m"),
|
|
169
|
-
greenBright: f("\x1B[92m", "\x1B[39m"),
|
|
170
|
-
yellowBright: f("\x1B[93m", "\x1B[39m"),
|
|
171
|
-
blueBright: f("\x1B[94m", "\x1B[39m"),
|
|
172
|
-
magentaBright: f("\x1B[95m", "\x1B[39m"),
|
|
173
|
-
cyanBright: f("\x1B[96m", "\x1B[39m"),
|
|
174
|
-
whiteBright: f("\x1B[97m", "\x1B[39m"),
|
|
175
|
-
bgBlackBright: f("\x1B[100m", "\x1B[49m"),
|
|
176
|
-
bgRedBright: f("\x1B[101m", "\x1B[49m"),
|
|
177
|
-
bgGreenBright: f("\x1B[102m", "\x1B[49m"),
|
|
178
|
-
bgYellowBright: f("\x1B[103m", "\x1B[49m"),
|
|
179
|
-
bgBlueBright: f("\x1B[104m", "\x1B[49m"),
|
|
180
|
-
bgMagentaBright: f("\x1B[105m", "\x1B[49m"),
|
|
181
|
-
bgCyanBright: f("\x1B[106m", "\x1B[49m"),
|
|
182
|
-
bgWhiteBright: f("\x1B[107m", "\x1B[49m")
|
|
183
|
-
};
|
|
184
|
-
};
|
|
185
|
-
module.exports = createColors();
|
|
186
|
-
module.exports.createColors = createColors;
|
|
187
|
-
}
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
// src/fastify.d.ts
|
|
191
|
-
import "fastify";
|
|
190
|
+
// src/CreateServer.ts
|
|
191
|
+
var import_picocolors4 = __toESM(require_picocolors(), 1);
|
|
192
|
+
import path5 from "path";
|
|
193
|
+
import { performance as performance2 } from "perf_hooks";
|
|
194
|
+
import fastifyStatic from "@fastify/static";
|
|
195
|
+
import Fastify from "fastify";
|
|
192
196
|
|
|
193
|
-
// src/
|
|
194
|
-
|
|
197
|
+
// src/Setup.ts
|
|
198
|
+
import { performance } from "perf_hooks";
|
|
195
199
|
|
|
196
200
|
// src/constants.ts
|
|
197
201
|
var import_picocolors = __toESM(require_picocolors(), 1);
|
|
@@ -231,351 +235,148 @@ var REGEX = {
|
|
|
231
235
|
SAFE_TRACE: /^[a-zA-Z0-9-_:.]{1,128}$/
|
|
232
236
|
};
|
|
233
237
|
|
|
234
|
-
// src/
|
|
235
|
-
var
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
canceled: 499,
|
|
242
|
-
// Client Closed Request (nginx convention)
|
|
243
|
-
timeout: 504
|
|
238
|
+
// src/Setup.ts
|
|
239
|
+
var extractBuildConfigs = (config) => {
|
|
240
|
+
return config.apps.map(({ appId, entryPoint, plugins }) => ({
|
|
241
|
+
appId,
|
|
242
|
+
entryPoint,
|
|
243
|
+
plugins
|
|
244
|
+
}));
|
|
244
245
|
};
|
|
245
|
-
var
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
};
|
|
246
|
+
var extractRoutes = (taujsConfig) => {
|
|
247
|
+
const t0 = performance.now();
|
|
248
|
+
const allRoutes = [];
|
|
249
|
+
const apps = [];
|
|
250
|
+
const warnings = [];
|
|
251
|
+
const pathTracker = /* @__PURE__ */ new Map();
|
|
252
|
+
for (const app of taujsConfig.apps) {
|
|
253
|
+
const appRoutes = (app.routes ?? []).map((route) => {
|
|
254
|
+
const fullRoute = { ...route, appId: app.appId };
|
|
255
|
+
if (!pathTracker.has(route.path)) pathTracker.set(route.path, []);
|
|
256
|
+
pathTracker.get(route.path).push(app.appId);
|
|
257
|
+
return fullRoute;
|
|
258
|
+
});
|
|
259
|
+
apps.push({ appId: app.appId, routeCount: appRoutes.length });
|
|
260
|
+
allRoutes.push(...appRoutes);
|
|
311
261
|
}
|
|
312
|
-
|
|
313
|
-
|
|
262
|
+
for (const [path7, appIds] of pathTracker.entries()) {
|
|
263
|
+
if (appIds.length > 1) warnings.push(`Route path "${path7}" is declared in multiple apps: ${appIds.join(", ")}`);
|
|
314
264
|
}
|
|
315
|
-
|
|
316
|
-
|
|
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;
|
|
265
|
+
const sortedRoutes = allRoutes.sort((a, b) => computeScore(b.path) - computeScore(a.path));
|
|
266
|
+
const durationMs = performance.now() - t0;
|
|
364
267
|
return {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
}
|
|
268
|
+
routes: sortedRoutes,
|
|
269
|
+
apps,
|
|
270
|
+
totalRoutes: allRoutes.length,
|
|
271
|
+
durationMs,
|
|
272
|
+
warnings
|
|
371
273
|
};
|
|
372
274
|
};
|
|
373
|
-
var
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
275
|
+
var extractSecurity = (taujsConfig) => {
|
|
276
|
+
const t0 = performance.now();
|
|
277
|
+
const user = taujsConfig.security ?? {};
|
|
278
|
+
const userCsp = user.csp;
|
|
279
|
+
const hasExplicitCSP = !!userCsp;
|
|
280
|
+
const normalisedCsp = userCsp ? {
|
|
281
|
+
defaultMode: userCsp.defaultMode ?? "merge",
|
|
282
|
+
directives: userCsp.directives,
|
|
283
|
+
generateCSP: userCsp.generateCSP,
|
|
284
|
+
reporting: userCsp.reporting ? {
|
|
285
|
+
endpoint: userCsp.reporting.endpoint,
|
|
286
|
+
onViolation: userCsp.reporting.onViolation,
|
|
287
|
+
reportOnly: userCsp.reporting.reportOnly ?? false
|
|
288
|
+
} : void 0
|
|
289
|
+
} : void 0;
|
|
290
|
+
const security = { csp: normalisedCsp };
|
|
291
|
+
const summary = {
|
|
292
|
+
mode: hasExplicitCSP ? "explicit" : "dev-defaults",
|
|
293
|
+
defaultMode: normalisedCsp?.defaultMode ?? "merge",
|
|
294
|
+
hasReporting: !!normalisedCsp?.reporting?.endpoint,
|
|
295
|
+
reportOnly: !!normalisedCsp?.reporting?.reportOnly
|
|
296
|
+
};
|
|
297
|
+
const durationMs = performance.now() - t0;
|
|
298
|
+
return {
|
|
299
|
+
security,
|
|
300
|
+
durationMs,
|
|
301
|
+
hasExplicitCSP,
|
|
302
|
+
summary
|
|
388
303
|
};
|
|
389
|
-
return map[status] ?? "Error";
|
|
390
304
|
};
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
}
|
|
305
|
+
function printConfigSummary(logger, apps, configsCount, totalRoutes, durationMs, warnings) {
|
|
306
|
+
logger.info({}, `${CONTENT.TAG} [config] Loaded ${configsCount} app(s), ${totalRoutes} route(s) in ${durationMs.toFixed(1)}ms`);
|
|
307
|
+
apps.forEach((a) => logger.debug("routes", {}, `\u2022 ${a.appId}: ${a.routeCount} route(s)`));
|
|
308
|
+
warnings.forEach((w) => logger.warn({}, `${CONTENT.TAG} [warn] ${w}`));
|
|
421
309
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
310
|
+
function printSecuritySummary(logger, routes, security, hasExplicitCSP, securityDurationMs) {
|
|
311
|
+
const total = routes.length;
|
|
312
|
+
const disabled = routes.filter((r) => r.attr?.middleware?.csp === false).length;
|
|
313
|
+
const custom = routes.filter((r) => {
|
|
314
|
+
const v = r.attr?.middleware?.csp;
|
|
315
|
+
return v !== void 0 && v !== false;
|
|
316
|
+
}).length;
|
|
317
|
+
const enabled = total - disabled;
|
|
318
|
+
const hasReporting = !!security.csp?.reporting?.endpoint;
|
|
319
|
+
const mode = security.csp?.defaultMode ?? "merge";
|
|
320
|
+
let status = "configured";
|
|
321
|
+
let detail = "";
|
|
322
|
+
if (hasExplicitCSP) {
|
|
323
|
+
detail = `explicit, mode=${mode}`;
|
|
324
|
+
if (hasReporting) detail += ", reporting";
|
|
325
|
+
if (custom > 0) detail += `, ${custom} route override(s)`;
|
|
326
|
+
} else {
|
|
327
|
+
if (process.env.NODE_ENV === "production") {
|
|
328
|
+
logger.warn({}, "(consider explicit config for production)");
|
|
329
|
+
}
|
|
434
330
|
}
|
|
435
|
-
};
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
};
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
if (segment.startsWith(":")) {
|
|
447
|
-
score += 1;
|
|
448
|
-
if (/[?+*]$/.test(segment)) score -= 0.5;
|
|
449
|
-
} else if (segment === "*") {
|
|
450
|
-
score += 0.1;
|
|
331
|
+
logger.info({}, `${CONTENT.TAG} [security] CSP ${status} (${enabled}/${total} routes) in ${securityDurationMs.toFixed(1)}ms`);
|
|
332
|
+
}
|
|
333
|
+
function printContractReport(logger, report) {
|
|
334
|
+
for (const r of report.items) {
|
|
335
|
+
const line = `${CONTENT.TAG} [security][${r.key}] ${r.message}`;
|
|
336
|
+
if (r.status === "error") {
|
|
337
|
+
logger.error({}, line);
|
|
338
|
+
} else if (r.status === "warning") {
|
|
339
|
+
logger.warn({}, line);
|
|
340
|
+
} else if (r.status === "skipped") {
|
|
341
|
+
logger.debug(r.key, {}, line);
|
|
451
342
|
} else {
|
|
452
|
-
|
|
343
|
+
logger.info({}, line);
|
|
453
344
|
}
|
|
454
345
|
}
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
});
|
|
346
|
+
}
|
|
347
|
+
var computeScore = (path7) => {
|
|
348
|
+
return path7.split("/").filter(Boolean).reduce((score, segment) => score + (segment.startsWith(":") ? 1 : 10), 0);
|
|
467
349
|
};
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
350
|
+
|
|
351
|
+
// src/network/Network.ts
|
|
352
|
+
var import_picocolors3 = __toESM(require_picocolors(), 1);
|
|
353
|
+
import { networkInterfaces } from "os";
|
|
354
|
+
|
|
355
|
+
// src/logging/Logger.ts
|
|
356
|
+
var import_picocolors2 = __toESM(require_picocolors(), 1);
|
|
357
|
+
|
|
358
|
+
// src/logging/Parser.ts
|
|
359
|
+
function parseDebugInput(input) {
|
|
360
|
+
if (input === void 0) return void 0;
|
|
361
|
+
if (typeof input === "boolean") return input;
|
|
362
|
+
if (Array.isArray(input)) {
|
|
363
|
+
const pos = /* @__PURE__ */ new Set();
|
|
364
|
+
const neg = /* @__PURE__ */ new Set();
|
|
365
|
+
for (const raw of input) {
|
|
366
|
+
const s = String(raw);
|
|
367
|
+
const isNeg = s.startsWith("-") || s.startsWith("!");
|
|
368
|
+
const key = isNeg ? s.slice(1) : s;
|
|
369
|
+
const isValid = DEBUG_CATEGORIES.includes(key);
|
|
370
|
+
if (!isValid) {
|
|
371
|
+
console.warn(`[parseDebugInput] Invalid debug category: "${key}". Valid: ${DEBUG_CATEGORIES.join(", ")}`);
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
(isNeg ? neg : pos).add(key);
|
|
478
375
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
};
|
|
511
|
-
|
|
512
|
-
// src/security/Auth.ts
|
|
513
|
-
var createAuthHook = (routeMatchers, logger) => {
|
|
514
|
-
return async function authHook(req, reply) {
|
|
515
|
-
const url = new URL(req.url, `http://${req.headers.host}`).pathname;
|
|
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 });
|
|
522
|
-
return;
|
|
523
|
-
}
|
|
524
|
-
if (typeof req.server.authenticate !== "function") {
|
|
525
|
-
logger.warn("Route requires auth but Fastify authenticate decorator is missing", {
|
|
526
|
-
path: url,
|
|
527
|
-
appId: route.appId
|
|
528
|
-
});
|
|
529
|
-
return reply.status(500).send("Server misconfiguration: auth decorator missing.");
|
|
530
|
-
}
|
|
531
|
-
try {
|
|
532
|
-
logger.debug("auth", "Invoking authenticate(...)", { method: req.method, url: req.url });
|
|
533
|
-
await req.server.authenticate(req, reply);
|
|
534
|
-
logger.debug("auth", "Authentication successful", { method: req.method, url: req.url });
|
|
535
|
-
} catch (err) {
|
|
536
|
-
logger.debug("auth", "Authentication failed", { method: req.method, url: req.url });
|
|
537
|
-
return reply.send(err);
|
|
538
|
-
}
|
|
539
|
-
};
|
|
540
|
-
};
|
|
541
|
-
|
|
542
|
-
// src/security/CSP.ts
|
|
543
|
-
var import_fastify_plugin = __toESM(require_plugin(), 1);
|
|
544
|
-
import crypto from "crypto";
|
|
545
|
-
|
|
546
|
-
// src/utils/System.ts
|
|
547
|
-
import { dirname, join } from "path";
|
|
548
|
-
import "path";
|
|
549
|
-
import { fileURLToPath } from "url";
|
|
550
|
-
var isDevelopment = process.env.NODE_ENV === "development";
|
|
551
|
-
var __filename = fileURLToPath(import.meta.url);
|
|
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;
|
|
376
|
+
if (neg.size > 0 && pos.size === 0) {
|
|
377
|
+
const o = { all: true };
|
|
378
|
+
for (const k of neg) o[k] = false;
|
|
379
|
+
return o;
|
|
579
380
|
}
|
|
580
381
|
if (pos.size > 0 || neg.size > 0) {
|
|
581
382
|
const o = {};
|
|
@@ -625,10 +426,8 @@ var Logger = class _Logger {
|
|
|
625
426
|
debugEnabled = /* @__PURE__ */ new Set();
|
|
626
427
|
context = {};
|
|
627
428
|
child(context) {
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
context: { ...this.context, ...context }
|
|
631
|
-
});
|
|
429
|
+
const customChild = this.config.custom?.child?.(context) ?? this.config.custom;
|
|
430
|
+
const child = new _Logger({ ...this.config, custom: customChild, context: { ...this.context, ...context } });
|
|
632
431
|
child.debugEnabled = new Set(this.debugEnabled);
|
|
633
432
|
return child;
|
|
634
433
|
}
|
|
@@ -690,54 +489,55 @@ var Logger = class _Logger {
|
|
|
690
489
|
if (!this.shouldEmit(level)) return;
|
|
691
490
|
const timestamp = this.formatTimestamp();
|
|
692
491
|
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]
|
|
492
|
+
const customSink = this.config.custom?.[level];
|
|
694
493
|
const consoleFallback = level === "error" ? console.error : level === "warn" ? console.warn : console.log;
|
|
695
494
|
const sink = customSink ?? consoleFallback;
|
|
696
495
|
const merged = meta ?? {};
|
|
697
496
|
const withCtx = wantCtx && Object.keys(this.context).length > 0 ? { context: this.context, ...merged } : merged;
|
|
698
497
|
const finalMeta = this.shouldIncludeStack(level) ? withCtx : this.stripStacks(withCtx);
|
|
699
498
|
const hasMeta = finalMeta && typeof finalMeta === "object" ? Object.keys(finalMeta).length > 0 : false;
|
|
700
|
-
const
|
|
701
|
-
|
|
499
|
+
const levelText = level.toLowerCase() + (category ? `:${category.toLowerCase()}` : "");
|
|
500
|
+
const plainTag = `[${levelText}]`;
|
|
501
|
+
const coloredTag = (() => {
|
|
702
502
|
switch (level) {
|
|
703
503
|
case "debug":
|
|
704
|
-
return import_picocolors2.default.gray(
|
|
504
|
+
return import_picocolors2.default.gray(plainTag);
|
|
705
505
|
case "info":
|
|
706
|
-
return import_picocolors2.default.cyan(
|
|
506
|
+
return import_picocolors2.default.cyan(plainTag);
|
|
707
507
|
case "warn":
|
|
708
|
-
return import_picocolors2.default.yellow(
|
|
508
|
+
return import_picocolors2.default.yellow(plainTag);
|
|
709
509
|
case "error":
|
|
710
|
-
return import_picocolors2.default.red(
|
|
510
|
+
return import_picocolors2.default.red(plainTag);
|
|
711
511
|
default:
|
|
712
|
-
return
|
|
512
|
+
return plainTag;
|
|
713
513
|
}
|
|
714
514
|
})();
|
|
715
|
-
const
|
|
716
|
-
|
|
515
|
+
const tagForOutput = customSink ? plainTag : coloredTag;
|
|
516
|
+
const formatted = `${timestamp} ${tagForOutput} ${message}`;
|
|
517
|
+
if (this.config.singleLine && hasMeta && !customSink) {
|
|
717
518
|
const metaStr = JSON.stringify(finalMeta).replace(/\n/g, "\\n");
|
|
718
|
-
|
|
519
|
+
consoleFallback(`${formatted} ${metaStr}`);
|
|
719
520
|
return;
|
|
720
521
|
}
|
|
721
522
|
if (customSink) {
|
|
722
|
-
|
|
723
|
-
|
|
523
|
+
const obj = hasMeta ? finalMeta : {};
|
|
524
|
+
customSink(obj, formatted);
|
|
724
525
|
} else {
|
|
725
|
-
|
|
726
|
-
else consoleFallback(formatted);
|
|
526
|
+
hasMeta ? consoleFallback(formatted, finalMeta) : consoleFallback(formatted);
|
|
727
527
|
}
|
|
728
528
|
}
|
|
729
|
-
info(
|
|
730
|
-
this.emit("info", message, meta);
|
|
529
|
+
info(meta, message) {
|
|
530
|
+
this.emit("info", message ?? "", meta);
|
|
731
531
|
}
|
|
732
|
-
warn(
|
|
733
|
-
this.emit("warn", message, meta);
|
|
532
|
+
warn(meta, message) {
|
|
533
|
+
this.emit("warn", message ?? "", meta);
|
|
734
534
|
}
|
|
735
|
-
error(
|
|
736
|
-
this.emit("error", message, meta);
|
|
535
|
+
error(meta, message) {
|
|
536
|
+
this.emit("error", message ?? "", meta);
|
|
737
537
|
}
|
|
738
|
-
debug(category,
|
|
538
|
+
debug(category, meta, message) {
|
|
739
539
|
if (!this.debugEnabled.has(category)) return;
|
|
740
|
-
this.emit("debug", message, meta, category);
|
|
540
|
+
this.emit("debug", message ?? "", meta, category);
|
|
741
541
|
}
|
|
742
542
|
};
|
|
743
543
|
function createLogger(opts) {
|
|
@@ -754,1192 +554,1473 @@ function createLogger(opts) {
|
|
|
754
554
|
return logger;
|
|
755
555
|
}
|
|
756
556
|
|
|
757
|
-
// src/
|
|
758
|
-
var
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
if (
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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("; ");
|
|
557
|
+
// src/network/Network.ts
|
|
558
|
+
var isPrivateIPv4 = (addr) => {
|
|
559
|
+
if (!/^\d+\.\d+\.\d+\.\d+$/.test(addr)) return false;
|
|
560
|
+
const [a, b, _c, _d] = addr.split(".").map(Number);
|
|
561
|
+
if (a === 10) return true;
|
|
562
|
+
if (a === 192 && b === 168) return true;
|
|
563
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
564
|
+
return false;
|
|
774
565
|
};
|
|
775
|
-
var
|
|
776
|
-
|
|
777
|
-
const
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
}
|
|
782
|
-
|
|
566
|
+
var bannerPlugin = async (fastify, options) => {
|
|
567
|
+
const logger = createLogger({ debug: options.debug });
|
|
568
|
+
const dbgNetwork = logger.isDebugEnabled("network");
|
|
569
|
+
fastify.decorate("showBanner", function showBanner() {
|
|
570
|
+
const addr = this.server.address();
|
|
571
|
+
if (!addr || typeof addr === "string") return;
|
|
572
|
+
const { address, port } = addr;
|
|
573
|
+
const boundHost = address === "::1" ? "localhost" : address === "::" ? "::" : address === "0.0.0.0" ? "0.0.0.0" : address;
|
|
574
|
+
console.log(`\u2503 Local ${import_picocolors3.default.bold(`http://localhost:${port}/`)}`);
|
|
575
|
+
if (boundHost === "localhost" || boundHost === "127.0.0.1") {
|
|
576
|
+
console.log("\u2503 Network use --host to expose\n");
|
|
577
|
+
return;
|
|
783
578
|
}
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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);
|
|
579
|
+
const nets = networkInterfaces();
|
|
580
|
+
let networkAddress = null;
|
|
581
|
+
for (const ifaces of Object.values(nets)) {
|
|
582
|
+
if (!ifaces) continue;
|
|
583
|
+
for (const iface of ifaces) {
|
|
584
|
+
if (iface.internal || iface.family !== "IPv4") continue;
|
|
585
|
+
if (isPrivateIPv4(iface.address)) {
|
|
586
|
+
networkAddress = iface.address;
|
|
587
|
+
break;
|
|
838
588
|
}
|
|
839
|
-
|
|
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);
|
|
589
|
+
if (!networkAddress) networkAddress = iface.address;
|
|
847
590
|
}
|
|
848
|
-
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
{
|
|
852
|
-
);
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
591
|
+
if (networkAddress && isPrivateIPv4(networkAddress)) break;
|
|
592
|
+
}
|
|
593
|
+
if (networkAddress) {
|
|
594
|
+
console.log(`\u2503 Network http://${networkAddress}:${port}/
|
|
595
|
+
`);
|
|
596
|
+
if (dbgNetwork) logger.warn({}, import_picocolors3.default.yellow(`${CONTENT.TAG} [network] Dev server exposed on network - for local testing only.`));
|
|
597
|
+
}
|
|
598
|
+
logger.info({}, import_picocolors3.default.green(`${CONTENT.TAG} [network] Bound to host: ${boundHost}`));
|
|
599
|
+
});
|
|
600
|
+
fastify.addHook("onReady", async function() {
|
|
601
|
+
if (this.server.listening) {
|
|
602
|
+
this.showBanner();
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
this.server.once("listening", () => this.showBanner());
|
|
606
|
+
});
|
|
607
|
+
};
|
|
858
608
|
|
|
859
|
-
// src/
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
const
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
const mod = server.moduleGraph.getModuleById(id);
|
|
878
|
-
if (!mod) return;
|
|
879
|
-
await Promise.all([...mod.importedModules].map((childMod) => traverse(childMod.url)));
|
|
609
|
+
// src/network/CLI.ts
|
|
610
|
+
function readFlag(argv, keys, bareValue) {
|
|
611
|
+
const end = argv.indexOf("--");
|
|
612
|
+
const limit = end === -1 ? argv.length : end;
|
|
613
|
+
for (let i = 0; i < limit; i++) {
|
|
614
|
+
const arg = argv[i];
|
|
615
|
+
for (const key of keys) {
|
|
616
|
+
if (arg === key) {
|
|
617
|
+
const next = argv[i + 1];
|
|
618
|
+
if (!next || next.startsWith("-")) return bareValue;
|
|
619
|
+
return next.trim();
|
|
620
|
+
}
|
|
621
|
+
const pref = `${key}=`;
|
|
622
|
+
if (arg && arg.startsWith(pref)) {
|
|
623
|
+
const v = arg.slice(pref.length).trim();
|
|
624
|
+
return v || bareValue;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
880
627
|
}
|
|
881
|
-
|
|
882
|
-
await Promise.all(entries.map((url) => traverse(url)));
|
|
883
|
-
return [...visited].filter((url) => url.match(CSS_LANGS_RE));
|
|
628
|
+
return void 0;
|
|
884
629
|
}
|
|
885
|
-
function
|
|
886
|
-
const
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
630
|
+
function resolveNet(input) {
|
|
631
|
+
const env = process.env;
|
|
632
|
+
const argv = process.argv;
|
|
633
|
+
let host = "localhost";
|
|
634
|
+
let port = 5173;
|
|
635
|
+
let hmrPort = 5174;
|
|
636
|
+
if (input?.host) host = input.host;
|
|
637
|
+
if (Number.isFinite(input?.port)) port = Number(input.port);
|
|
638
|
+
if (Number.isFinite(input?.hmrPort)) hmrPort = Number(input.hmrPort);
|
|
639
|
+
if (env.HOST?.trim()) host = env.HOST.trim();
|
|
640
|
+
else if (env.FASTIFY_ADDRESS?.trim()) host = env.FASTIFY_ADDRESS.trim();
|
|
641
|
+
if (env.PORT) port = Number(env.PORT) || port;
|
|
642
|
+
if (env.FASTIFY_PORT) port = Number(env.FASTIFY_PORT) || port;
|
|
643
|
+
if (env.HMR_PORT) hmrPort = Number(env.HMR_PORT) || hmrPort;
|
|
644
|
+
const cliHost = readFlag(argv, ["--host", "--hostname", "-H"], "0.0.0.0");
|
|
645
|
+
const cliPort = readFlag(argv, ["--port", "-p"]);
|
|
646
|
+
const cliHMR = readFlag(argv, ["--hmr-port"]);
|
|
647
|
+
if (cliHost) host = cliHost;
|
|
648
|
+
if (cliPort) port = Number(cliPort) || port;
|
|
649
|
+
if (cliHMR) hmrPort = Number(cliHMR) || hmrPort;
|
|
650
|
+
if (host === "true" || host === "") host = "0.0.0.0";
|
|
651
|
+
return { host, port, hmrPort };
|
|
900
652
|
}
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
case "woff":
|
|
909
|
-
case "woff2":
|
|
910
|
-
return `<link rel="preload" href="${file}" as="font" type="font/${fileType}" crossorigin>`;
|
|
911
|
-
case "gif":
|
|
912
|
-
case "jpeg":
|
|
913
|
-
case "jpg":
|
|
914
|
-
case "png":
|
|
915
|
-
return `<link rel="preload" href="${file}" as="image" type="image/${fileType}">`;
|
|
916
|
-
case "svg":
|
|
917
|
-
return `<link rel="preload" href="${file}" as="image" type="image/svg+xml">`;
|
|
918
|
-
default:
|
|
919
|
-
return "";
|
|
653
|
+
|
|
654
|
+
// src/security/VerifyMiddleware.ts
|
|
655
|
+
var isAuthRequired = (route) => Boolean(route.attr?.middleware?.auth);
|
|
656
|
+
var hasAuthenticate = (app) => typeof app.authenticate === "function";
|
|
657
|
+
function formatCspLoadedMsg(hasGlobal, custom) {
|
|
658
|
+
if (hasGlobal) {
|
|
659
|
+
return custom > 0 ? `Loaded global config with ${custom} route override(s)` : "Loaded global config";
|
|
920
660
|
}
|
|
661
|
+
return custom > 0 ? `Loaded development defaults with ${custom} route override(s)` : "Loaded development defaults";
|
|
921
662
|
}
|
|
922
|
-
|
|
923
|
-
const
|
|
924
|
-
const
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
663
|
+
var verifyContracts = (app, routes, contracts, security) => {
|
|
664
|
+
const items = [];
|
|
665
|
+
for (const contract of contracts) {
|
|
666
|
+
const isRequired = contract.required(routes, security);
|
|
667
|
+
if (!isRequired) {
|
|
668
|
+
items.push({
|
|
669
|
+
key: contract.key,
|
|
670
|
+
status: "skipped",
|
|
671
|
+
message: `No routes require "${contract.key}"`
|
|
672
|
+
});
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
if (!contract.verify(app)) {
|
|
676
|
+
const msg = `[\u03C4js] ${contract.errorMessage}`;
|
|
677
|
+
items.push({ key: contract.key, status: "error", message: msg });
|
|
678
|
+
throw new Error(msg);
|
|
679
|
+
}
|
|
680
|
+
if (contract.key === "csp") {
|
|
681
|
+
const total = routes.length;
|
|
682
|
+
const disabled = routes.filter((r) => r.attr?.middleware?.csp === false).length;
|
|
683
|
+
const custom = routes.filter((r) => {
|
|
684
|
+
const v = r.attr?.middleware?.csp;
|
|
685
|
+
return v !== void 0 && v !== false;
|
|
686
|
+
}).length;
|
|
687
|
+
const enabled = total - disabled;
|
|
688
|
+
const hasGlobal = !!security?.csp;
|
|
689
|
+
let status = "verified";
|
|
690
|
+
let tail = "";
|
|
691
|
+
if (!hasGlobal && process.env.NODE_ENV === "production") {
|
|
692
|
+
status = "warning";
|
|
693
|
+
tail = " (consider adding global CSP for production)";
|
|
933
694
|
}
|
|
695
|
+
const baseMsg = formatCspLoadedMsg(hasGlobal, custom);
|
|
696
|
+
items.push({
|
|
697
|
+
key: "csp",
|
|
698
|
+
status,
|
|
699
|
+
message: baseMsg + tail
|
|
700
|
+
});
|
|
701
|
+
items.push({
|
|
702
|
+
key: "csp",
|
|
703
|
+
status,
|
|
704
|
+
message: `\u2713 Verified (${enabled} enabled, ${disabled} disabled, ${total} total). ` + tail
|
|
705
|
+
});
|
|
706
|
+
} else {
|
|
707
|
+
const count = routes.filter((r) => contract.required([r], security)).length;
|
|
708
|
+
items.push({
|
|
709
|
+
key: contract.key,
|
|
710
|
+
status: "verified",
|
|
711
|
+
message: `\u2713 ${count} route(s)`
|
|
712
|
+
});
|
|
934
713
|
}
|
|
935
714
|
}
|
|
936
|
-
return
|
|
937
|
-
}
|
|
938
|
-
var overrideCSSHMRConsoleError = () => {
|
|
939
|
-
const originalConsoleError = console.error;
|
|
940
|
-
console.error = function(message, ...optionalParams) {
|
|
941
|
-
if (typeof message === "string" && message.includes("css hmr is not supported in runtime mode")) return;
|
|
942
|
-
originalConsoleError.apply(console, [message, ...optionalParams]);
|
|
943
|
-
};
|
|
944
|
-
};
|
|
945
|
-
var ensureNonNull = (value, errorMessage) => {
|
|
946
|
-
if (value === void 0 || value === null) throw new Error(errorMessage);
|
|
947
|
-
return value;
|
|
948
|
-
};
|
|
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.`);
|
|
954
|
-
return {
|
|
955
|
-
beforeHead: headSplit,
|
|
956
|
-
afterHead: "",
|
|
957
|
-
beforeBody: beforeBody.replace(/\s*$/, ""),
|
|
958
|
-
afterBody: afterBody.replace(/^\s*/, "")
|
|
959
|
-
};
|
|
960
|
-
}
|
|
961
|
-
var rebuildTemplate = (parts, headContent, bodyContent) => {
|
|
962
|
-
return `${parts.beforeHead}${headContent}${parts.afterHead}${parts.beforeBody}${bodyContent}${parts.afterBody}`;
|
|
715
|
+
return { items };
|
|
963
716
|
};
|
|
964
717
|
|
|
965
|
-
// src/
|
|
966
|
-
var
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
return {
|
|
979
|
-
clientRoot,
|
|
980
|
-
entryPoint: config.entryPoint,
|
|
981
|
-
entryClient: config.entryClient || templateDefaults.defaultEntryClient,
|
|
982
|
-
entryServer: config.entryServer || templateDefaults.defaultEntryServer,
|
|
983
|
-
htmlTemplate: config.htmlTemplate || templateDefaults.defaultHtmlTemplate,
|
|
984
|
-
appId: config.appId
|
|
985
|
-
};
|
|
986
|
-
});
|
|
718
|
+
// src/SSRServer.ts
|
|
719
|
+
var import_fastify_plugin3 = __toESM(require_plugin(), 1);
|
|
720
|
+
|
|
721
|
+
// src/logging/AppError.ts
|
|
722
|
+
var HTTP_STATUS = {
|
|
723
|
+
infra: 500,
|
|
724
|
+
upstream: 502,
|
|
725
|
+
domain: 404,
|
|
726
|
+
validation: 400,
|
|
727
|
+
auth: 403,
|
|
728
|
+
canceled: 499,
|
|
729
|
+
// Client Closed Request (nginx convention)
|
|
730
|
+
timeout: 504
|
|
987
731
|
};
|
|
988
|
-
var
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
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
|
-
}
|
|
1051
|
-
} else {
|
|
1052
|
-
const bootstrapModule = `/${adjustedRelativePath}/${entryClient}`.replace(/\/{2,}/g, "/");
|
|
1053
|
-
bootstrapModules.set(clientRoot, bootstrapModule);
|
|
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"
|
|
732
|
+
var AppError = class _AppError extends Error {
|
|
733
|
+
kind;
|
|
734
|
+
httpStatus;
|
|
735
|
+
details;
|
|
736
|
+
safeMessage;
|
|
737
|
+
code;
|
|
738
|
+
constructor(message, kind, options = {}) {
|
|
739
|
+
super(message);
|
|
740
|
+
this.name = "AppError";
|
|
741
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
742
|
+
if (options.cause !== void 0) {
|
|
743
|
+
Object.defineProperty(this, "cause", {
|
|
744
|
+
value: options.cause,
|
|
745
|
+
enumerable: false,
|
|
746
|
+
writable: false,
|
|
747
|
+
configurable: true
|
|
1059
748
|
});
|
|
1060
749
|
}
|
|
750
|
+
this.kind = kind;
|
|
751
|
+
this.httpStatus = options.httpStatus ?? HTTP_STATUS[kind];
|
|
752
|
+
this.details = options.details;
|
|
753
|
+
this.safeMessage = options.safeMessage ?? this.getSafeMessage(kind, message);
|
|
754
|
+
this.code = options.code;
|
|
755
|
+
if (Error.captureStackTrace) Error.captureStackTrace(this, this.constructor);
|
|
1061
756
|
}
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
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
|
-
}
|
|
757
|
+
getSafeMessage(kind, message) {
|
|
758
|
+
return kind === "domain" || kind === "validation" || kind === "auth" ? message : "Internal Server Error";
|
|
759
|
+
}
|
|
760
|
+
serialiseValue(value, seen = /* @__PURE__ */ new WeakSet()) {
|
|
761
|
+
if (value === null || value === void 0) return value;
|
|
762
|
+
if (typeof value !== "object") return value;
|
|
763
|
+
if (seen.has(value)) return "[circular]";
|
|
764
|
+
seen.add(value);
|
|
765
|
+
if (value instanceof Error) {
|
|
766
|
+
return {
|
|
767
|
+
name: value.name,
|
|
768
|
+
message: value.message,
|
|
769
|
+
stack: value.stack,
|
|
770
|
+
...value instanceof _AppError && {
|
|
771
|
+
kind: value.kind,
|
|
772
|
+
httpStatus: value.httpStatus,
|
|
773
|
+
code: value.code
|
|
1108
774
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
if (Array.isArray(value)) return value.map((item) => this.serialiseValue(item, seen));
|
|
778
|
+
const result = {};
|
|
779
|
+
for (const [key, val] of Object.entries(value)) {
|
|
780
|
+
result[key] = this.serialiseValue(val, seen);
|
|
781
|
+
}
|
|
782
|
+
return result;
|
|
783
|
+
}
|
|
784
|
+
toJSON() {
|
|
785
|
+
return {
|
|
786
|
+
name: this.name,
|
|
787
|
+
kind: this.kind,
|
|
788
|
+
message: this.message,
|
|
789
|
+
safeMessage: this.safeMessage,
|
|
790
|
+
httpStatus: this.httpStatus,
|
|
791
|
+
...this.code && { code: this.code },
|
|
792
|
+
details: this.serialiseValue(this.details),
|
|
793
|
+
stack: this.stack,
|
|
794
|
+
...this.cause && {
|
|
795
|
+
cause: this.serialiseValue(this.cause)
|
|
1127
796
|
}
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
static notFound(message, details, code) {
|
|
800
|
+
return new _AppError(message, "domain", { httpStatus: 404, details, code });
|
|
801
|
+
}
|
|
802
|
+
static forbidden(message, details, code) {
|
|
803
|
+
return new _AppError(message, "auth", { httpStatus: 403, details, code });
|
|
804
|
+
}
|
|
805
|
+
static badRequest(message, details, code) {
|
|
806
|
+
return new _AppError(message, "validation", { httpStatus: 400, details, code });
|
|
807
|
+
}
|
|
808
|
+
static unprocessable(message, details, code) {
|
|
809
|
+
return new _AppError(message, "validation", { httpStatus: 422, details, code });
|
|
810
|
+
}
|
|
811
|
+
static timeout(message, details, code) {
|
|
812
|
+
return new _AppError(message, "timeout", { details, code });
|
|
813
|
+
}
|
|
814
|
+
static canceled(message, details, code) {
|
|
815
|
+
return new _AppError(message, "canceled", { details, code });
|
|
816
|
+
}
|
|
817
|
+
static internal(message, cause, details, code) {
|
|
818
|
+
return new _AppError(message, "infra", { cause, details, code });
|
|
819
|
+
}
|
|
820
|
+
static upstream(message, cause, details, code) {
|
|
821
|
+
return new _AppError(message, "upstream", { cause, details, code });
|
|
822
|
+
}
|
|
823
|
+
static serviceUnavailable(message, cause, details, code) {
|
|
824
|
+
return new _AppError(message, "infra", { httpStatus: 503, cause, details, code });
|
|
825
|
+
}
|
|
826
|
+
static from(err, fallback = "Internal error") {
|
|
827
|
+
return err instanceof _AppError ? err : _AppError.internal(err?.message ?? fallback, err);
|
|
828
|
+
}
|
|
829
|
+
};
|
|
830
|
+
function normaliseError(e) {
|
|
831
|
+
if (e instanceof Error) return { name: e.name, message: e.message, stack: e.stack };
|
|
832
|
+
const hasMessageProp = e != null && typeof e.message !== "undefined";
|
|
833
|
+
const msg = hasMessageProp ? String(e.message) : String(e);
|
|
834
|
+
return { name: "Error", message: msg };
|
|
835
|
+
}
|
|
836
|
+
function toReason(e) {
|
|
837
|
+
if (e instanceof Error) return e;
|
|
838
|
+
if (e === null) return new Error("null");
|
|
839
|
+
if (typeof e === "undefined") return new Error("Unknown render error");
|
|
840
|
+
const maybeMsg = e?.message;
|
|
841
|
+
if (typeof maybeMsg !== "undefined") return new Error(String(maybeMsg));
|
|
842
|
+
return new Error(String(e));
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// src/logging/utils/index.ts
|
|
846
|
+
var httpStatusFrom = (err, fallback = 500) => err instanceof AppError ? err.httpStatus : fallback;
|
|
847
|
+
var toHttp = (err) => {
|
|
848
|
+
const app = AppError.from(err);
|
|
849
|
+
const status = httpStatusFrom(app);
|
|
850
|
+
const errorMessage = app.safeMessage;
|
|
851
|
+
return {
|
|
852
|
+
status,
|
|
853
|
+
body: {
|
|
854
|
+
error: errorMessage,
|
|
855
|
+
...app.code && { code: app.code },
|
|
856
|
+
statusText: statusText(status)
|
|
1128
857
|
}
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
858
|
+
};
|
|
859
|
+
};
|
|
860
|
+
var statusText = (status) => {
|
|
861
|
+
const map = {
|
|
862
|
+
400: "Bad Request",
|
|
863
|
+
401: "Unauthorized",
|
|
864
|
+
403: "Forbidden",
|
|
865
|
+
404: "Not Found",
|
|
866
|
+
405: "Method Not Allowed",
|
|
867
|
+
408: "Request Timeout",
|
|
868
|
+
422: "Unprocessable Entity",
|
|
869
|
+
429: "Too Many Requests",
|
|
870
|
+
499: "Client Closed Request",
|
|
871
|
+
500: "Internal Server Error",
|
|
872
|
+
502: "Bad Gateway",
|
|
873
|
+
503: "Service Unavailable",
|
|
874
|
+
504: "Gateway Timeout"
|
|
875
|
+
};
|
|
876
|
+
return map[status] ?? "Error";
|
|
1139
877
|
};
|
|
1140
878
|
|
|
1141
|
-
// src/utils/
|
|
1142
|
-
import
|
|
1143
|
-
import { PassThrough } from "stream";
|
|
879
|
+
// src/utils/DataRoutes.ts
|
|
880
|
+
import { match } from "path-to-regexp";
|
|
1144
881
|
|
|
1145
|
-
// src/utils/
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
const
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
const
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
882
|
+
// src/utils/DataServices.ts
|
|
883
|
+
async function callServiceMethod(registry, serviceName, methodName, params, ctx) {
|
|
884
|
+
if (ctx.signal?.aborted) throw AppError.timeout("Request canceled");
|
|
885
|
+
const service = registry[serviceName];
|
|
886
|
+
if (!service) throw AppError.notFound(`Unknown service: ${serviceName}`);
|
|
887
|
+
const desc = service[methodName];
|
|
888
|
+
if (!desc) throw AppError.notFound(`Unknown method: ${serviceName}.${methodName}`);
|
|
889
|
+
const logger = ctx.logger?.child({
|
|
890
|
+
component: "service-call",
|
|
891
|
+
service: serviceName,
|
|
892
|
+
method: methodName,
|
|
893
|
+
traceId: ctx.traceId
|
|
894
|
+
});
|
|
895
|
+
try {
|
|
896
|
+
const p = desc.parsers?.params ? desc.parsers.params(params) : params;
|
|
897
|
+
const data = await desc.handler(p, ctx);
|
|
898
|
+
const out = desc.parsers?.result ? desc.parsers.result(data) : data;
|
|
899
|
+
if (typeof out !== "object" || out === null) throw AppError.internal(`Non-object result from ${serviceName}.${methodName}`);
|
|
900
|
+
return out;
|
|
901
|
+
} catch (err) {
|
|
902
|
+
logger?.error(
|
|
903
|
+
{
|
|
904
|
+
params,
|
|
905
|
+
error: err instanceof Error ? { name: err.name, message: err.message, stack: err.stack } : String(err)
|
|
906
|
+
},
|
|
907
|
+
"Service method failed"
|
|
908
|
+
);
|
|
909
|
+
throw err;
|
|
910
|
+
}
|
|
1161
911
|
}
|
|
912
|
+
var isServiceDescriptor = (obj) => {
|
|
913
|
+
if (typeof obj !== "object" || obj === null || Array.isArray(obj)) return false;
|
|
914
|
+
const maybe = obj;
|
|
915
|
+
return typeof maybe.serviceName === "string" && typeof maybe.serviceMethod === "string";
|
|
916
|
+
};
|
|
1162
917
|
|
|
1163
|
-
// src/utils/
|
|
1164
|
-
var
|
|
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
|
-
});
|
|
918
|
+
// src/utils/DataRoutes.ts
|
|
919
|
+
var safeDecode = (value) => {
|
|
1172
920
|
try {
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
921
|
+
return decodeURIComponent(value);
|
|
922
|
+
} catch {
|
|
923
|
+
return value;
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
var cleanPath = (path7) => {
|
|
927
|
+
if (!path7) return "/";
|
|
928
|
+
const basePart = path7.split("?")[0];
|
|
929
|
+
const base = basePart ? basePart.split("#")[0] : "/";
|
|
930
|
+
return base || "/";
|
|
931
|
+
};
|
|
932
|
+
var calculateSpecificity = (path7) => {
|
|
933
|
+
let score = 0;
|
|
934
|
+
const segments = path7.split("/").filter(Boolean);
|
|
935
|
+
for (const segment of segments) {
|
|
936
|
+
if (segment.startsWith(":")) {
|
|
937
|
+
score += 1;
|
|
938
|
+
if (/[?+*]$/.test(segment)) score -= 0.5;
|
|
939
|
+
} else if (segment === "*") {
|
|
940
|
+
score += 0.1;
|
|
941
|
+
} else {
|
|
942
|
+
score += 10;
|
|
1181
943
|
}
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
944
|
+
}
|
|
945
|
+
score += segments.length * 0.1;
|
|
946
|
+
return score;
|
|
947
|
+
};
|
|
948
|
+
var isPlainObject = (v) => !!v && typeof v === "object" && Object.getPrototypeOf(v) === Object.prototype;
|
|
949
|
+
var createRouteMatchers = (routes) => {
|
|
950
|
+
const sortedRoutes = [...routes].sort((a, b) => calculateSpecificity(b.path) - calculateSpecificity(a.path));
|
|
951
|
+
return sortedRoutes.map((route) => {
|
|
952
|
+
const matcher = match(route.path, { decode: safeDecode });
|
|
953
|
+
const specificity = calculateSpecificity(route.path);
|
|
954
|
+
const keys = [];
|
|
955
|
+
return { route, matcher, keys, specificity };
|
|
956
|
+
});
|
|
957
|
+
};
|
|
958
|
+
var matchRoute = (url, routeMatchers) => {
|
|
959
|
+
const path7 = cleanPath(url);
|
|
960
|
+
for (const { route, matcher, keys } of routeMatchers) {
|
|
961
|
+
const match2 = matcher(path7);
|
|
962
|
+
if (match2) {
|
|
963
|
+
return {
|
|
964
|
+
route,
|
|
965
|
+
params: match2.params,
|
|
966
|
+
keys
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
return null;
|
|
971
|
+
};
|
|
972
|
+
var fetchInitialData = async (attr, params, serviceRegistry, ctx, callServiceMethodImpl = callServiceMethod) => {
|
|
973
|
+
const dataHandler = attr?.data;
|
|
974
|
+
if (!dataHandler || typeof dataHandler !== "function") return {};
|
|
975
|
+
try {
|
|
976
|
+
const result = await dataHandler(params, {
|
|
977
|
+
...ctx,
|
|
978
|
+
headers: ctx.headers ?? {}
|
|
979
|
+
});
|
|
980
|
+
if (isServiceDescriptor(result)) {
|
|
981
|
+
const { serviceName, serviceMethod, args } = result;
|
|
982
|
+
return callServiceMethodImpl(serviceRegistry, serviceName, serviceMethod, args ?? {}, ctx);
|
|
983
|
+
}
|
|
984
|
+
if (isPlainObject(result)) return result;
|
|
985
|
+
throw AppError.badRequest("attr.data must return a plain object or a ServiceDescriptor");
|
|
986
|
+
} catch (err) {
|
|
987
|
+
let e = AppError.from(err);
|
|
988
|
+
const msg = String(err?.message ?? "");
|
|
989
|
+
const looksLikeHtml = /<!DOCTYPE/i.test(msg) || /<html/i.test(msg) || /Unexpected token <.*JSON/i.test(msg);
|
|
990
|
+
if (looksLikeHtml) {
|
|
991
|
+
const prevDetails = e.details && typeof e.details === "object" ? e.details : {};
|
|
992
|
+
e = AppError.internal("attr.data expected JSON but received HTML. Likely cause: API route missing or returning HTML.", err, {
|
|
993
|
+
...prevDetails,
|
|
994
|
+
hint: "api-missing-or-content-type",
|
|
995
|
+
suggestion: "Register api route so it returns JSON, or return a ServiceDescriptor from attr.data and use the ServiceRegistry.",
|
|
996
|
+
logged: true
|
|
1192
997
|
});
|
|
1193
998
|
}
|
|
1194
|
-
const
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
999
|
+
const level = e.kind === "domain" || e.kind === "validation" || e.kind === "auth" ? "warn" : "error";
|
|
1000
|
+
const meta = {
|
|
1001
|
+
component: "fetch-initial-data",
|
|
1002
|
+
kind: e.kind,
|
|
1003
|
+
httpStatus: e.httpStatus,
|
|
1004
|
+
...e.code ? { code: e.code } : {},
|
|
1005
|
+
...e.details ? { details: e.details } : {},
|
|
1006
|
+
...params ? { params } : {},
|
|
1007
|
+
traceId: ctx.traceId
|
|
1008
|
+
};
|
|
1009
|
+
ctx.logger?.[level](meta, e.message);
|
|
1010
|
+
throw e;
|
|
1011
|
+
}
|
|
1012
|
+
};
|
|
1013
|
+
|
|
1014
|
+
// src/security/Auth.ts
|
|
1015
|
+
var createAuthHook = (routeMatchers, logger) => {
|
|
1016
|
+
return async function authHook(req, reply) {
|
|
1017
|
+
const url = new URL(req.url, `http://${req.headers.host}`).pathname;
|
|
1018
|
+
const match2 = matchRoute(url, routeMatchers);
|
|
1019
|
+
if (!match2) return;
|
|
1020
|
+
const { route } = match2;
|
|
1021
|
+
const authConfig = route.attr?.middleware?.auth;
|
|
1022
|
+
if (!authConfig) {
|
|
1023
|
+
logger.debug("auth", { method: req.method, url: req.url }, "(none)");
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
if (typeof req.server.authenticate !== "function") {
|
|
1027
|
+
logger.warn(
|
|
1028
|
+
{
|
|
1029
|
+
path: url,
|
|
1030
|
+
appId: route.appId
|
|
1031
|
+
},
|
|
1032
|
+
"Route requires auth but Fastify authenticate decorator is missing"
|
|
1033
|
+
);
|
|
1034
|
+
return reply.status(500).send("Server misconfiguration: auth decorator missing.");
|
|
1035
|
+
}
|
|
1036
|
+
try {
|
|
1037
|
+
logger.debug("auth", { method: req.method, url: req.url }, "Invoking authenticate(...)");
|
|
1038
|
+
await req.server.authenticate(req, reply);
|
|
1039
|
+
logger.debug("auth", { method: req.method, url: req.url }, "Authentication successful");
|
|
1040
|
+
} catch (err) {
|
|
1041
|
+
logger.debug("auth", { method: req.method, url: req.url }, "Authentication failed");
|
|
1042
|
+
return reply.send(err);
|
|
1043
|
+
}
|
|
1044
|
+
};
|
|
1045
|
+
};
|
|
1046
|
+
|
|
1047
|
+
// src/security/CSP.ts
|
|
1048
|
+
var import_fastify_plugin = __toESM(require_plugin(), 1);
|
|
1049
|
+
import crypto from "crypto";
|
|
1050
|
+
|
|
1051
|
+
// src/utils/System.ts
|
|
1052
|
+
import { dirname, join } from "path";
|
|
1053
|
+
import "path";
|
|
1054
|
+
import { fileURLToPath } from "url";
|
|
1055
|
+
var isDevelopment = process.env.NODE_ENV === "development";
|
|
1056
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
1057
|
+
var __dirname = join(dirname(__filename), !isDevelopment ? "./" : "..");
|
|
1058
|
+
|
|
1059
|
+
// src/security/CSP.ts
|
|
1060
|
+
var defaultGenerateCSP = (directives, nonce, req) => {
|
|
1061
|
+
const merged = { ...directives };
|
|
1062
|
+
merged["script-src"] = merged["script-src"] || ["'self'"];
|
|
1063
|
+
if (!merged["script-src"].some((v) => v.startsWith("'nonce-"))) {
|
|
1064
|
+
merged["script-src"].push(`'nonce-${nonce}'`);
|
|
1065
|
+
}
|
|
1066
|
+
if (isDevelopment) {
|
|
1067
|
+
const connect = merged["connect-src"] || ["'self'"];
|
|
1068
|
+
if (!connect.includes("ws:")) connect.push("ws:");
|
|
1069
|
+
if (!connect.includes("http:")) connect.push("http:");
|
|
1070
|
+
merged["connect-src"] = connect;
|
|
1071
|
+
const style = merged["style-src"] || ["'self'"];
|
|
1072
|
+
if (!style.includes("'unsafe-inline'")) style.push("'unsafe-inline'");
|
|
1073
|
+
merged["style-src"] = style;
|
|
1074
|
+
}
|
|
1075
|
+
return Object.entries(merged).map(([key, values]) => `${key} ${values.join(" ")}`).join("; ");
|
|
1076
|
+
};
|
|
1077
|
+
var generateNonce = () => crypto.randomBytes(16).toString("base64");
|
|
1078
|
+
var mergeDirectives = (base, override) => {
|
|
1079
|
+
const merged = { ...base };
|
|
1080
|
+
for (const [directive, values] of Object.entries(override)) {
|
|
1081
|
+
if (merged[directive]) {
|
|
1082
|
+
merged[directive] = [.../* @__PURE__ */ new Set([...merged[directive], ...values])];
|
|
1216
1083
|
} else {
|
|
1217
|
-
|
|
1218
|
-
if (!renderModule) throw AppError.internal(`Render module not found for clientRoot: ${clientRoot}. Module should have been preloaded.`);
|
|
1084
|
+
merged[directive] = [...values];
|
|
1219
1085
|
}
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
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 = "";
|
|
1086
|
+
}
|
|
1087
|
+
return merged;
|
|
1088
|
+
};
|
|
1089
|
+
var findMatchingRoute = (routeMatchers, path7) => {
|
|
1090
|
+
if (!routeMatchers) return null;
|
|
1091
|
+
const match2 = matchRoute(path7, routeMatchers);
|
|
1092
|
+
return match2 ? { route: match2.route, params: match2.params } : null;
|
|
1093
|
+
};
|
|
1094
|
+
var cspPlugin = (0, import_fastify_plugin.default)(
|
|
1095
|
+
async (fastify, opts) => {
|
|
1096
|
+
const { generateCSP = defaultGenerateCSP, routes = [], routeMatchers, debug } = opts;
|
|
1097
|
+
const globalDirectives = opts.directives || DEV_CSP_DIRECTIVES;
|
|
1098
|
+
const matchers = routeMatchers || (routes.length > 0 ? createRouteMatchers(routes) : null);
|
|
1099
|
+
const logger = createLogger({
|
|
1100
|
+
debug,
|
|
1101
|
+
context: { component: "csp-plugin" }
|
|
1102
|
+
});
|
|
1103
|
+
fastify.addHook("onRequest", (req, reply, done) => {
|
|
1104
|
+
const nonce = generateNonce();
|
|
1105
|
+
req.cspNonce = nonce;
|
|
1247
1106
|
try {
|
|
1248
|
-
const
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
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 });
|
|
1107
|
+
const routeMatch = findMatchingRoute(matchers, req.url);
|
|
1108
|
+
const routeCSP = routeMatch?.route.attr?.middleware?.csp;
|
|
1109
|
+
if (routeCSP === false) {
|
|
1110
|
+
done();
|
|
1256
1111
|
return;
|
|
1257
1112
|
}
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
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,
|
|
1315
|
-
{
|
|
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: () => {
|
|
1113
|
+
let finalDirectives = globalDirectives;
|
|
1114
|
+
if (routeCSP && typeof routeCSP === "object") {
|
|
1115
|
+
if (!routeCSP.disabled) {
|
|
1116
|
+
let routeDirectives;
|
|
1117
|
+
if (typeof routeCSP.directives === "function") {
|
|
1118
|
+
const params = routeMatch?.params || {};
|
|
1119
|
+
routeDirectives = routeCSP.directives({
|
|
1120
|
+
url: req.url,
|
|
1121
|
+
params,
|
|
1122
|
+
headers: req.headers,
|
|
1123
|
+
req
|
|
1124
|
+
});
|
|
1125
|
+
} else {
|
|
1126
|
+
routeDirectives = routeCSP.directives || {};
|
|
1127
|
+
}
|
|
1128
|
+
if (routeCSP.mode === "replace") {
|
|
1129
|
+
finalDirectives = routeDirectives;
|
|
1130
|
+
} else {
|
|
1131
|
+
finalDirectives = mergeDirectives(globalDirectives, routeDirectives);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
let cspHeader;
|
|
1136
|
+
if (routeCSP?.generateCSP) {
|
|
1137
|
+
cspHeader = routeCSP.generateCSP(finalDirectives, nonce, req);
|
|
1138
|
+
} else {
|
|
1139
|
+
cspHeader = generateCSP(finalDirectives, nonce, req);
|
|
1140
|
+
}
|
|
1141
|
+
reply.header("Content-Security-Policy", cspHeader);
|
|
1142
|
+
} catch (error) {
|
|
1143
|
+
logger.error(
|
|
1144
|
+
{
|
|
1145
|
+
url: req.url,
|
|
1146
|
+
error: error instanceof Error ? { name: error.name, message: error.message, stack: error.stack } : String(error)
|
|
1323
1147
|
},
|
|
1324
|
-
|
|
1325
|
-
|
|
1148
|
+
"CSP plugin error"
|
|
1149
|
+
);
|
|
1150
|
+
const fallbackHeader = generateCSP(globalDirectives, nonce, req);
|
|
1151
|
+
reply.header("Content-Security-Policy", fallbackHeader);
|
|
1152
|
+
}
|
|
1153
|
+
done();
|
|
1154
|
+
});
|
|
1155
|
+
},
|
|
1156
|
+
{ name: "taujs-csp-plugin" }
|
|
1157
|
+
);
|
|
1158
|
+
|
|
1159
|
+
// src/security/CSPReporting.ts
|
|
1160
|
+
var import_fastify_plugin2 = __toESM(require_plugin(), 1);
|
|
1161
|
+
function sanitiseContext(ctx) {
|
|
1162
|
+
return {
|
|
1163
|
+
userAgent: ctx.userAgent,
|
|
1164
|
+
ip: ctx.ip,
|
|
1165
|
+
referer: ctx.referer,
|
|
1166
|
+
timestamp: ctx.timestamp
|
|
1167
|
+
// headers: ctx.headers,
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
function logCspViolation(logger, report, context) {
|
|
1171
|
+
logger.warn(
|
|
1172
|
+
{
|
|
1173
|
+
violation: {
|
|
1174
|
+
documentUri: report["document-uri"],
|
|
1175
|
+
violatedDirective: report["violated-directive"],
|
|
1176
|
+
blockedUri: report["blocked-uri"],
|
|
1177
|
+
sourceFile: report["source-file"],
|
|
1178
|
+
line: report["line-number"],
|
|
1179
|
+
column: report["column-number"],
|
|
1180
|
+
scriptSample: report["script-sample"],
|
|
1181
|
+
originalPolicy: report["original-policy"],
|
|
1182
|
+
disposition: report.disposition
|
|
1183
|
+
},
|
|
1184
|
+
context: {
|
|
1185
|
+
userAgent: context.userAgent,
|
|
1186
|
+
ip: context.ip,
|
|
1187
|
+
referer: context.referer,
|
|
1188
|
+
timestamp: context.timestamp
|
|
1189
|
+
}
|
|
1190
|
+
},
|
|
1191
|
+
"CSP Violation"
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
var processCSPReport = (body, context, logger) => {
|
|
1195
|
+
try {
|
|
1196
|
+
const reportData = body?.["csp-report"] || body;
|
|
1197
|
+
if (!reportData || typeof reportData !== "object") {
|
|
1198
|
+
logger.warn(
|
|
1199
|
+
{
|
|
1200
|
+
bodyType: typeof body,
|
|
1201
|
+
context: sanitiseContext(context)
|
|
1202
|
+
},
|
|
1203
|
+
"Ignoring malformed CSP report"
|
|
1204
|
+
);
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
const documentUri = reportData["document-uri"] ?? reportData["documentURL"];
|
|
1208
|
+
const violatedDirective = reportData["violated-directive"] ?? reportData["violatedDirective"];
|
|
1209
|
+
if (!documentUri || !violatedDirective) {
|
|
1210
|
+
logger.warn(
|
|
1211
|
+
{
|
|
1212
|
+
hasDocumentUri: !!documentUri,
|
|
1213
|
+
hasViolatedDirective: !!violatedDirective,
|
|
1214
|
+
context: sanitiseContext(context)
|
|
1215
|
+
},
|
|
1216
|
+
"Ignoring incomplete CSP report"
|
|
1217
|
+
);
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
const violation = {
|
|
1221
|
+
"document-uri": String(documentUri),
|
|
1222
|
+
"violated-directive": String(violatedDirective),
|
|
1223
|
+
"blocked-uri": reportData["blocked-uri"] ?? reportData["blockedURL"] ?? "",
|
|
1224
|
+
"source-file": reportData["source-file"] ?? reportData["sourceFile"],
|
|
1225
|
+
"line-number": reportData["line-number"] ?? reportData["lineNumber"],
|
|
1226
|
+
"column-number": reportData["column-number"] ?? reportData["columnNumber"],
|
|
1227
|
+
"script-sample": reportData["script-sample"] ?? reportData["sample"],
|
|
1228
|
+
"original-policy": reportData["original-policy"] ?? reportData["originalPolicy"] ?? "",
|
|
1229
|
+
disposition: reportData.disposition ?? "enforce"
|
|
1230
|
+
};
|
|
1231
|
+
logCspViolation(logger, violation, context);
|
|
1232
|
+
} catch (processingError) {
|
|
1233
|
+
logger.warn(
|
|
1234
|
+
{
|
|
1235
|
+
error: processingError instanceof Error ? processingError.message : String(processingError),
|
|
1236
|
+
bodyType: typeof body,
|
|
1237
|
+
context: sanitiseContext(context)
|
|
1238
|
+
},
|
|
1239
|
+
"CSP report processing failed"
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1242
|
+
};
|
|
1243
|
+
var cspReportPlugin = (0, import_fastify_plugin2.default)(
|
|
1244
|
+
async (fastify, opts) => {
|
|
1245
|
+
const { onViolation } = opts;
|
|
1246
|
+
if (!opts.path || typeof opts.path !== "string") throw AppError.badRequest("CSP report path is required and must be a string");
|
|
1247
|
+
const logger = createLogger({
|
|
1248
|
+
debug: opts.debug,
|
|
1249
|
+
context: { service: "csp-reporting" },
|
|
1250
|
+
minLevel: "info"
|
|
1251
|
+
});
|
|
1252
|
+
fastify.post(opts.path, async (req, reply) => {
|
|
1253
|
+
const context = {
|
|
1254
|
+
userAgent: req.headers["user-agent"],
|
|
1255
|
+
ip: req.ip,
|
|
1256
|
+
referer: req.headers.referer,
|
|
1257
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1258
|
+
headers: req.headers,
|
|
1259
|
+
__fastifyRequest: req
|
|
1260
|
+
// onViolation callback
|
|
1261
|
+
};
|
|
1262
|
+
try {
|
|
1263
|
+
processCSPReport(req.body, context, logger);
|
|
1264
|
+
const reportData = req.body?.["csp-report"] || req.body;
|
|
1265
|
+
if (onViolation && reportData && typeof reportData === "object") {
|
|
1266
|
+
const documentUri = reportData["document-uri"] ?? reportData["documentURL"];
|
|
1267
|
+
const violatedDirective = reportData["violated-directive"] ?? reportData["violatedDirective"];
|
|
1268
|
+
if (documentUri && violatedDirective) {
|
|
1269
|
+
const violation = {
|
|
1270
|
+
"document-uri": String(documentUri),
|
|
1271
|
+
"violated-directive": String(violatedDirective),
|
|
1272
|
+
"blocked-uri": reportData["blocked-uri"] ?? reportData["blockedURL"] ?? "",
|
|
1273
|
+
"source-file": reportData["source-file"] ?? reportData["sourceFile"],
|
|
1274
|
+
"line-number": reportData["line-number"] ?? reportData["lineNumber"],
|
|
1275
|
+
"column-number": reportData["column-number"] ?? reportData["columnNumber"],
|
|
1276
|
+
"script-sample": reportData["script-sample"] ?? reportData["sample"],
|
|
1277
|
+
"original-policy": reportData["original-policy"] ?? reportData["originalPolicy"] ?? "",
|
|
1278
|
+
disposition: reportData.disposition ?? "enforce"
|
|
1279
|
+
};
|
|
1280
|
+
onViolation(violation, req);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
} catch (err) {
|
|
1284
|
+
logger.warn(
|
|
1285
|
+
{
|
|
1286
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1326
1287
|
},
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1288
|
+
"CSP reporting route failed"
|
|
1289
|
+
);
|
|
1290
|
+
}
|
|
1291
|
+
reply.code(204).send();
|
|
1292
|
+
});
|
|
1293
|
+
},
|
|
1294
|
+
{ name: "taujs-csp-report-plugin" }
|
|
1295
|
+
);
|
|
1296
|
+
|
|
1297
|
+
// src/utils/AssetManager.ts
|
|
1298
|
+
import { readFile } from "fs/promises";
|
|
1299
|
+
import path2 from "path";
|
|
1300
|
+
import { pathToFileURL } from "url";
|
|
1301
|
+
|
|
1302
|
+
// src/utils/Templates.ts
|
|
1303
|
+
var CSS_LANGS_RE = /\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\?)/;
|
|
1304
|
+
async function collectStyle(server, entries) {
|
|
1305
|
+
const urls = await collectStyleUrls(server, entries);
|
|
1306
|
+
const codes = await Promise.all(
|
|
1307
|
+
urls.map(async (url) => {
|
|
1308
|
+
const res = await server.transformRequest(url + "?direct");
|
|
1309
|
+
return [`/* [collectStyle] ${url} */`, res?.code];
|
|
1310
|
+
})
|
|
1311
|
+
);
|
|
1312
|
+
return codes.flat().filter(Boolean).join("\n\n");
|
|
1313
|
+
}
|
|
1314
|
+
async function collectStyleUrls(server, entries) {
|
|
1315
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1316
|
+
async function traverse(url) {
|
|
1317
|
+
const [, id] = await server.moduleGraph.resolveUrl(url);
|
|
1318
|
+
if (visited.has(id)) return;
|
|
1319
|
+
visited.add(id);
|
|
1320
|
+
const mod = server.moduleGraph.getModuleById(id);
|
|
1321
|
+
if (!mod) return;
|
|
1322
|
+
await Promise.all([...mod.importedModules].map((childMod) => traverse(childMod.url)));
|
|
1323
|
+
}
|
|
1324
|
+
await Promise.all(entries.map((e) => server.transformRequest(e)));
|
|
1325
|
+
await Promise.all(entries.map((url) => traverse(url)));
|
|
1326
|
+
return [...visited].filter((url) => url.match(CSS_LANGS_RE));
|
|
1327
|
+
}
|
|
1328
|
+
function renderPreloadLinks(ssrManifest, basePath = "") {
|
|
1329
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1330
|
+
let links = "";
|
|
1331
|
+
for (const moduleId in ssrManifest) {
|
|
1332
|
+
const files = ssrManifest[moduleId];
|
|
1333
|
+
if (files) {
|
|
1334
|
+
files.forEach((file) => {
|
|
1335
|
+
if (!seen.has(file)) {
|
|
1336
|
+
seen.add(file);
|
|
1337
|
+
links += renderPreloadLink(basePath ? `${basePath}/${file}` : `${file}`);
|
|
1338
|
+
}
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
return links;
|
|
1343
|
+
}
|
|
1344
|
+
function renderPreloadLink(file) {
|
|
1345
|
+
const fileType = file.match(/\.(js|css|woff2?|gif|jpe?g|png|svg)$/)?.[1];
|
|
1346
|
+
switch (fileType) {
|
|
1347
|
+
case "js":
|
|
1348
|
+
return `<link rel="modulepreload" href="${file}">`;
|
|
1349
|
+
case "css":
|
|
1350
|
+
return `<link rel="stylesheet" href="${file}">`;
|
|
1351
|
+
case "woff":
|
|
1352
|
+
case "woff2":
|
|
1353
|
+
return `<link rel="preload" href="${file}" as="font" type="font/${fileType}" crossorigin>`;
|
|
1354
|
+
case "gif":
|
|
1355
|
+
case "jpeg":
|
|
1356
|
+
case "jpg":
|
|
1357
|
+
case "png":
|
|
1358
|
+
return `<link rel="preload" href="${file}" as="image" type="image/${fileType}">`;
|
|
1359
|
+
case "svg":
|
|
1360
|
+
return `<link rel="preload" href="${file}" as="image" type="image/svg+xml">`;
|
|
1361
|
+
default:
|
|
1362
|
+
return "";
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
function getCssLinks(manifest, basePath = "") {
|
|
1366
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1367
|
+
const styles = [];
|
|
1368
|
+
for (const key in manifest) {
|
|
1369
|
+
const entry = manifest[key];
|
|
1370
|
+
if (entry && entry.css) {
|
|
1371
|
+
for (const cssFile of entry.css) {
|
|
1372
|
+
if (!seen.has(cssFile)) {
|
|
1373
|
+
seen.add(cssFile);
|
|
1374
|
+
styles.push(`<link rel="preload stylesheet" as="style" type="text/css" href="${basePath}/${cssFile}">`);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
return styles.join("\n");
|
|
1380
|
+
}
|
|
1381
|
+
var overrideCSSHMRConsoleError = () => {
|
|
1382
|
+
const originalConsoleError = console.error;
|
|
1383
|
+
console.error = function(message, ...optionalParams) {
|
|
1384
|
+
if (typeof message === "string" && message.includes("css hmr is not supported in runtime mode")) return;
|
|
1385
|
+
originalConsoleError.apply(console, [message, ...optionalParams]);
|
|
1386
|
+
};
|
|
1387
|
+
};
|
|
1388
|
+
var ensureNonNull = (value, errorMessage) => {
|
|
1389
|
+
if (value === void 0 || value === null) throw new Error(errorMessage);
|
|
1390
|
+
return value;
|
|
1391
|
+
};
|
|
1392
|
+
function processTemplate(template) {
|
|
1393
|
+
const [headSplit, bodySplit] = template.split(SSRTAG.ssrHead);
|
|
1394
|
+
if (typeof bodySplit === "undefined") throw new Error(`Template is missing ${SSRTAG.ssrHead} marker.`);
|
|
1395
|
+
const [beforeBody, afterBody] = bodySplit.split(SSRTAG.ssrHtml);
|
|
1396
|
+
if (typeof beforeBody === "undefined" || typeof afterBody === "undefined") throw new Error(`Template is missing ${SSRTAG.ssrHtml} marker.`);
|
|
1397
|
+
return {
|
|
1398
|
+
beforeHead: headSplit,
|
|
1399
|
+
afterHead: "",
|
|
1400
|
+
beforeBody: beforeBody.replace(/\s*$/, ""),
|
|
1401
|
+
afterBody: afterBody.replace(/^\s*/, "")
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1404
|
+
var rebuildTemplate = (parts, headContent, bodyContent) => {
|
|
1405
|
+
return `${parts.beforeHead}${headContent}${parts.afterHead}${parts.beforeBody}${bodyContent}${parts.afterBody}`;
|
|
1406
|
+
};
|
|
1407
|
+
|
|
1408
|
+
// src/utils/AssetManager.ts
|
|
1409
|
+
var createMaps = () => ({
|
|
1410
|
+
bootstrapModules: /* @__PURE__ */ new Map(),
|
|
1411
|
+
cssLinks: /* @__PURE__ */ new Map(),
|
|
1412
|
+
manifests: /* @__PURE__ */ new Map(),
|
|
1413
|
+
preloadLinks: /* @__PURE__ */ new Map(),
|
|
1414
|
+
renderModules: /* @__PURE__ */ new Map(),
|
|
1415
|
+
ssrManifests: /* @__PURE__ */ new Map(),
|
|
1416
|
+
templates: /* @__PURE__ */ new Map()
|
|
1417
|
+
});
|
|
1418
|
+
var processConfigs = (configs, baseClientRoot, templateDefaults) => {
|
|
1419
|
+
return configs.map((config) => {
|
|
1420
|
+
const clientRoot = path2.resolve(baseClientRoot, config.entryPoint);
|
|
1421
|
+
return {
|
|
1422
|
+
clientRoot,
|
|
1423
|
+
entryPoint: config.entryPoint,
|
|
1424
|
+
entryClient: config.entryClient || templateDefaults.defaultEntryClient,
|
|
1425
|
+
entryServer: config.entryServer || templateDefaults.defaultEntryServer,
|
|
1426
|
+
htmlTemplate: config.htmlTemplate || templateDefaults.defaultHtmlTemplate,
|
|
1427
|
+
appId: config.appId
|
|
1428
|
+
};
|
|
1429
|
+
});
|
|
1430
|
+
};
|
|
1431
|
+
var loadAssets = async (processedConfigs, baseClientRoot, bootstrapModules, cssLinks, manifests, preloadLinks, renderModules, ssrManifests, templates, opts = {}) => {
|
|
1432
|
+
const logger = opts.logger ?? createLogger({
|
|
1433
|
+
debug: opts.debug,
|
|
1434
|
+
includeContext: true
|
|
1435
|
+
});
|
|
1436
|
+
for (const config of processedConfigs) {
|
|
1437
|
+
const { clientRoot, entryClient, entryServer, htmlTemplate } = config;
|
|
1438
|
+
try {
|
|
1439
|
+
const templateHtmlPath = path2.join(clientRoot, htmlTemplate);
|
|
1440
|
+
const templateHtml = await readFile(templateHtmlPath, "utf-8");
|
|
1441
|
+
templates.set(clientRoot, templateHtml);
|
|
1442
|
+
const relativeBasePath = path2.relative(baseClientRoot, clientRoot).replace(/\\/g, "/");
|
|
1443
|
+
const adjustedRelativePath = relativeBasePath ? `/${relativeBasePath}` : "";
|
|
1444
|
+
if (!isDevelopment) {
|
|
1445
|
+
try {
|
|
1446
|
+
const manifestPath = path2.join(clientRoot, ".vite/manifest.json");
|
|
1447
|
+
const manifestContent = await readFile(manifestPath, "utf-8");
|
|
1448
|
+
const manifest = JSON.parse(manifestContent);
|
|
1449
|
+
manifests.set(clientRoot, manifest);
|
|
1450
|
+
const ssrManifestPath = path2.join(clientRoot, ".vite/ssr-manifest.json");
|
|
1451
|
+
const ssrManifestContent = await readFile(ssrManifestPath, "utf-8");
|
|
1452
|
+
const ssrManifest = JSON.parse(ssrManifestContent);
|
|
1453
|
+
ssrManifests.set(clientRoot, ssrManifest);
|
|
1454
|
+
const entryClientFile = manifest[`${entryClient}.tsx`]?.file;
|
|
1455
|
+
if (!entryClientFile) {
|
|
1456
|
+
throw AppError.internal(`Entry client file not found in manifest for ${entryClient}.tsx`, {
|
|
1457
|
+
details: {
|
|
1458
|
+
clientRoot,
|
|
1459
|
+
entryClient,
|
|
1460
|
+
availableKeys: Object.keys(manifest)
|
|
1334
1461
|
}
|
|
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
1462
|
});
|
|
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
1463
|
}
|
|
1464
|
+
const bootstrapModule = `/${adjustedRelativePath}/${entryClientFile}`.replace(/\/{2,}/g, "/");
|
|
1465
|
+
bootstrapModules.set(clientRoot, bootstrapModule);
|
|
1466
|
+
const preloadLink = renderPreloadLinks(ssrManifest, adjustedRelativePath);
|
|
1467
|
+
preloadLinks.set(clientRoot, preloadLink);
|
|
1468
|
+
const cssLink = getCssLinks(manifest, adjustedRelativePath);
|
|
1469
|
+
cssLinks.set(clientRoot, cssLink);
|
|
1470
|
+
const renderModulePath = path2.join(clientRoot, `${entryServer}.js`);
|
|
1471
|
+
const moduleUrl = pathToFileURL(renderModulePath).href;
|
|
1472
|
+
try {
|
|
1473
|
+
const importedModule = await import(moduleUrl);
|
|
1474
|
+
renderModules.set(clientRoot, importedModule);
|
|
1475
|
+
} catch (err) {
|
|
1476
|
+
throw AppError.internal(`Failed to load render module ${renderModulePath}`, {
|
|
1477
|
+
cause: err,
|
|
1478
|
+
details: { moduleUrl, clientRoot, entryServer }
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
} catch (err) {
|
|
1482
|
+
if (err instanceof AppError) {
|
|
1483
|
+
logger.error(
|
|
1484
|
+
{
|
|
1485
|
+
error: { name: err.name, message: err.message, stack: err.stack, code: err.code },
|
|
1486
|
+
stage: "loadAssets:production"
|
|
1487
|
+
},
|
|
1488
|
+
"Asset load failed"
|
|
1489
|
+
);
|
|
1490
|
+
} else {
|
|
1491
|
+
logger.error(
|
|
1492
|
+
{
|
|
1493
|
+
error: err instanceof Error ? { name: err.name, message: err.message, stack: err.stack } : String(err),
|
|
1494
|
+
stage: "loadAssets:production"
|
|
1495
|
+
},
|
|
1496
|
+
"Asset load failed"
|
|
1497
|
+
);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
} else {
|
|
1501
|
+
const bootstrapModule = `/${adjustedRelativePath}/${entryClient}`.replace(/\/{2,}/g, "/");
|
|
1502
|
+
bootstrapModules.set(clientRoot, bootstrapModule);
|
|
1503
|
+
}
|
|
1504
|
+
} catch (err) {
|
|
1505
|
+
logger.error(
|
|
1506
|
+
{
|
|
1507
|
+
error: err instanceof Error ? { name: err.name, message: err.message, stack: err.stack } : String(err),
|
|
1508
|
+
stage: "loadAssets:config"
|
|
1355
1509
|
},
|
|
1356
|
-
|
|
1357
|
-
req.url,
|
|
1358
|
-
shouldHydrate ? bootstrapModule : void 0,
|
|
1359
|
-
attr?.meta,
|
|
1360
|
-
cspNonce,
|
|
1361
|
-
ac.signal,
|
|
1362
|
-
{ logger: reqLogger }
|
|
1510
|
+
"Failed to process config"
|
|
1363
1511
|
);
|
|
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
1512
|
}
|
|
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
1513
|
}
|
|
1383
1514
|
};
|
|
1384
1515
|
|
|
1385
|
-
// src/utils/
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
context: {
|
|
1516
|
+
// src/utils/DevServer.ts
|
|
1517
|
+
import path3 from "path";
|
|
1518
|
+
var setupDevServer = async (app, baseClientRoot, alias, debug, devNet) => {
|
|
1519
|
+
const logger = createLogger({
|
|
1520
|
+
context: { service: "setupDevServer" },
|
|
1521
|
+
debug,
|
|
1522
|
+
minLevel: "debug"
|
|
1390
1523
|
});
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1524
|
+
const host = devNet?.host ?? process.env.HOST?.trim() ?? process.env.FASTIFY_ADDRESS?.trim() ?? "localhost";
|
|
1525
|
+
const hmrPort = devNet?.hmrPort ?? (Number(process.env.HMR_PORT) || 5174);
|
|
1526
|
+
const { createServer: createServer2 } = await import("vite");
|
|
1527
|
+
const viteDevServer = await createServer2({
|
|
1528
|
+
appType: "custom",
|
|
1529
|
+
css: {
|
|
1530
|
+
preprocessorOptions: {
|
|
1531
|
+
scss: {
|
|
1532
|
+
api: "modern-compiler"
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
},
|
|
1536
|
+
mode: "development",
|
|
1537
|
+
plugins: [
|
|
1538
|
+
...debug ? [
|
|
1539
|
+
{
|
|
1540
|
+
name: "\u03C4js-development-server-debug-logging",
|
|
1541
|
+
configureServer(server) {
|
|
1542
|
+
logger.debug("vite", `${CONTENT.TAG} Development server debug started`);
|
|
1543
|
+
server.middlewares.use((req, res, next) => {
|
|
1544
|
+
logger.debug(
|
|
1545
|
+
"vite",
|
|
1546
|
+
{
|
|
1547
|
+
method: req.method,
|
|
1548
|
+
url: req.url,
|
|
1549
|
+
host: req.headers.host,
|
|
1550
|
+
ua: req.headers["user-agent"]
|
|
1551
|
+
},
|
|
1552
|
+
"\u2190 rx"
|
|
1553
|
+
);
|
|
1554
|
+
res.on("finish", () => {
|
|
1555
|
+
logger.debug(
|
|
1556
|
+
"vite",
|
|
1557
|
+
{
|
|
1558
|
+
method: req.method,
|
|
1559
|
+
url: req.url,
|
|
1560
|
+
statusCode: res.statusCode
|
|
1561
|
+
},
|
|
1562
|
+
"\u2192 tx"
|
|
1563
|
+
);
|
|
1564
|
+
});
|
|
1565
|
+
next();
|
|
1566
|
+
});
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
] : []
|
|
1570
|
+
],
|
|
1571
|
+
resolve: {
|
|
1572
|
+
alias: {
|
|
1573
|
+
"@client": path3.resolve(baseClientRoot),
|
|
1574
|
+
"@server": path3.resolve(__dirname),
|
|
1575
|
+
"@shared": path3.resolve(__dirname, "../shared"),
|
|
1576
|
+
...alias
|
|
1577
|
+
}
|
|
1578
|
+
},
|
|
1579
|
+
root: baseClientRoot,
|
|
1580
|
+
server: {
|
|
1581
|
+
middlewareMode: true,
|
|
1582
|
+
hmr: {
|
|
1583
|
+
clientPort: hmrPort,
|
|
1584
|
+
host: host !== "localhost" ? host : void 0,
|
|
1585
|
+
port: hmrPort,
|
|
1586
|
+
protocol: "ws"
|
|
1587
|
+
}
|
|
1395
1588
|
}
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1589
|
+
});
|
|
1590
|
+
overrideCSSHMRConsoleError();
|
|
1591
|
+
app.addHook("onRequest", async (request, reply) => {
|
|
1592
|
+
await new Promise((resolve) => {
|
|
1593
|
+
viteDevServer.middlewares(request.raw, reply.raw, () => {
|
|
1594
|
+
if (!reply.sent) resolve();
|
|
1401
1595
|
});
|
|
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
1596
|
});
|
|
1415
|
-
|
|
1416
|
-
|
|
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
|
-
}
|
|
1597
|
+
});
|
|
1598
|
+
return viteDevServer;
|
|
1433
1599
|
};
|
|
1434
1600
|
|
|
1435
|
-
// src/
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1601
|
+
// src/utils/HandleRender.ts
|
|
1602
|
+
import path4 from "path";
|
|
1603
|
+
import { PassThrough } from "stream";
|
|
1604
|
+
|
|
1605
|
+
// src/utils/Telemetry.ts
|
|
1606
|
+
import crypto2 from "crypto";
|
|
1607
|
+
function createRequestContext(req, reply, baseLogger) {
|
|
1608
|
+
const raw = typeof req.headers["x-trace-id"] === "string" ? req.headers["x-trace-id"] : "";
|
|
1609
|
+
const traceId = raw && REGEX.SAFE_TRACE.test(raw) ? raw : typeof req.id === "string" ? req.id : crypto2.randomUUID();
|
|
1610
|
+
reply.header("x-trace-id", traceId);
|
|
1611
|
+
const anyLogger = baseLogger;
|
|
1612
|
+
const child = anyLogger.child;
|
|
1613
|
+
const logger = typeof child === "function" ? child.call(baseLogger, { traceId, url: req.url, method: req.method }) : baseLogger;
|
|
1614
|
+
const headers = Object.fromEntries(
|
|
1615
|
+
Object.entries(req.headers).map(([headerName, headerValue]) => {
|
|
1616
|
+
const normalisedValue = Array.isArray(headerValue) ? headerValue.join(",") : headerValue ?? "";
|
|
1617
|
+
return [headerName, normalisedValue];
|
|
1618
|
+
})
|
|
1619
|
+
);
|
|
1620
|
+
return { traceId, logger, headers };
|
|
1445
1621
|
}
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
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
|
-
}
|
|
1622
|
+
|
|
1623
|
+
// src/utils/HandleRender.ts
|
|
1624
|
+
var handleRender = async (req, reply, routeMatchers, processedConfigs, serviceRegistry, maps, opts = {}) => {
|
|
1625
|
+
const { viteDevServer } = opts;
|
|
1626
|
+
const logger = opts.logger ?? createLogger({
|
|
1627
|
+
debug: opts.debug,
|
|
1628
|
+
minLevel: isDevelopment ? "debug" : "info",
|
|
1629
|
+
includeContext: true,
|
|
1630
|
+
includeStack: (lvl) => lvl === "error" || isDevelopment
|
|
1465
1631
|
});
|
|
1466
|
-
}
|
|
1467
|
-
var processCSPReport = (body, context, logger) => {
|
|
1468
1632
|
try {
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1633
|
+
if (/\.\w+$/.test(req.raw.url ?? "")) return reply.callNotFound();
|
|
1634
|
+
const url = req.url ? new URL(req.url, `http://${req.headers.host}`).pathname : "/";
|
|
1635
|
+
const matchedRoute = matchRoute(url, routeMatchers);
|
|
1636
|
+
const rawNonce = req.cspNonce;
|
|
1637
|
+
const cspNonce = rawNonce && rawNonce.length > 0 ? rawNonce : void 0;
|
|
1638
|
+
if (!matchedRoute) {
|
|
1639
|
+
reply.callNotFound();
|
|
1475
1640
|
return;
|
|
1476
1641
|
}
|
|
1477
|
-
const
|
|
1478
|
-
const
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1642
|
+
const { route, params } = matchedRoute;
|
|
1643
|
+
const { attr, appId } = route;
|
|
1644
|
+
const config = processedConfigs.find((c) => c.appId === appId);
|
|
1645
|
+
if (!config) {
|
|
1646
|
+
throw AppError.internal("No configuration found for the request", {
|
|
1647
|
+
details: {
|
|
1648
|
+
appId,
|
|
1649
|
+
availableAppIds: processedConfigs.map((c) => c.appId),
|
|
1650
|
+
url
|
|
1651
|
+
}
|
|
1484
1652
|
});
|
|
1485
|
-
return;
|
|
1486
1653
|
}
|
|
1487
|
-
const
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
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
|
-
};
|
|
1654
|
+
const { clientRoot, entryServer } = config;
|
|
1655
|
+
let template = ensureNonNull(maps.templates.get(clientRoot), `Template not found for clientRoot: ${clientRoot}`);
|
|
1656
|
+
const bootstrapModule = maps.bootstrapModules.get(clientRoot);
|
|
1657
|
+
const cssLink = maps.cssLinks.get(clientRoot);
|
|
1658
|
+
const manifest = maps.manifests.get(clientRoot);
|
|
1659
|
+
const preloadLink = maps.preloadLinks.get(clientRoot);
|
|
1660
|
+
const ssrManifest = maps.ssrManifests.get(clientRoot);
|
|
1661
|
+
let renderModule;
|
|
1662
|
+
if (isDevelopment && viteDevServer) {
|
|
1526
1663
|
try {
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1664
|
+
template = template.replace(/<script type="module" src="\/@vite\/client"><\/script>/g, "");
|
|
1665
|
+
template = template.replace(/<style type="text\/css">[\s\S]*?<\/style>/g, "");
|
|
1666
|
+
const entryServerPath = path4.join(clientRoot, `${entryServer}.tsx`);
|
|
1667
|
+
const executedModule = await viteDevServer.ssrLoadModule(entryServerPath);
|
|
1668
|
+
renderModule = executedModule;
|
|
1669
|
+
const styles = await collectStyle(viteDevServer, [entryServerPath]);
|
|
1670
|
+
const styleNonce = cspNonce ? ` nonce="${cspNonce}"` : "";
|
|
1671
|
+
template = template?.replace("</head>", `<style type="text/css"${styleNonce}>${styles}</style></head>`);
|
|
1672
|
+
template = await viteDevServer.transformIndexHtml(url, template);
|
|
1673
|
+
} catch (error) {
|
|
1674
|
+
throw AppError.internal("Failed to load dev assets", { cause: error, details: { clientRoot, entryServer, url } });
|
|
1675
|
+
}
|
|
1676
|
+
} else {
|
|
1677
|
+
renderModule = maps.renderModules.get(clientRoot);
|
|
1678
|
+
if (!renderModule) throw AppError.internal(`Render module not found for clientRoot: ${clientRoot}. Module should have been preloaded.`);
|
|
1679
|
+
}
|
|
1680
|
+
const renderType = attr?.render ?? RENDERTYPE.ssr;
|
|
1681
|
+
const templateParts = processTemplate(template);
|
|
1682
|
+
const baseLogger = opts.logger ?? logger;
|
|
1683
|
+
const { traceId, logger: reqLogger, headers } = createRequestContext(req, reply, baseLogger);
|
|
1684
|
+
const ctx = { traceId, logger: reqLogger, headers };
|
|
1685
|
+
const initialDataInput = () => fetchInitialData(attr, params, serviceRegistry, ctx);
|
|
1686
|
+
if (renderType === RENDERTYPE.ssr) {
|
|
1687
|
+
const { renderSSR } = renderModule;
|
|
1688
|
+
if (!renderSSR) {
|
|
1689
|
+
throw AppError.internal("renderSSR function not found in module", {
|
|
1690
|
+
details: { clientRoot, availableFunctions: Object.keys(renderModule) }
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
const ac = new AbortController();
|
|
1694
|
+
const onAborted = () => ac.abort("client_aborted");
|
|
1695
|
+
req.raw.on("aborted", onAborted);
|
|
1696
|
+
reply.raw.on("close", () => {
|
|
1697
|
+
if (!reply.raw.writableEnded) ac.abort("socket_closed");
|
|
1698
|
+
});
|
|
1699
|
+
reply.raw.on("finish", () => req.raw.off("aborted", onAborted));
|
|
1700
|
+
if (ac.signal.aborted) {
|
|
1701
|
+
logger.warn("SSR skipped; already aborted", { url: req.url });
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
const initialDataResolved = await initialDataInput();
|
|
1705
|
+
let headContent = "";
|
|
1706
|
+
let appHtml = "";
|
|
1707
|
+
try {
|
|
1708
|
+
const res = await renderSSR(initialDataResolved, req.url, attr?.meta, ac.signal, { logger: reqLogger });
|
|
1709
|
+
headContent = res.headContent;
|
|
1710
|
+
appHtml = res.appHtml;
|
|
1711
|
+
} catch (err) {
|
|
1712
|
+
const msg = String(err?.message ?? err ?? "");
|
|
1713
|
+
const benign = REGEX.BENIGN_NET_ERR.test(msg);
|
|
1714
|
+
if (ac.signal.aborted || benign) {
|
|
1715
|
+
logger.warn("SSR aborted mid-render (benign)", { url: req.url, reason: msg });
|
|
1716
|
+
return;
|
|
1546
1717
|
}
|
|
1718
|
+
logger.error("SSR render failed", { url: req.url, error: normaliseError(err) });
|
|
1719
|
+
throw err;
|
|
1720
|
+
}
|
|
1721
|
+
let aggregateHeadContent = headContent;
|
|
1722
|
+
if (ssrManifest && preloadLink) aggregateHeadContent += preloadLink;
|
|
1723
|
+
if (manifest && cssLink) aggregateHeadContent += cssLink;
|
|
1724
|
+
const shouldHydrate = attr?.hydrate !== false;
|
|
1725
|
+
const nonceAttr = cspNonce ? ` nonce="${cspNonce}"` : "";
|
|
1726
|
+
const initialDataScript = `<script${nonceAttr}>window.__INITIAL_DATA__ = ${JSON.stringify(initialDataResolved).replace(/</g, "\\u003c")};</script>`;
|
|
1727
|
+
const bootstrapScriptTag = shouldHydrate && bootstrapModule ? `<script${nonceAttr} type="module" src="${bootstrapModule}" defer></script>` : "";
|
|
1728
|
+
const safeAppHtml = appHtml.trim();
|
|
1729
|
+
const fullHtml = rebuildTemplate(templateParts, aggregateHeadContent, `${safeAppHtml}${initialDataScript}${bootstrapScriptTag}`);
|
|
1730
|
+
try {
|
|
1731
|
+
return reply.status(200).header("Content-Type", "text/html").send(fullHtml);
|
|
1547
1732
|
} catch (err) {
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
});
|
|
1733
|
+
const msg = String(err?.message ?? err ?? "");
|
|
1734
|
+
const benign = REGEX.BENIGN_NET_ERR.test(msg);
|
|
1735
|
+
if (!benign) logger.error("SSR send failed", { url: req.url, error: normaliseError(err) });
|
|
1736
|
+
else logger.warn("SSR send aborted (benign)", { url: req.url, reason: msg });
|
|
1737
|
+
return;
|
|
1551
1738
|
}
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
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
|
|
1739
|
+
} else {
|
|
1740
|
+
const { renderStream } = renderModule;
|
|
1741
|
+
if (!renderStream) {
|
|
1742
|
+
throw AppError.internal("renderStream function not found in module", {
|
|
1743
|
+
details: { clientRoot, availableFunctions: Object.keys(renderModule) }
|
|
1744
|
+
});
|
|
1586
1745
|
}
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
root: baseClientRoot,
|
|
1592
|
-
prefix: "/",
|
|
1593
|
-
index: false,
|
|
1594
|
-
wildcard: false,
|
|
1595
|
-
...options ?? {}
|
|
1746
|
+
const cspHeader = reply.getHeader("Content-Security-Policy");
|
|
1747
|
+
reply.raw.writeHead(200, {
|
|
1748
|
+
"Content-Security-Policy": cspHeader,
|
|
1749
|
+
"Content-Type": "text/html; charset=utf-8"
|
|
1596
1750
|
});
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1751
|
+
const ac = new AbortController();
|
|
1752
|
+
const onAborted = () => ac.abort();
|
|
1753
|
+
req.raw.on("aborted", onAborted);
|
|
1754
|
+
reply.raw.on("close", () => {
|
|
1755
|
+
if (!reply.raw.writableEnded) ac.abort();
|
|
1756
|
+
});
|
|
1757
|
+
reply.raw.on("finish", () => req.raw.off("aborted", onAborted));
|
|
1758
|
+
const shouldHydrate = attr?.hydrate !== false;
|
|
1759
|
+
const abortedState = { aborted: false };
|
|
1760
|
+
const isBenignSocketAbort = (e) => {
|
|
1761
|
+
const msg = String(e?.message ?? e ?? "");
|
|
1762
|
+
return REGEX.BENIGN_NET_ERR.test(msg);
|
|
1763
|
+
};
|
|
1764
|
+
const writable = new PassThrough();
|
|
1765
|
+
writable.on("error", (err) => {
|
|
1766
|
+
if (!isBenignSocketAbort(err)) logger.error("PassThrough error:", { error: err });
|
|
1604
1767
|
});
|
|
1605
|
-
|
|
1606
|
-
|
|
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));
|
|
1614
|
-
app.get("/*", async (req, reply) => {
|
|
1615
|
-
await handleRender(req, reply, routeMatchers, processedConfigs, serviceRegistry, maps, {
|
|
1616
|
-
debug: opts.debug,
|
|
1617
|
-
logger,
|
|
1618
|
-
viteDevServer
|
|
1768
|
+
reply.raw.on("error", (err) => {
|
|
1769
|
+
if (!isBenignSocketAbort(err)) logger.error("HTTP socket error:", { error: err });
|
|
1619
1770
|
});
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
reply,
|
|
1625
|
-
processedConfigs,
|
|
1771
|
+
writable.pipe(reply.raw, { end: false });
|
|
1772
|
+
let finalData = void 0;
|
|
1773
|
+
renderStream(
|
|
1774
|
+
writable,
|
|
1626
1775
|
{
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1776
|
+
onHead: (headContent) => {
|
|
1777
|
+
let aggregateHeadContent = headContent;
|
|
1778
|
+
if (ssrManifest && preloadLink) aggregateHeadContent += preloadLink;
|
|
1779
|
+
if (manifest && cssLink) aggregateHeadContent += cssLink;
|
|
1780
|
+
return reply.raw.write(`${templateParts.beforeHead}${aggregateHeadContent}${templateParts.afterHead}${templateParts.beforeBody}`);
|
|
1781
|
+
},
|
|
1782
|
+
onShellReady: () => {
|
|
1783
|
+
},
|
|
1784
|
+
onAllReady: (data) => {
|
|
1785
|
+
if (!abortedState.aborted) finalData = data;
|
|
1786
|
+
},
|
|
1787
|
+
onError: (err) => {
|
|
1788
|
+
if (abortedState.aborted || isBenignSocketAbort(err)) {
|
|
1789
|
+
logger.warn("Client disconnected before stream finished");
|
|
1790
|
+
try {
|
|
1791
|
+
if (!reply.raw.writableEnded && !reply.raw.destroyed) reply.raw.destroy();
|
|
1792
|
+
} catch (e) {
|
|
1793
|
+
logger.debug?.("stream teardown: destroy() failed", { error: normaliseError(e) });
|
|
1794
|
+
}
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1797
|
+
abortedState.aborted = true;
|
|
1798
|
+
logger.error("Critical rendering error during stream", {
|
|
1799
|
+
error: normaliseError(err),
|
|
1800
|
+
clientRoot,
|
|
1801
|
+
url: req.url
|
|
1802
|
+
});
|
|
1803
|
+
try {
|
|
1804
|
+
ac?.abort?.();
|
|
1805
|
+
} catch (e) {
|
|
1806
|
+
logger.debug?.("stream teardown: abort() failed", { error: normaliseError(e) });
|
|
1807
|
+
}
|
|
1808
|
+
const reason = toReason(err);
|
|
1809
|
+
try {
|
|
1810
|
+
if (!reply.raw.writableEnded && !reply.raw.destroyed) reply.raw.destroy(reason);
|
|
1811
|
+
} catch (e) {
|
|
1812
|
+
logger.debug?.("stream teardown: destroy() failed", { error: normaliseError(e) });
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1630
1815
|
},
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1816
|
+
initialDataInput,
|
|
1817
|
+
req.url,
|
|
1818
|
+
shouldHydrate ? bootstrapModule : void 0,
|
|
1819
|
+
attr?.meta,
|
|
1820
|
+
cspNonce,
|
|
1821
|
+
ac.signal,
|
|
1822
|
+
{ logger: reqLogger }
|
|
1635
1823
|
);
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
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 {
|
|
1824
|
+
writable.on("finish", () => {
|
|
1825
|
+
if (abortedState.aborted || reply.raw.writableEnded) return;
|
|
1826
|
+
const data = finalData ?? {};
|
|
1827
|
+
const initialDataScript = `<script${cspNonce ? ` nonce="${cspNonce}"` : ""}>window.__INITIAL_DATA__ = ${JSON.stringify(data).replace(
|
|
1828
|
+
/</g,
|
|
1829
|
+
"\\u003c"
|
|
1830
|
+
)}; window.dispatchEvent(new Event('taujs:data-ready'));</script>`;
|
|
1831
|
+
reply.raw.write(initialDataScript);
|
|
1832
|
+
reply.raw.write(templateParts.afterBody);
|
|
1653
1833
|
reply.raw.end();
|
|
1654
|
-
}
|
|
1655
|
-
});
|
|
1656
|
-
},
|
|
1657
|
-
{ name: "\u03C4js-ssr-server" }
|
|
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);
|
|
1834
|
+
});
|
|
1774
1835
|
}
|
|
1836
|
+
} catch (err) {
|
|
1837
|
+
if (err instanceof AppError) throw err;
|
|
1838
|
+
throw AppError.internal("handleRender failed", err, {
|
|
1839
|
+
url: req.url,
|
|
1840
|
+
route: req.routeOptions?.url
|
|
1841
|
+
});
|
|
1775
1842
|
}
|
|
1776
|
-
}
|
|
1777
|
-
var computeScore = (path6) => {
|
|
1778
|
-
return path6.split("/").filter(Boolean).reduce((score, segment) => score + (segment.startsWith(":") ? 1 : 10), 0);
|
|
1779
1843
|
};
|
|
1780
1844
|
|
|
1781
|
-
// src/
|
|
1782
|
-
var
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
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;
|
|
1845
|
+
// src/utils/HandleNotFound.ts
|
|
1846
|
+
var handleNotFound = async (req, reply, processedConfigs, maps, opts = {}) => {
|
|
1847
|
+
const logger = opts.logger ?? createLogger({
|
|
1848
|
+
debug: opts.debug,
|
|
1849
|
+
context: { component: "handle-not-found", url: req.url, method: req.method, traceId: req.id }
|
|
1850
|
+
});
|
|
1851
|
+
try {
|
|
1852
|
+
if (/\.\w+$/.test(req.raw.url ?? "")) {
|
|
1853
|
+
logger.debug?.("ssr", { url: req.raw.url }, "Delegating asset-like request to Fastify notFound handler");
|
|
1854
|
+
return reply.callNotFound();
|
|
1818
1855
|
}
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1856
|
+
const defaultConfig = processedConfigs[0];
|
|
1857
|
+
if (!defaultConfig) {
|
|
1858
|
+
logger.error?.({ configCount: processedConfigs.length, url: req.raw.url }, "No default configuration found");
|
|
1859
|
+
throw AppError.internal("No default configuration found", {
|
|
1860
|
+
details: { configCount: processedConfigs.length, url: req.raw.url }
|
|
1861
|
+
});
|
|
1823
1862
|
}
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1863
|
+
const { clientRoot } = defaultConfig;
|
|
1864
|
+
const cspNonce = req.cspNonce ?? void 0;
|
|
1865
|
+
const template = ensureNonNull(maps.templates.get(clientRoot), `Template not found for clientRoot: ${clientRoot}`);
|
|
1866
|
+
const cssLink = maps.cssLinks.get(clientRoot);
|
|
1867
|
+
const bootstrapModule = maps.bootstrapModules.get(clientRoot);
|
|
1868
|
+
logger.debug?.(
|
|
1869
|
+
"ssr",
|
|
1870
|
+
{
|
|
1871
|
+
clientRoot,
|
|
1872
|
+
hasCssLink: !!cssLink,
|
|
1873
|
+
hasBootstrapModule: !!bootstrapModule,
|
|
1874
|
+
isDevelopment,
|
|
1875
|
+
hasCspNonce: !!cspNonce
|
|
1876
|
+
},
|
|
1877
|
+
"Preparing not-found fallback HTML"
|
|
1878
|
+
);
|
|
1879
|
+
let processedTemplate = template.replace(SSRTAG.ssrHead, "").replace(SSRTAG.ssrHtml, "");
|
|
1880
|
+
if (!isDevelopment && cssLink) {
|
|
1881
|
+
processedTemplate = processedTemplate.replace("</head>", `${cssLink}</head>`);
|
|
1830
1882
|
}
|
|
1831
|
-
|
|
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
|
-
}
|
|
1883
|
+
if (bootstrapModule) {
|
|
1884
|
+
const nonceAttr = cspNonce ? ` nonce="${cspNonce}"` : "";
|
|
1885
|
+
processedTemplate = processedTemplate.replace("</body>", `<script${nonceAttr} type="module" src="${bootstrapModule}" defer></script></body>`);
|
|
1852
1886
|
}
|
|
1887
|
+
logger.debug?.("ssr", { status: 200 }, "Sending not-found fallback HTML");
|
|
1888
|
+
return reply.status(200).type("text/html").send(processedTemplate);
|
|
1889
|
+
} catch (err) {
|
|
1890
|
+
logger.error?.({ error: err, url: req.url, clientRoot: processedConfigs[0]?.clientRoot }, "handleNotFound failed");
|
|
1891
|
+
throw AppError.internal("handleNotFound failed", err, {
|
|
1892
|
+
stage: "handleNotFound",
|
|
1893
|
+
url: req.url,
|
|
1894
|
+
clientRoot: processedConfigs[0]?.clientRoot
|
|
1895
|
+
});
|
|
1853
1896
|
}
|
|
1854
|
-
|
|
1897
|
+
};
|
|
1898
|
+
|
|
1899
|
+
// src/utils/StaticAssets.ts
|
|
1900
|
+
function normalizeStaticAssets(reg) {
|
|
1901
|
+
if (!reg) return [];
|
|
1902
|
+
return Array.isArray(reg) ? reg : [reg];
|
|
1855
1903
|
}
|
|
1856
|
-
function
|
|
1857
|
-
|
|
1858
|
-
|
|
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 };
|
|
1904
|
+
function prefixWeight(prefix) {
|
|
1905
|
+
if (typeof prefix !== "string" || prefix === "/" || prefix.length === 0) return 0;
|
|
1906
|
+
return prefix.split("/").filter(Boolean).length;
|
|
1878
1907
|
}
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1908
|
+
async function registerStaticAssets(app, baseClientRoot, reg, defaults) {
|
|
1909
|
+
const entries = normalizeStaticAssets(reg).map(({ plugin, options }) => ({
|
|
1910
|
+
plugin,
|
|
1911
|
+
options: {
|
|
1912
|
+
root: baseClientRoot,
|
|
1913
|
+
prefix: "/",
|
|
1914
|
+
index: false,
|
|
1915
|
+
wildcard: false,
|
|
1916
|
+
...defaults ?? {},
|
|
1917
|
+
...options ?? {}
|
|
1918
|
+
}
|
|
1919
|
+
}));
|
|
1920
|
+
entries.sort((a, b) => prefixWeight(b.options?.prefix) - prefixWeight(a.options?.prefix));
|
|
1921
|
+
for (const { plugin, options } of entries) {
|
|
1922
|
+
await app.register(plugin, options);
|
|
1886
1923
|
}
|
|
1887
|
-
return custom > 0 ? `Loaded development defaults with ${custom} route override(s)` : "Loaded development defaults";
|
|
1888
1924
|
}
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
}
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
status = "warning";
|
|
1919
|
-
tail = " (consider adding global CSP for production)";
|
|
1925
|
+
|
|
1926
|
+
// src/SSRServer.ts
|
|
1927
|
+
var SSRServer = (0, import_fastify_plugin3.default)(
|
|
1928
|
+
async (app, opts) => {
|
|
1929
|
+
const { alias, configs, routes, serviceRegistry, clientRoot: baseClientRoot, security } = opts;
|
|
1930
|
+
const logger = createLogger({
|
|
1931
|
+
debug: opts.debug,
|
|
1932
|
+
context: { component: "ssr-server" },
|
|
1933
|
+
minLevel: process.env.NODE_ENV === "production" ? "info" : "debug",
|
|
1934
|
+
includeContext: true,
|
|
1935
|
+
singleLine: true
|
|
1936
|
+
});
|
|
1937
|
+
const maps = createMaps();
|
|
1938
|
+
const processedConfigs = processConfigs(configs, baseClientRoot, TEMPLATE);
|
|
1939
|
+
const routeMatchers = createRouteMatchers(routes);
|
|
1940
|
+
let viteDevServer;
|
|
1941
|
+
await loadAssets(
|
|
1942
|
+
processedConfigs,
|
|
1943
|
+
baseClientRoot,
|
|
1944
|
+
maps.bootstrapModules,
|
|
1945
|
+
maps.cssLinks,
|
|
1946
|
+
maps.manifests,
|
|
1947
|
+
maps.preloadLinks,
|
|
1948
|
+
maps.renderModules,
|
|
1949
|
+
maps.ssrManifests,
|
|
1950
|
+
maps.templates,
|
|
1951
|
+
{
|
|
1952
|
+
debug: opts.debug,
|
|
1953
|
+
logger
|
|
1920
1954
|
}
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
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)`
|
|
1955
|
+
);
|
|
1956
|
+
if (opts.staticAssets) await registerStaticAssets(app, baseClientRoot, opts.staticAssets);
|
|
1957
|
+
if (security?.csp?.reporting) {
|
|
1958
|
+
app.register(cspReportPlugin, {
|
|
1959
|
+
path: security.csp.reporting.endpoint,
|
|
1960
|
+
debug: opts.debug,
|
|
1961
|
+
logger,
|
|
1962
|
+
onViolation: security.csp.reporting.onViolation
|
|
1938
1963
|
});
|
|
1939
1964
|
}
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1965
|
+
app.register(cspPlugin, {
|
|
1966
|
+
directives: opts.security?.csp?.directives,
|
|
1967
|
+
generateCSP: opts.security?.csp?.generateCSP,
|
|
1968
|
+
routeMatchers,
|
|
1969
|
+
debug: opts.debug
|
|
1970
|
+
});
|
|
1971
|
+
if (isDevelopment) viteDevServer = await setupDevServer(app, baseClientRoot, alias, opts.debug, opts.devNet);
|
|
1972
|
+
app.addHook("onRequest", createAuthHook(routeMatchers, logger));
|
|
1973
|
+
app.get("/*", async (req, reply) => {
|
|
1974
|
+
await handleRender(req, reply, routeMatchers, processedConfigs, serviceRegistry, maps, {
|
|
1975
|
+
debug: opts.debug,
|
|
1976
|
+
logger,
|
|
1977
|
+
viteDevServer
|
|
1978
|
+
});
|
|
1979
|
+
});
|
|
1980
|
+
app.setNotFoundHandler(async (req, reply) => {
|
|
1981
|
+
await handleNotFound(
|
|
1982
|
+
req,
|
|
1983
|
+
reply,
|
|
1984
|
+
processedConfigs,
|
|
1985
|
+
{
|
|
1986
|
+
cssLinks: maps.cssLinks,
|
|
1987
|
+
bootstrapModules: maps.bootstrapModules,
|
|
1988
|
+
templates: maps.templates
|
|
1989
|
+
},
|
|
1990
|
+
{
|
|
1991
|
+
debug: opts.debug,
|
|
1992
|
+
logger
|
|
1993
|
+
}
|
|
1994
|
+
);
|
|
1995
|
+
});
|
|
1996
|
+
app.setErrorHandler((err, req, reply) => {
|
|
1997
|
+
const e = AppError.from(err);
|
|
1998
|
+
const alreadyLogged = !!e?.details && e.details && e.details.logged;
|
|
1999
|
+
if (!alreadyLogged) {
|
|
2000
|
+
logger.error(
|
|
2001
|
+
{
|
|
2002
|
+
kind: e.kind,
|
|
2003
|
+
httpStatus: e.httpStatus,
|
|
2004
|
+
...e.code ? { code: e.code } : {},
|
|
2005
|
+
...e.details ? { details: e.details } : {},
|
|
2006
|
+
method: req.method,
|
|
2007
|
+
url: req.url,
|
|
2008
|
+
route: req.routeOptions?.url,
|
|
2009
|
+
stack: e.stack
|
|
2010
|
+
},
|
|
2011
|
+
e.message
|
|
2012
|
+
);
|
|
2013
|
+
}
|
|
2014
|
+
if (!reply.raw.headersSent) {
|
|
2015
|
+
const { status, body } = toHttp(e);
|
|
2016
|
+
reply.status(status).send(body);
|
|
2017
|
+
} else {
|
|
2018
|
+
reply.raw.end();
|
|
2019
|
+
}
|
|
2020
|
+
});
|
|
2021
|
+
},
|
|
2022
|
+
{ name: "\u03C4js-ssr-server" }
|
|
2023
|
+
);
|
|
1943
2024
|
|
|
1944
2025
|
// src/CreateServer.ts
|
|
1945
2026
|
var createServer = async (opts) => {
|
|
@@ -1988,17 +2069,20 @@ var createServer = async (opts) => {
|
|
|
1988
2069
|
configs,
|
|
1989
2070
|
routes,
|
|
1990
2071
|
serviceRegistry: opts.serviceRegistry,
|
|
1991
|
-
|
|
2072
|
+
staticAssets: opts.staticAssets !== void 0 ? opts.staticAssets : { plugin: fastifyStatic },
|
|
1992
2073
|
debug: opts.debug,
|
|
1993
2074
|
alias: opts.alias,
|
|
1994
2075
|
security,
|
|
1995
2076
|
devNet: { host: net.host, hmrPort: net.hmrPort }
|
|
1996
2077
|
});
|
|
1997
2078
|
} catch (err) {
|
|
1998
|
-
logger.error(
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2079
|
+
logger.error(
|
|
2080
|
+
{
|
|
2081
|
+
step: "register:SSRServer",
|
|
2082
|
+
error: normaliseError(err)
|
|
2083
|
+
},
|
|
2084
|
+
"Failed to register SSRServer"
|
|
2085
|
+
);
|
|
2002
2086
|
}
|
|
2003
2087
|
const t1 = performance2.now();
|
|
2004
2088
|
console.log(`
|
|
@@ -2007,8 +2091,90 @@ ${import_picocolors4.default.bgGreen(import_picocolors4.default.black(` ${CONTEN
|
|
|
2007
2091
|
if (opts.fastify) return { net };
|
|
2008
2092
|
return { app, net };
|
|
2009
2093
|
};
|
|
2094
|
+
|
|
2095
|
+
// src/Build.ts
|
|
2096
|
+
import path6 from "path";
|
|
2097
|
+
import { build } from "vite";
|
|
2098
|
+
import { nodePolyfills } from "vite-plugin-node-polyfills";
|
|
2099
|
+
async function taujsBuild({
|
|
2100
|
+
config,
|
|
2101
|
+
projectRoot,
|
|
2102
|
+
clientBaseDir,
|
|
2103
|
+
isSSRBuild = process.env.BUILD_MODE === "ssr"
|
|
2104
|
+
}) {
|
|
2105
|
+
const deleteDist = async () => {
|
|
2106
|
+
const { rm } = await import("fs/promises");
|
|
2107
|
+
const distPath = path6.resolve(projectRoot, "dist");
|
|
2108
|
+
try {
|
|
2109
|
+
await rm(distPath, { recursive: true, force: true });
|
|
2110
|
+
console.log("Deleted the dist directory\n");
|
|
2111
|
+
} catch (err) {
|
|
2112
|
+
console.error("Error deleting dist directory:", err);
|
|
2113
|
+
}
|
|
2114
|
+
};
|
|
2115
|
+
const extractedConfigs = extractBuildConfigs(config);
|
|
2116
|
+
const processedConfigs = processConfigs(extractedConfigs, clientBaseDir, TEMPLATE);
|
|
2117
|
+
if (!isSSRBuild) await deleteDist();
|
|
2118
|
+
for (const config2 of processedConfigs) {
|
|
2119
|
+
const { appId, entryPoint, clientRoot, entryClient, entryServer, htmlTemplate, plugins = [] } = config2;
|
|
2120
|
+
const outDir = path6.resolve(projectRoot, `dist/client/${entryPoint}`);
|
|
2121
|
+
const root = entryPoint ? path6.resolve(clientBaseDir, entryPoint) : clientBaseDir;
|
|
2122
|
+
const server = path6.resolve(clientRoot, `${entryServer}.tsx`);
|
|
2123
|
+
const client = path6.resolve(clientRoot, `${entryClient}.tsx`);
|
|
2124
|
+
const main = path6.resolve(clientRoot, htmlTemplate);
|
|
2125
|
+
const viteConfig = {
|
|
2126
|
+
base: entryPoint ? `/${entryPoint}/` : "/",
|
|
2127
|
+
build: {
|
|
2128
|
+
outDir,
|
|
2129
|
+
manifest: !isSSRBuild,
|
|
2130
|
+
rollupOptions: {
|
|
2131
|
+
input: isSSRBuild ? { server } : { client, main }
|
|
2132
|
+
},
|
|
2133
|
+
ssr: isSSRBuild ? server : void 0,
|
|
2134
|
+
ssrManifest: isSSRBuild,
|
|
2135
|
+
...isSSRBuild && {
|
|
2136
|
+
format: "esm",
|
|
2137
|
+
target: `node${process.versions.node.split(".").map(Number)[0]}`
|
|
2138
|
+
}
|
|
2139
|
+
},
|
|
2140
|
+
css: {
|
|
2141
|
+
preprocessorOptions: {
|
|
2142
|
+
scss: { api: "modern-compiler" }
|
|
2143
|
+
}
|
|
2144
|
+
},
|
|
2145
|
+
plugins: [...plugins, nodePolyfills({ include: ["fs", "stream"] })],
|
|
2146
|
+
publicDir: "public",
|
|
2147
|
+
resolve: {
|
|
2148
|
+
alias: {
|
|
2149
|
+
"@client": root,
|
|
2150
|
+
"@server": path6.resolve(projectRoot, "src/server"),
|
|
2151
|
+
"@shared": path6.resolve(projectRoot, "src/shared")
|
|
2152
|
+
}
|
|
2153
|
+
},
|
|
2154
|
+
root,
|
|
2155
|
+
server: {
|
|
2156
|
+
proxy: {
|
|
2157
|
+
"/api": {
|
|
2158
|
+
target: "http://localhost:3000",
|
|
2159
|
+
changeOrigin: true,
|
|
2160
|
+
rewrite: (path7) => path7.replace(/^\/api/, "")
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
};
|
|
2165
|
+
try {
|
|
2166
|
+
console.log(`Building for entryPoint: "${entryPoint}" (${appId})`);
|
|
2167
|
+
await build(viteConfig);
|
|
2168
|
+
console.log(`Build complete for entryPoint: "${entryPoint}"
|
|
2169
|
+
`);
|
|
2170
|
+
} catch (error) {
|
|
2171
|
+
console.error(`Error building for entryPoint: "${entryPoint}"
|
|
2172
|
+
`, error);
|
|
2173
|
+
process.exit(1);
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2010
2177
|
export {
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
createServer
|
|
2178
|
+
createServer,
|
|
2179
|
+
taujsBuild
|
|
2014
2180
|
};
|