@taujs/server 0.3.7 → 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/LICENSE +1 -1
- package/README.md +5 -3
- package/dist/Config-LCDjtT9m.d.ts +175 -0
- package/dist/Config.d.ts +3 -0
- package/dist/Config.js +27 -0
- package/dist/index.d.ts +52 -23
- package/dist/index.js +1897 -444
- package/package.json +19 -22
- package/dist/SSRServer-CbXIDaoA.d.ts +0 -142
- package/dist/build.d.ts +0 -25
- package/dist/build.js +0 -805
- package/dist/config.d.ts +0 -38
- package/dist/config.js +0 -146
- package/dist/security/csp.d.ts +0 -12
- package/dist/security/csp.js +0 -175
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,109 +187,18 @@ 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/types.d.ts
|
|
191
|
-
import "fastify";
|
|
192
|
-
|
|
193
|
-
// src/SSRServer.ts
|
|
194
|
-
var import_fastify_plugin2 = __toESM(require_plugin(), 1);
|
|
195
|
-
var import_picocolors = __toESM(require_picocolors(), 1);
|
|
196
|
-
import { readFile } from "fs/promises";
|
|
197
|
-
import path2 from "path";
|
|
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";
|
|
198
196
|
|
|
199
|
-
// src/
|
|
200
|
-
|
|
201
|
-
log: (...args) => {
|
|
202
|
-
if (debug) console.log(...args);
|
|
203
|
-
},
|
|
204
|
-
warn: (...args) => {
|
|
205
|
-
if (debug) console.warn(...args);
|
|
206
|
-
},
|
|
207
|
-
error: (...args) => {
|
|
208
|
-
if (debug) console.error(...args);
|
|
209
|
-
}
|
|
210
|
-
});
|
|
211
|
-
var debugLog = (logger, message, req) => {
|
|
212
|
-
const prefix = "[\u03C4js]";
|
|
213
|
-
const method = req?.method ?? "";
|
|
214
|
-
const url = req?.url ?? "";
|
|
215
|
-
const tag = method && url ? `${method} ${url}` : "";
|
|
216
|
-
logger.log(`${prefix} ${tag} ${message}`);
|
|
217
|
-
};
|
|
197
|
+
// src/Setup.ts
|
|
198
|
+
import { performance } from "perf_hooks";
|
|
218
199
|
|
|
219
200
|
// src/constants.ts
|
|
201
|
+
var import_picocolors = __toESM(require_picocolors(), 1);
|
|
220
202
|
var RENDERTYPE = {
|
|
221
203
|
ssr: "ssr",
|
|
222
204
|
streaming: "streaming"
|
|
@@ -236,45 +218,1088 @@ var DEV_CSP_DIRECTIVES = {
|
|
|
236
218
|
"style-src": ["'self'", "'unsafe-inline'"],
|
|
237
219
|
"img-src": ["'self'", "data:"]
|
|
238
220
|
};
|
|
221
|
+
var CONTENT = {
|
|
222
|
+
TAG: "\u03C4js"
|
|
223
|
+
};
|
|
224
|
+
var DEBUG = {
|
|
225
|
+
auth: { label: "auth", colour: import_picocolors.default.blue },
|
|
226
|
+
csp: { label: "csp", colour: import_picocolors.default.yellow },
|
|
227
|
+
errors: { label: "errors", colour: import_picocolors.default.red },
|
|
228
|
+
routes: { label: "routes", colour: import_picocolors.default.cyan },
|
|
229
|
+
security: { label: "security", colour: import_picocolors.default.yellow },
|
|
230
|
+
trx: { label: "trx", colour: import_picocolors.default.magenta },
|
|
231
|
+
vite: { label: "vite", colour: import_picocolors.default.yellow }
|
|
232
|
+
};
|
|
233
|
+
var REGEX = {
|
|
234
|
+
BENIGN_NET_ERR: /\b(?:ECONNRESET|EPIPE|ECONNABORTED)\b|socket hang up|aborted|premature(?: close)?/i,
|
|
235
|
+
SAFE_TRACE: /^[a-zA-Z0-9-_:.]{1,128}$/
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// src/Setup.ts
|
|
239
|
+
var extractBuildConfigs = (config) => {
|
|
240
|
+
return config.apps.map(({ appId, entryPoint, plugins }) => ({
|
|
241
|
+
appId,
|
|
242
|
+
entryPoint,
|
|
243
|
+
plugins
|
|
244
|
+
}));
|
|
245
|
+
};
|
|
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);
|
|
261
|
+
}
|
|
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(", ")}`);
|
|
264
|
+
}
|
|
265
|
+
const sortedRoutes = allRoutes.sort((a, b) => computeScore(b.path) - computeScore(a.path));
|
|
266
|
+
const durationMs = performance.now() - t0;
|
|
267
|
+
return {
|
|
268
|
+
routes: sortedRoutes,
|
|
269
|
+
apps,
|
|
270
|
+
totalRoutes: allRoutes.length,
|
|
271
|
+
durationMs,
|
|
272
|
+
warnings
|
|
273
|
+
};
|
|
274
|
+
};
|
|
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
|
|
303
|
+
};
|
|
304
|
+
};
|
|
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}`));
|
|
309
|
+
}
|
|
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
|
+
}
|
|
330
|
+
}
|
|
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);
|
|
342
|
+
} else {
|
|
343
|
+
logger.info({}, line);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
var computeScore = (path7) => {
|
|
348
|
+
return path7.split("/").filter(Boolean).reduce((score, segment) => score + (segment.startsWith(":") ? 1 : 10), 0);
|
|
349
|
+
};
|
|
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);
|
|
375
|
+
}
|
|
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;
|
|
380
|
+
}
|
|
381
|
+
if (pos.size > 0 || neg.size > 0) {
|
|
382
|
+
const o = {};
|
|
383
|
+
for (const k of pos) o[k] = true;
|
|
384
|
+
for (const k of neg) o[k] = false;
|
|
385
|
+
return o;
|
|
386
|
+
}
|
|
387
|
+
return void 0;
|
|
388
|
+
}
|
|
389
|
+
if (typeof input === "string") {
|
|
390
|
+
const raw = input.trim();
|
|
391
|
+
if (!raw) return void 0;
|
|
392
|
+
if (raw === "*" || raw.toLowerCase() === "true" || raw.toLowerCase() === "all") return true;
|
|
393
|
+
const parts = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
394
|
+
const flags = {};
|
|
395
|
+
const on = /* @__PURE__ */ new Set();
|
|
396
|
+
const off = /* @__PURE__ */ new Set();
|
|
397
|
+
for (const p of parts) {
|
|
398
|
+
const neg = p.startsWith("-") || p.startsWith("!");
|
|
399
|
+
const key = neg ? p.slice(1) : p;
|
|
400
|
+
const isValid = DEBUG_CATEGORIES.includes(key);
|
|
401
|
+
if (!isValid) {
|
|
402
|
+
console.warn(`[parseDebugInput] Invalid debug category: "${key}". Valid: ${DEBUG_CATEGORIES.join(", ")}`);
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
(neg ? off : on).add(key);
|
|
406
|
+
}
|
|
407
|
+
if (off.size > 0 && on.size === 0) {
|
|
408
|
+
flags.all = true;
|
|
409
|
+
for (const k of off) flags[k] = false;
|
|
410
|
+
return flags;
|
|
411
|
+
}
|
|
412
|
+
for (const k of on) flags[k] = true;
|
|
413
|
+
for (const k of off) flags[k] = false;
|
|
414
|
+
return flags;
|
|
415
|
+
}
|
|
416
|
+
return input;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// src/logging/Logger.ts
|
|
420
|
+
var DEBUG_CATEGORIES = ["auth", "routes", "errors", "vite", "network", "ssr"];
|
|
421
|
+
var Logger = class _Logger {
|
|
422
|
+
constructor(config = {}) {
|
|
423
|
+
this.config = config;
|
|
424
|
+
if (config.context) this.context = { ...config.context };
|
|
425
|
+
}
|
|
426
|
+
debugEnabled = /* @__PURE__ */ new Set();
|
|
427
|
+
context = {};
|
|
428
|
+
child(context) {
|
|
429
|
+
const customChild = this.config.custom?.child?.(context) ?? this.config.custom;
|
|
430
|
+
const child = new _Logger({ ...this.config, custom: customChild, context: { ...this.context, ...context } });
|
|
431
|
+
child.debugEnabled = new Set(this.debugEnabled);
|
|
432
|
+
return child;
|
|
433
|
+
}
|
|
434
|
+
configure(debug) {
|
|
435
|
+
this.debugEnabled.clear();
|
|
436
|
+
if (debug === true) {
|
|
437
|
+
this.debugEnabled = new Set(DEBUG_CATEGORIES);
|
|
438
|
+
} else if (Array.isArray(debug)) {
|
|
439
|
+
this.debugEnabled = new Set(debug);
|
|
440
|
+
} else if (typeof debug === "object" && debug) {
|
|
441
|
+
if (debug.all) this.debugEnabled = new Set(DEBUG_CATEGORIES);
|
|
442
|
+
Object.entries(debug).forEach(([key, value]) => {
|
|
443
|
+
if (key !== "all" && typeof value === "boolean") {
|
|
444
|
+
if (value) this.debugEnabled.add(key);
|
|
445
|
+
else this.debugEnabled.delete(key);
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
isDebugEnabled(category) {
|
|
451
|
+
return this.debugEnabled.has(category);
|
|
452
|
+
}
|
|
453
|
+
shouldEmit(level) {
|
|
454
|
+
const order = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
455
|
+
const minLevel = this.config.minLevel ?? "info";
|
|
456
|
+
return order[level] >= order[minLevel];
|
|
457
|
+
}
|
|
458
|
+
shouldIncludeStack(level) {
|
|
459
|
+
const include = this.config.includeStack;
|
|
460
|
+
if (include === void 0) return level === "error" || level === "warn" && process.env.NODE_ENV !== "production";
|
|
461
|
+
if (typeof include === "boolean") return include;
|
|
462
|
+
return include(level);
|
|
463
|
+
}
|
|
464
|
+
stripStacks(meta, seen = /* @__PURE__ */ new WeakSet()) {
|
|
465
|
+
if (!meta || typeof meta !== "object") return meta;
|
|
466
|
+
if (seen.has(meta)) return "[circular]";
|
|
467
|
+
seen.add(meta);
|
|
468
|
+
if (Array.isArray(meta)) return meta.map((v) => this.stripStacks(v, seen));
|
|
469
|
+
const copy = { ...meta };
|
|
470
|
+
for (const k of Object.keys(copy)) {
|
|
471
|
+
if (k === "stack" || k.endsWith("Stack")) {
|
|
472
|
+
delete copy[k];
|
|
473
|
+
} else {
|
|
474
|
+
copy[k] = this.stripStacks(copy[k], seen);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return copy;
|
|
478
|
+
}
|
|
479
|
+
formatTimestamp() {
|
|
480
|
+
const now = /* @__PURE__ */ new Date();
|
|
481
|
+
if (process.env.NODE_ENV === "production") return now.toISOString();
|
|
482
|
+
const hours = String(now.getHours()).padStart(2, "0");
|
|
483
|
+
const minutes = String(now.getMinutes()).padStart(2, "0");
|
|
484
|
+
const seconds = String(now.getSeconds()).padStart(2, "0");
|
|
485
|
+
const millis = String(now.getMilliseconds()).padStart(3, "0");
|
|
486
|
+
return `${hours}:${minutes}:${seconds}.${millis}`;
|
|
487
|
+
}
|
|
488
|
+
emit(level, message, meta, category) {
|
|
489
|
+
if (!this.shouldEmit(level)) return;
|
|
490
|
+
const timestamp = this.formatTimestamp();
|
|
491
|
+
const wantCtx = this.config.includeContext === void 0 ? false : typeof this.config.includeContext === "function" ? this.config.includeContext(level) : this.config.includeContext;
|
|
492
|
+
const customSink = this.config.custom?.[level];
|
|
493
|
+
const consoleFallback = level === "error" ? console.error : level === "warn" ? console.warn : console.log;
|
|
494
|
+
const sink = customSink ?? consoleFallback;
|
|
495
|
+
const merged = meta ?? {};
|
|
496
|
+
const withCtx = wantCtx && Object.keys(this.context).length > 0 ? { context: this.context, ...merged } : merged;
|
|
497
|
+
const finalMeta = this.shouldIncludeStack(level) ? withCtx : this.stripStacks(withCtx);
|
|
498
|
+
const hasMeta = finalMeta && typeof finalMeta === "object" ? Object.keys(finalMeta).length > 0 : false;
|
|
499
|
+
const levelText = level.toLowerCase() + (category ? `:${category.toLowerCase()}` : "");
|
|
500
|
+
const plainTag = `[${levelText}]`;
|
|
501
|
+
const coloredTag = (() => {
|
|
502
|
+
switch (level) {
|
|
503
|
+
case "debug":
|
|
504
|
+
return import_picocolors2.default.gray(plainTag);
|
|
505
|
+
case "info":
|
|
506
|
+
return import_picocolors2.default.cyan(plainTag);
|
|
507
|
+
case "warn":
|
|
508
|
+
return import_picocolors2.default.yellow(plainTag);
|
|
509
|
+
case "error":
|
|
510
|
+
return import_picocolors2.default.red(plainTag);
|
|
511
|
+
default:
|
|
512
|
+
return plainTag;
|
|
513
|
+
}
|
|
514
|
+
})();
|
|
515
|
+
const tagForOutput = customSink ? plainTag : coloredTag;
|
|
516
|
+
const formatted = `${timestamp} ${tagForOutput} ${message}`;
|
|
517
|
+
if (this.config.singleLine && hasMeta && !customSink) {
|
|
518
|
+
const metaStr = JSON.stringify(finalMeta).replace(/\n/g, "\\n");
|
|
519
|
+
consoleFallback(`${formatted} ${metaStr}`);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
if (customSink) {
|
|
523
|
+
const obj = hasMeta ? finalMeta : {};
|
|
524
|
+
customSink(obj, formatted);
|
|
525
|
+
} else {
|
|
526
|
+
hasMeta ? consoleFallback(formatted, finalMeta) : consoleFallback(formatted);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
info(meta, message) {
|
|
530
|
+
this.emit("info", message ?? "", meta);
|
|
531
|
+
}
|
|
532
|
+
warn(meta, message) {
|
|
533
|
+
this.emit("warn", message ?? "", meta);
|
|
534
|
+
}
|
|
535
|
+
error(meta, message) {
|
|
536
|
+
this.emit("error", message ?? "", meta);
|
|
537
|
+
}
|
|
538
|
+
debug(category, meta, message) {
|
|
539
|
+
if (!this.debugEnabled.has(category)) return;
|
|
540
|
+
this.emit("debug", message ?? "", meta, category);
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
function createLogger(opts) {
|
|
544
|
+
const logger = new Logger({
|
|
545
|
+
custom: opts?.custom,
|
|
546
|
+
context: opts?.context,
|
|
547
|
+
minLevel: opts?.minLevel,
|
|
548
|
+
includeStack: opts?.includeStack,
|
|
549
|
+
includeContext: opts?.includeContext,
|
|
550
|
+
singleLine: opts?.singleLine
|
|
551
|
+
});
|
|
552
|
+
const parsed = parseDebugInput(opts?.debug);
|
|
553
|
+
if (parsed !== void 0) logger.configure(parsed);
|
|
554
|
+
return logger;
|
|
555
|
+
}
|
|
556
|
+
|
|
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;
|
|
565
|
+
};
|
|
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;
|
|
578
|
+
}
|
|
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;
|
|
588
|
+
}
|
|
589
|
+
if (!networkAddress) networkAddress = iface.address;
|
|
590
|
+
}
|
|
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
|
+
};
|
|
608
|
+
|
|
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
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return void 0;
|
|
629
|
+
}
|
|
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 };
|
|
652
|
+
}
|
|
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";
|
|
660
|
+
}
|
|
661
|
+
return custom > 0 ? `Loaded development defaults with ${custom} route override(s)` : "Loaded development defaults";
|
|
662
|
+
}
|
|
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)";
|
|
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
|
+
});
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
return { items };
|
|
716
|
+
};
|
|
717
|
+
|
|
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
|
|
731
|
+
};
|
|
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
|
|
748
|
+
});
|
|
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);
|
|
756
|
+
}
|
|
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
|
|
774
|
+
}
|
|
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)
|
|
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)
|
|
857
|
+
}
|
|
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";
|
|
877
|
+
};
|
|
878
|
+
|
|
879
|
+
// src/utils/DataRoutes.ts
|
|
880
|
+
import { match } from "path-to-regexp";
|
|
881
|
+
|
|
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
|
+
}
|
|
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
|
+
};
|
|
239
917
|
|
|
240
|
-
// src/
|
|
241
|
-
|
|
242
|
-
|
|
918
|
+
// src/utils/DataRoutes.ts
|
|
919
|
+
var safeDecode = (value) => {
|
|
920
|
+
try {
|
|
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;
|
|
943
|
+
}
|
|
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
|
|
997
|
+
});
|
|
998
|
+
}
|
|
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) => {
|
|
243
1016
|
return async function authHook(req, reply) {
|
|
244
1017
|
const url = new URL(req.url, `http://${req.headers.host}`).pathname;
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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])];
|
|
1083
|
+
} else {
|
|
1084
|
+
merged[directive] = [...values];
|
|
1085
|
+
}
|
|
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;
|
|
1106
|
+
try {
|
|
1107
|
+
const routeMatch = findMatchingRoute(matchers, req.url);
|
|
1108
|
+
const routeCSP = routeMatch?.route.attr?.middleware?.csp;
|
|
1109
|
+
if (routeCSP === false) {
|
|
1110
|
+
done();
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
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)
|
|
1147
|
+
},
|
|
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
|
+
);
|
|
249
1205
|
return;
|
|
250
1206
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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;
|
|
262
1219
|
}
|
|
263
|
-
|
|
264
|
-
|
|
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)
|
|
1287
|
+
},
|
|
1288
|
+
"CSP reporting route failed"
|
|
1289
|
+
);
|
|
1290
|
+
}
|
|
1291
|
+
reply.code(204).send();
|
|
1292
|
+
});
|
|
1293
|
+
},
|
|
1294
|
+
{ name: "taujs-csp-report-plugin" }
|
|
1295
|
+
);
|
|
265
1296
|
|
|
266
|
-
// src/
|
|
267
|
-
|
|
268
|
-
import
|
|
1297
|
+
// src/utils/AssetManager.ts
|
|
1298
|
+
import { readFile } from "fs/promises";
|
|
1299
|
+
import path2 from "path";
|
|
1300
|
+
import { pathToFileURL } from "url";
|
|
269
1301
|
|
|
270
|
-
// src/utils/
|
|
271
|
-
import { dirname, join } from "path";
|
|
272
|
-
import "path";
|
|
273
|
-
import { fileURLToPath } from "url";
|
|
274
|
-
import { match } from "path-to-regexp";
|
|
275
|
-
var isDevelopment = process.env.NODE_ENV === "development";
|
|
276
|
-
var __filename = fileURLToPath(import.meta.url);
|
|
277
|
-
var __dirname = join(dirname(__filename), !isDevelopment ? "./" : "..");
|
|
1302
|
+
// src/utils/Templates.ts
|
|
278
1303
|
var CSS_LANGS_RE = /\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\?)/;
|
|
279
1304
|
async function collectStyle(server, entries) {
|
|
280
1305
|
const urls = await collectStyleUrls(server, entries);
|
|
@@ -337,44 +1362,6 @@ function renderPreloadLink(file) {
|
|
|
337
1362
|
return "";
|
|
338
1363
|
}
|
|
339
1364
|
}
|
|
340
|
-
var callServiceMethod = async (registry, serviceName, methodName, params) => {
|
|
341
|
-
const service = registry[serviceName];
|
|
342
|
-
if (!service) throw new Error(`Service ${String(serviceName)} does not exist in the registry`);
|
|
343
|
-
const method = service[methodName];
|
|
344
|
-
if (typeof method !== "function") throw new Error(`Service method ${String(methodName)} does not exist on ${String(serviceName)}`);
|
|
345
|
-
const data = await method(params);
|
|
346
|
-
if (typeof data !== "object" || data === null)
|
|
347
|
-
throw new Error(`Expected object response from ${String(serviceName)}.${String(methodName)}, but got ${typeof data}`);
|
|
348
|
-
return data;
|
|
349
|
-
};
|
|
350
|
-
var isServiceDescriptor = (obj) => {
|
|
351
|
-
if (typeof obj !== "object" || obj === null || Array.isArray(obj)) return false;
|
|
352
|
-
const maybe = obj;
|
|
353
|
-
return typeof maybe.serviceName === "string" && typeof maybe.serviceMethod === "string";
|
|
354
|
-
};
|
|
355
|
-
var fetchInitialData = async (attr, params, serviceRegistry, ctx = { headers: {} }, callServiceMethodImpl = callServiceMethod) => {
|
|
356
|
-
const dataHandler = attr?.data;
|
|
357
|
-
if (!dataHandler || typeof dataHandler !== "function") return Promise.resolve({});
|
|
358
|
-
return dataHandler(params, ctx).then(async (result) => {
|
|
359
|
-
if (isServiceDescriptor(result)) {
|
|
360
|
-
const { serviceName, serviceMethod, args } = result;
|
|
361
|
-
if (serviceRegistry[serviceName]?.[serviceMethod]) return callServiceMethodImpl(serviceRegistry, serviceName, serviceMethod, args ?? {});
|
|
362
|
-
throw new Error(`Invalid service: serviceName=${String(serviceName)}, method=${String(serviceMethod)}`);
|
|
363
|
-
}
|
|
364
|
-
if (typeof result === "object" && result !== null) return result;
|
|
365
|
-
throw new Error("Invalid result from attr.data");
|
|
366
|
-
});
|
|
367
|
-
};
|
|
368
|
-
var matchRoute = (url, renderRoutes) => {
|
|
369
|
-
for (const route of renderRoutes) {
|
|
370
|
-
const matcher = match(route.path, {
|
|
371
|
-
decode: decodeURIComponent
|
|
372
|
-
});
|
|
373
|
-
const matched = matcher(url);
|
|
374
|
-
if (matched) return { route, params: matched.params };
|
|
375
|
-
}
|
|
376
|
-
return null;
|
|
377
|
-
};
|
|
378
1365
|
function getCssLinks(manifest, basePath = "") {
|
|
379
1366
|
const seen = /* @__PURE__ */ new Set();
|
|
380
1367
|
const styles = [];
|
|
@@ -402,73 +1389,32 @@ var ensureNonNull = (value, errorMessage) => {
|
|
|
402
1389
|
if (value === void 0 || value === null) throw new Error(errorMessage);
|
|
403
1390
|
return value;
|
|
404
1391
|
};
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
if (!merged["script-src"].some((v) => v.startsWith("'nonce-"))) merged["script-src"].push(`'nonce-${nonce}'`);
|
|
411
|
-
if (isDevelopment) {
|
|
412
|
-
const connect = merged["connect-src"] || ["'self'"];
|
|
413
|
-
if (!connect.includes("ws:")) connect.push("ws:");
|
|
414
|
-
if (!connect.includes("http:")) connect.push("http:");
|
|
415
|
-
merged["connect-src"] = connect;
|
|
416
|
-
const style = merged["style-src"] || ["'self'"];
|
|
417
|
-
if (!style.includes("'unsafe-inline'")) style.push("'unsafe-inline'");
|
|
418
|
-
merged["style-src"] = style;
|
|
419
|
-
}
|
|
420
|
-
return Object.entries(merged).map(([key, values]) => `${key} ${values.join(" ")}`).join("; ");
|
|
421
|
-
};
|
|
422
|
-
var generateNonce = () => crypto.randomBytes(16).toString("base64");
|
|
423
|
-
var cspPlugin = (0, import_fastify_plugin.default)(
|
|
424
|
-
async (fastify, opts) => {
|
|
425
|
-
fastify.addHook("onRequest", (req, reply, done) => {
|
|
426
|
-
const nonce = generateNonce();
|
|
427
|
-
req.cspNonce = nonce;
|
|
428
|
-
const directives = opts.directives ?? DEV_CSP_DIRECTIVES;
|
|
429
|
-
const generate = opts.generateCSP ?? defaultGenerateCSP;
|
|
430
|
-
const cspHeader = generate(directives, nonce);
|
|
431
|
-
reply.header("Content-Security-Policy", cspHeader);
|
|
432
|
-
done();
|
|
433
|
-
});
|
|
434
|
-
},
|
|
435
|
-
{
|
|
436
|
-
name: "taujs-csp-plugin"
|
|
437
|
-
}
|
|
438
|
-
);
|
|
439
|
-
|
|
440
|
-
// src/security/verifyMiddleware.ts
|
|
441
|
-
var isAuthRequired = (r) => r.attr?.middleware?.auth?.required === true;
|
|
442
|
-
var hasAuthenticate = (app) => typeof app.authenticate === "function";
|
|
443
|
-
var verifyContracts = (app, routes, contracts, isDebug) => {
|
|
444
|
-
const logger = createLogger(Boolean(isDebug));
|
|
445
|
-
for (const contract of contracts) {
|
|
446
|
-
const isUsed = routes.some(contract.required);
|
|
447
|
-
if (!isUsed) {
|
|
448
|
-
debugLog(logger, `Middleware "${contract.key}" not used in any routes`);
|
|
449
|
-
continue;
|
|
450
|
-
}
|
|
451
|
-
if (!contract.verify(app)) {
|
|
452
|
-
const error = new Error(`[\u03C4js] ${contract.errorMessage}`);
|
|
453
|
-
logger.error(error.message);
|
|
454
|
-
throw error;
|
|
455
|
-
}
|
|
456
|
-
debugLog(logger, `Middleware "${contract.key}" verified \u2713`);
|
|
457
|
-
}
|
|
458
|
-
};
|
|
459
|
-
|
|
460
|
-
// src/SSRServer.ts
|
|
461
|
-
var createMaps = () => {
|
|
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.`);
|
|
462
1397
|
return {
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
renderModules: /* @__PURE__ */ new Map(),
|
|
468
|
-
ssrManifests: /* @__PURE__ */ new Map(),
|
|
469
|
-
templates: /* @__PURE__ */ new Map()
|
|
1398
|
+
beforeHead: headSplit,
|
|
1399
|
+
afterHead: "",
|
|
1400
|
+
beforeBody: beforeBody.replace(/\s*$/, ""),
|
|
1401
|
+
afterBody: afterBody.replace(/^\s*/, "")
|
|
470
1402
|
};
|
|
1403
|
+
}
|
|
1404
|
+
var rebuildTemplate = (parts, headContent, bodyContent) => {
|
|
1405
|
+
return `${parts.beforeHead}${headContent}${parts.afterHead}${parts.beforeBody}${bodyContent}${parts.afterBody}`;
|
|
471
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
|
+
});
|
|
472
1418
|
var processConfigs = (configs, baseClientRoot, templateDefaults) => {
|
|
473
1419
|
return configs.map((config) => {
|
|
474
1420
|
const clientRoot = path2.resolve(baseClientRoot, config.entryPoint);
|
|
@@ -482,246 +1428,753 @@ var processConfigs = (configs, baseClientRoot, templateDefaults) => {
|
|
|
482
1428
|
};
|
|
483
1429
|
});
|
|
484
1430
|
};
|
|
485
|
-
var
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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 {
|
|
493
1439
|
const templateHtmlPath = path2.join(clientRoot, htmlTemplate);
|
|
494
1440
|
const templateHtml = await readFile(templateHtmlPath, "utf-8");
|
|
495
1441
|
templates.set(clientRoot, templateHtml);
|
|
496
1442
|
const relativeBasePath = path2.relative(baseClientRoot, clientRoot).replace(/\\/g, "/");
|
|
497
1443
|
const adjustedRelativePath = relativeBasePath ? `/${relativeBasePath}` : "";
|
|
498
1444
|
if (!isDevelopment) {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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)
|
|
1461
|
+
}
|
|
1462
|
+
});
|
|
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
|
+
}
|
|
515
1500
|
} else {
|
|
516
1501
|
const bootstrapModule = `/${adjustedRelativePath}/${entryClient}`.replace(/\/{2,}/g, "/");
|
|
517
1502
|
bootstrapModules.set(clientRoot, bootstrapModule);
|
|
518
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"
|
|
1509
|
+
},
|
|
1510
|
+
"Failed to process config"
|
|
1511
|
+
);
|
|
519
1512
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
1513
|
+
}
|
|
1514
|
+
};
|
|
1515
|
+
|
|
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"
|
|
1523
|
+
});
|
|
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 ? [
|
|
525
1539
|
{
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
+
}
|
|
530
1568
|
}
|
|
531
|
-
]
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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
|
+
}
|
|
543
1588
|
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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();
|
|
1595
|
+
});
|
|
547
1596
|
});
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
1597
|
+
});
|
|
1598
|
+
return viteDevServer;
|
|
1599
|
+
};
|
|
1600
|
+
|
|
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 };
|
|
1621
|
+
}
|
|
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
|
|
1631
|
+
});
|
|
1632
|
+
try {
|
|
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();
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
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
|
|
590
1651
|
}
|
|
591
1652
|
});
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
1653
|
+
}
|
|
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) {
|
|
1663
|
+
try {
|
|
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) }
|
|
598
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");
|
|
599
1698
|
});
|
|
600
|
-
|
|
601
|
-
|
|
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 = "";
|
|
602
1707
|
try {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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 });
|
|
609
1716
|
return;
|
|
610
1717
|
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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);
|
|
1732
|
+
} catch (err) {
|
|
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;
|
|
1738
|
+
}
|
|
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
|
+
});
|
|
1745
|
+
}
|
|
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"
|
|
1750
|
+
});
|
|
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 });
|
|
1767
|
+
});
|
|
1768
|
+
reply.raw.on("error", (err) => {
|
|
1769
|
+
if (!isBenignSocketAbort(err)) logger.error("HTTP socket error:", { error: err });
|
|
1770
|
+
});
|
|
1771
|
+
writable.pipe(reply.raw, { end: false });
|
|
1772
|
+
let finalData = void 0;
|
|
1773
|
+
renderStream(
|
|
1774
|
+
writable,
|
|
1775
|
+
{
|
|
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) });
|
|
684
1794
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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
|
+
}
|
|
1815
|
+
},
|
|
1816
|
+
initialDataInput,
|
|
1817
|
+
req.url,
|
|
1818
|
+
shouldHydrate ? bootstrapModule : void 0,
|
|
1819
|
+
attr?.meta,
|
|
1820
|
+
cspNonce,
|
|
1821
|
+
ac.signal,
|
|
1822
|
+
{ logger: reqLogger }
|
|
1823
|
+
);
|
|
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);
|
|
1833
|
+
reply.raw.end();
|
|
1834
|
+
});
|
|
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
|
+
});
|
|
1842
|
+
}
|
|
1843
|
+
};
|
|
1844
|
+
|
|
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();
|
|
1855
|
+
}
|
|
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
|
+
});
|
|
1862
|
+
}
|
|
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>`);
|
|
1882
|
+
}
|
|
1883
|
+
if (bootstrapModule) {
|
|
1884
|
+
const nonceAttr = cspNonce ? ` nonce="${cspNonce}"` : "";
|
|
1885
|
+
processedTemplate = processedTemplate.replace("</body>", `<script${nonceAttr} type="module" src="${bootstrapModule}" defer></script></body>`);
|
|
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
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
};
|
|
1898
|
+
|
|
1899
|
+
// src/utils/StaticAssets.ts
|
|
1900
|
+
function normalizeStaticAssets(reg) {
|
|
1901
|
+
if (!reg) return [];
|
|
1902
|
+
return Array.isArray(reg) ? reg : [reg];
|
|
1903
|
+
}
|
|
1904
|
+
function prefixWeight(prefix) {
|
|
1905
|
+
if (typeof prefix !== "string" || prefix === "/" || prefix.length === 0) return 0;
|
|
1906
|
+
return prefix.split("/").filter(Boolean).length;
|
|
1907
|
+
}
|
|
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);
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
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
|
|
697
1954
|
}
|
|
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
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
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
|
+
});
|
|
698
1979
|
});
|
|
699
1980
|
app.setNotFoundHandler(async (req, reply) => {
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
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();
|
|
717
2019
|
}
|
|
718
2020
|
});
|
|
719
2021
|
},
|
|
720
|
-
{ name: "
|
|
2022
|
+
{ name: "\u03C4js-ssr-server" }
|
|
721
2023
|
);
|
|
2024
|
+
|
|
2025
|
+
// src/CreateServer.ts
|
|
2026
|
+
var createServer = async (opts) => {
|
|
2027
|
+
const t0 = performance2.now();
|
|
2028
|
+
const clientRoot = opts.clientRoot ?? path5.resolve(process.cwd(), "client");
|
|
2029
|
+
const app = opts.fastify ?? Fastify({ logger: false });
|
|
2030
|
+
const net = resolveNet(opts.config.server);
|
|
2031
|
+
await app.register(bannerPlugin, {
|
|
2032
|
+
debug: opts.debug,
|
|
2033
|
+
hmr: { host: net.host, port: net.hmrPort }
|
|
2034
|
+
});
|
|
2035
|
+
const logger = createLogger({
|
|
2036
|
+
debug: opts.debug,
|
|
2037
|
+
custom: opts.logger,
|
|
2038
|
+
minLevel: process.env.NODE_ENV === "production" ? "info" : "debug",
|
|
2039
|
+
includeContext: true
|
|
2040
|
+
});
|
|
2041
|
+
const configs = extractBuildConfigs(opts.config);
|
|
2042
|
+
const { routes, apps, totalRoutes, durationMs, warnings } = extractRoutes(opts.config);
|
|
2043
|
+
const { security, durationMs: securityDuration, hasExplicitCSP } = extractSecurity(opts.config);
|
|
2044
|
+
printConfigSummary(logger, apps, configs.length, totalRoutes, durationMs, warnings);
|
|
2045
|
+
printSecuritySummary(logger, routes, security, hasExplicitCSP, securityDuration);
|
|
2046
|
+
const report = verifyContracts(
|
|
2047
|
+
app,
|
|
2048
|
+
routes,
|
|
2049
|
+
[
|
|
2050
|
+
{
|
|
2051
|
+
key: "auth",
|
|
2052
|
+
required: (rts) => rts.some(isAuthRequired),
|
|
2053
|
+
verify: hasAuthenticate,
|
|
2054
|
+
errorMessage: "Routes require auth but Fastify is missing .authenticate decorator."
|
|
2055
|
+
},
|
|
2056
|
+
{
|
|
2057
|
+
key: "csp",
|
|
2058
|
+
required: () => true,
|
|
2059
|
+
verify: () => true,
|
|
2060
|
+
errorMessage: "CSP plugin failed to register."
|
|
2061
|
+
}
|
|
2062
|
+
],
|
|
2063
|
+
security
|
|
2064
|
+
);
|
|
2065
|
+
printContractReport(logger, report);
|
|
2066
|
+
try {
|
|
2067
|
+
await app.register(SSRServer, {
|
|
2068
|
+
clientRoot,
|
|
2069
|
+
configs,
|
|
2070
|
+
routes,
|
|
2071
|
+
serviceRegistry: opts.serviceRegistry,
|
|
2072
|
+
staticAssets: opts.staticAssets !== void 0 ? opts.staticAssets : { plugin: fastifyStatic },
|
|
2073
|
+
debug: opts.debug,
|
|
2074
|
+
alias: opts.alias,
|
|
2075
|
+
security,
|
|
2076
|
+
devNet: { host: net.host, hmrPort: net.hmrPort }
|
|
2077
|
+
});
|
|
2078
|
+
} catch (err) {
|
|
2079
|
+
logger.error(
|
|
2080
|
+
{
|
|
2081
|
+
step: "register:SSRServer",
|
|
2082
|
+
error: normaliseError(err)
|
|
2083
|
+
},
|
|
2084
|
+
"Failed to register SSRServer"
|
|
2085
|
+
);
|
|
2086
|
+
}
|
|
2087
|
+
const t1 = performance2.now();
|
|
2088
|
+
console.log(`
|
|
2089
|
+
${import_picocolors4.default.bgGreen(import_picocolors4.default.black(` ${CONTENT.TAG} `))} configured in ${(t1 - t0).toFixed(0)}ms
|
|
2090
|
+
`);
|
|
2091
|
+
if (opts.fastify) return { net };
|
|
2092
|
+
return { app, net };
|
|
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
|
+
}
|
|
722
2177
|
export {
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
createMaps,
|
|
726
|
-
processConfigs
|
|
2178
|
+
createServer,
|
|
2179
|
+
taujsBuild
|
|
727
2180
|
};
|