@taujs/server 0.2.4 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build.js +62 -6
- package/dist/index.d.ts +4 -113
- package/dist/index.js +60 -4
- package/dist/security/csp.d.ts +131 -0
- package/dist/security/csp.js +58 -0
- package/package.json +6 -1
package/dist/build.js
CHANGED
|
@@ -188,7 +188,6 @@ var require_picocolors = __commonJS({
|
|
|
188
188
|
});
|
|
189
189
|
|
|
190
190
|
// src/build.ts
|
|
191
|
-
import fs from "fs/promises";
|
|
192
191
|
import path3 from "path";
|
|
193
192
|
import { build } from "vite";
|
|
194
193
|
import { nodePolyfills } from "vite-plugin-node-polyfills";
|
|
@@ -213,6 +212,53 @@ var TEMPLATE = {
|
|
|
213
212
|
defaultEntryServer: "entry-server",
|
|
214
213
|
defaultHtmlTemplate: "index.html"
|
|
215
214
|
};
|
|
215
|
+
var DEV_CSP_DIRECTIVES = {
|
|
216
|
+
"default-src": ["'self'"],
|
|
217
|
+
"connect-src": ["'self'", "ws:", "http:"],
|
|
218
|
+
"style-src": ["'self'", "'unsafe-inline'"],
|
|
219
|
+
"img-src": ["'self'", "data:"]
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// src/security/csp.ts
|
|
223
|
+
import crypto from "crypto";
|
|
224
|
+
var defaultGenerateCSP = (directives, nonce) => {
|
|
225
|
+
const merged = { ...directives };
|
|
226
|
+
merged["script-src"] = merged["script-src"] || ["'self'"];
|
|
227
|
+
if (!merged["script-src"].some((v) => v.startsWith("'nonce-"))) merged["script-src"].push(`'nonce-${nonce}'`);
|
|
228
|
+
if (process.env.NODE_ENV !== "production") {
|
|
229
|
+
const connect = merged["connect-src"] || ["'self'"];
|
|
230
|
+
if (!connect.includes("ws:")) connect.push("ws:");
|
|
231
|
+
if (!connect.includes("http:")) connect.push("http:");
|
|
232
|
+
merged["connect-src"] = connect;
|
|
233
|
+
const style = merged["style-src"] || ["'self'"];
|
|
234
|
+
if (!style.includes("'unsafe-inline'")) style.push("'unsafe-inline'");
|
|
235
|
+
merged["style-src"] = style;
|
|
236
|
+
}
|
|
237
|
+
return Object.entries(merged).map(([key, values]) => `${key} ${values.join(" ")}`).join("; ");
|
|
238
|
+
};
|
|
239
|
+
var generateNonce = () => crypto.randomBytes(16).toString("base64");
|
|
240
|
+
var cspHook = (options = {}) => (req, reply, done) => {
|
|
241
|
+
const nonce = generateNonce();
|
|
242
|
+
const directives = options.directives ?? DEV_CSP_DIRECTIVES;
|
|
243
|
+
const generate = options.generateCSP ?? defaultGenerateCSP;
|
|
244
|
+
const cspHeader = generate(directives, nonce);
|
|
245
|
+
reply.header("Content-Security-Policy", cspHeader);
|
|
246
|
+
if (typeof options.exposeNonce === "function") {
|
|
247
|
+
options.exposeNonce(req, nonce);
|
|
248
|
+
} else {
|
|
249
|
+
req.nonce = nonce;
|
|
250
|
+
}
|
|
251
|
+
done();
|
|
252
|
+
};
|
|
253
|
+
var applyCSP = (security, reply) => {
|
|
254
|
+
if (!security?.csp) return;
|
|
255
|
+
const nonce = generateNonce();
|
|
256
|
+
const { directives = {}, generateCSP = defaultGenerateCSP } = security.csp;
|
|
257
|
+
const header = generateCSP(directives, nonce);
|
|
258
|
+
reply.header("Content-Security-Policy", header);
|
|
259
|
+
reply.request.nonce = nonce;
|
|
260
|
+
return nonce;
|
|
261
|
+
};
|
|
216
262
|
|
|
217
263
|
// src/utils/Utils.ts
|
|
218
264
|
import { dirname, join } from "path";
|
|
@@ -426,6 +472,13 @@ var SSRServer = (0, import_fastify_plugin.default)(
|
|
|
426
472
|
root: baseClientRoot,
|
|
427
473
|
wildcard: false
|
|
428
474
|
});
|
|
475
|
+
app.addHook(
|
|
476
|
+
"onRequest",
|
|
477
|
+
cspHook({
|
|
478
|
+
directives: opts.security?.csp?.directives ?? DEV_CSP_DIRECTIVES,
|
|
479
|
+
generateCSP: opts.security?.csp?.generateCSP
|
|
480
|
+
})
|
|
481
|
+
);
|
|
429
482
|
if (isDevelopment) {
|
|
430
483
|
const { createServer } = await import("vite");
|
|
431
484
|
viteDevServer = await createServer({
|
|
@@ -483,6 +536,7 @@ var SSRServer = (0, import_fastify_plugin.default)(
|
|
|
483
536
|
if (/\.\w+$/.test(req.raw.url ?? "")) return reply.callNotFound();
|
|
484
537
|
const url = req.url ? new URL(req.url, `http://${req.headers.host}`).pathname : "/";
|
|
485
538
|
const matchedRoute = matchRoute(url, routes);
|
|
539
|
+
const nonce = applyCSP(opts.security, reply);
|
|
486
540
|
if (!matchedRoute) {
|
|
487
541
|
reply.callNotFound();
|
|
488
542
|
return;
|
|
@@ -524,12 +578,12 @@ var SSRServer = (0, import_fastify_plugin.default)(
|
|
|
524
578
|
if (renderType === RENDERTYPE.ssr) {
|
|
525
579
|
const { renderSSR } = renderModule;
|
|
526
580
|
const initialDataResolved = await initialDataPromise;
|
|
527
|
-
const initialDataScript = `<script>window.__INITIAL_DATA__ = ${JSON.stringify(initialDataResolved).replace(/</g, "\\u003c")}</script>`;
|
|
581
|
+
const initialDataScript = `<script nonce="${nonce}">window.__INITIAL_DATA__ = ${JSON.stringify(initialDataResolved).replace(/</g, "\\u003c")}</script>`;
|
|
528
582
|
const { headContent, appHtml } = await renderSSR(initialDataResolved, req.url, attr?.meta);
|
|
529
583
|
let aggregateHeadContent = headContent;
|
|
530
584
|
if (ssrManifest && preloadLink) aggregateHeadContent += preloadLink;
|
|
531
585
|
if (manifest && cssLink) aggregateHeadContent += cssLink;
|
|
532
|
-
const fullHtml = template.replace(SSRTAG.ssrHead, aggregateHeadContent).replace(SSRTAG.ssrHtml, `${appHtml}${initialDataScript}<script type="module" src="${bootstrapModule}" defer></script>`);
|
|
586
|
+
const fullHtml = template.replace(SSRTAG.ssrHead, aggregateHeadContent).replace(SSRTAG.ssrHtml, `${appHtml}${initialDataScript}<script nonce="${nonce}" type="module" src="${bootstrapModule}" defer></script>`);
|
|
533
587
|
return reply.status(200).header("Content-Type", "text/html").send(fullHtml);
|
|
534
588
|
} else {
|
|
535
589
|
const { renderStream } = renderModule;
|
|
@@ -544,7 +598,7 @@ var SSRServer = (0, import_fastify_plugin.default)(
|
|
|
544
598
|
reply.raw.write(`${beforeHead}${aggregateHeadContent}${afterHead}`);
|
|
545
599
|
},
|
|
546
600
|
onFinish: async (initialDataResolved) => {
|
|
547
|
-
reply.raw.write(`<script>window.__INITIAL_DATA__ = ${JSON.stringify(initialDataResolved).replace(/</g, "\\u003c")}</script>`);
|
|
601
|
+
reply.raw.write(`<script nonce="${nonce}">window.__INITIAL_DATA__ = ${JSON.stringify(initialDataResolved).replace(/</g, "\\u003c")}</script>`);
|
|
548
602
|
reply.raw.write(afterBody);
|
|
549
603
|
reply.raw.end();
|
|
550
604
|
},
|
|
@@ -571,12 +625,13 @@ var SSRServer = (0, import_fastify_plugin.default)(
|
|
|
571
625
|
const defaultConfig = processedConfigs[0];
|
|
572
626
|
if (!defaultConfig) throw new Error("No default configuration found.");
|
|
573
627
|
const { clientRoot } = defaultConfig;
|
|
628
|
+
const nonce = applyCSP(opts.security, reply);
|
|
574
629
|
let template = ensureNonNull(templates.get(clientRoot), `Template not found for clientRoot: ${clientRoot}`);
|
|
575
630
|
const cssLink = cssLinks.get(clientRoot);
|
|
576
631
|
const bootstrapModule = bootstrapModules.get(clientRoot);
|
|
577
632
|
template = template.replace(SSRTAG.ssrHead, "").replace(SSRTAG.ssrHtml, "");
|
|
578
633
|
if (!isDevelopment && cssLink) template = template.replace("</head>", `${cssLink}</head>`);
|
|
579
|
-
if (bootstrapModule) template = template.replace("</body>", `<script type="module" src="${bootstrapModule}" defer></script></body>`);
|
|
634
|
+
if (bootstrapModule) template = template.replace("</body>", `<script nonce="${nonce}" type="module" src="${bootstrapModule}" defer></script></body>`);
|
|
580
635
|
reply.status(200).type("text/html").send(template);
|
|
581
636
|
} catch (error) {
|
|
582
637
|
console.error("Failed to serve clientHtmlTemplate:", error);
|
|
@@ -595,9 +650,10 @@ async function taujsBuild({
|
|
|
595
650
|
isSSRBuild = process.env.BUILD_MODE === "ssr"
|
|
596
651
|
}) {
|
|
597
652
|
const deleteDist = async () => {
|
|
653
|
+
const { rm } = await import("fs/promises");
|
|
598
654
|
const distPath = path3.resolve(projectRoot, "dist");
|
|
599
655
|
try {
|
|
600
|
-
await
|
|
656
|
+
await rm(distPath, { recursive: true, force: true });
|
|
601
657
|
console.log("Deleted the dist directory\n");
|
|
602
658
|
} catch (err) {
|
|
603
659
|
console.error("Error deleting dist directory:", err);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,113 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
declare const RENDERTYPE: {
|
|
6
|
-
ssr: string;
|
|
7
|
-
streaming: string;
|
|
8
|
-
};
|
|
9
|
-
declare const TEMPLATE: {
|
|
10
|
-
defaultEntryClient: string;
|
|
11
|
-
defaultEntryServer: string;
|
|
12
|
-
defaultHtmlTemplate: string;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
declare const createMaps: () => {
|
|
16
|
-
bootstrapModules: Map<string, string>;
|
|
17
|
-
cssLinks: Map<string, string>;
|
|
18
|
-
manifests: Map<string, Manifest>;
|
|
19
|
-
preloadLinks: Map<string, string>;
|
|
20
|
-
renderModules: Map<string, RenderModule>;
|
|
21
|
-
ssrManifests: Map<string, SSRManifest>;
|
|
22
|
-
templates: Map<string, string>;
|
|
23
|
-
};
|
|
24
|
-
declare const processConfigs: (configs: Config[], baseClientRoot: string, templateDefaults: typeof TEMPLATE) => ProcessedConfig[];
|
|
25
|
-
declare const SSRServer: FastifyPluginAsync<SSRServerOptions>;
|
|
26
|
-
type Config = {
|
|
27
|
-
appId: string;
|
|
28
|
-
entryPoint: string;
|
|
29
|
-
entryClient?: string;
|
|
30
|
-
entryServer?: string;
|
|
31
|
-
htmlTemplate?: string;
|
|
32
|
-
};
|
|
33
|
-
type ProcessedConfig = {
|
|
34
|
-
appId: string;
|
|
35
|
-
clientRoot: string;
|
|
36
|
-
entryClient: string;
|
|
37
|
-
entryPoint: string;
|
|
38
|
-
entryServer: string;
|
|
39
|
-
htmlTemplate: string;
|
|
40
|
-
plugins?: PluginOption[];
|
|
41
|
-
};
|
|
42
|
-
type SSRServerOptions = {
|
|
43
|
-
alias?: Record<string, string>;
|
|
44
|
-
clientRoot: string;
|
|
45
|
-
configs: Config[];
|
|
46
|
-
routes: Route<RouteParams>[];
|
|
47
|
-
serviceRegistry: ServiceRegistry;
|
|
48
|
-
isDebug?: boolean;
|
|
49
|
-
};
|
|
50
|
-
type ServiceMethod = (params: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
51
|
-
type NamedService = Record<string, ServiceMethod>;
|
|
52
|
-
type ServiceRegistry = Record<string, NamedService>;
|
|
53
|
-
type RenderCallbacks = {
|
|
54
|
-
onHead: (headContent: string) => void;
|
|
55
|
-
onFinish: (initialDataResolved: unknown) => void;
|
|
56
|
-
onError: (error: unknown) => void;
|
|
57
|
-
};
|
|
58
|
-
type FetchConfig = {
|
|
59
|
-
url?: string;
|
|
60
|
-
options: RequestInit & {
|
|
61
|
-
params?: Record<string, unknown>;
|
|
62
|
-
};
|
|
63
|
-
serviceName?: string;
|
|
64
|
-
serviceMethod?: string;
|
|
65
|
-
};
|
|
66
|
-
type SSRManifest = {
|
|
67
|
-
[key: string]: string[];
|
|
68
|
-
};
|
|
69
|
-
type ManifestEntry = {
|
|
70
|
-
file: string;
|
|
71
|
-
src?: string;
|
|
72
|
-
isDynamicEntry?: boolean;
|
|
73
|
-
imports?: string[];
|
|
74
|
-
css?: string[];
|
|
75
|
-
assets?: string[];
|
|
76
|
-
};
|
|
77
|
-
type Manifest = {
|
|
78
|
-
[key: string]: ManifestEntry;
|
|
79
|
-
};
|
|
80
|
-
type RenderSSR = (initialDataResolved: Record<string, unknown>, location: string, meta?: Record<string, unknown>) => Promise<{
|
|
81
|
-
headContent: string;
|
|
82
|
-
appHtml: string;
|
|
83
|
-
initialDataScript: string;
|
|
84
|
-
}>;
|
|
85
|
-
type RenderStream = (serverResponse: ServerResponse, callbacks: RenderCallbacks, initialDataPromise: Promise<Record<string, unknown>>, location: string, bootstrapModules?: string, meta?: Record<string, unknown>) => void;
|
|
86
|
-
type RenderModule = {
|
|
87
|
-
renderSSR: RenderSSR;
|
|
88
|
-
renderStream: RenderStream;
|
|
89
|
-
};
|
|
90
|
-
type RouteAttributes<Params = {}> = {
|
|
91
|
-
fetch?: (params?: Params, options?: RequestInit & {
|
|
92
|
-
params?: Record<string, unknown>;
|
|
93
|
-
}) => Promise<FetchConfig>;
|
|
94
|
-
} & ({
|
|
95
|
-
render?: typeof RENDERTYPE.ssr;
|
|
96
|
-
meta?: Record<string, unknown>;
|
|
97
|
-
} | {
|
|
98
|
-
render: typeof RENDERTYPE.streaming;
|
|
99
|
-
meta: Record<string, unknown>;
|
|
100
|
-
});
|
|
101
|
-
type Route<Params = {}> = {
|
|
102
|
-
attr?: RouteAttributes<Params>;
|
|
103
|
-
path: string;
|
|
104
|
-
appId?: string;
|
|
105
|
-
};
|
|
106
|
-
interface InitialRouteParams extends Record<string, unknown> {
|
|
107
|
-
serviceName?: string;
|
|
108
|
-
serviceMethod?: string;
|
|
109
|
-
}
|
|
110
|
-
type RouteParams = InitialRouteParams & Record<string, unknown>;
|
|
111
|
-
type RoutePathsAndAttributes<Params = {}> = Omit<Route<Params>, 'element'>;
|
|
112
|
-
|
|
113
|
-
export { type Config, type FetchConfig, type InitialRouteParams, type Manifest, type ManifestEntry, type NamedService, type ProcessedConfig, type RenderCallbacks, type RenderModule, type RenderSSR, type RenderStream, type Route, type RouteAttributes, type RouteParams, type RoutePathsAndAttributes, type SSRManifest, SSRServer, type SSRServerOptions, type ServiceMethod, type ServiceRegistry, TEMPLATE, createMaps, processConfigs };
|
|
1
|
+
export { C as Config, F as FetchConfig, I as InitialRouteParams, f as Manifest, M as ManifestEntry, N as NamedService, P as ProcessedConfig, R as RenderCallbacks, i as RenderModule, g as RenderSSR, h as RenderStream, k as Route, j as RouteAttributes, l as RouteParams, m as RoutePathsAndAttributes, e as SSRManifest, S as SSRServer, a as SSRServerOptions, b as ServiceMethod, d as ServiceRegistry, T as TEMPLATE, c as createMaps, p as processConfigs } from './security/csp.js';
|
|
2
|
+
import 'node:http';
|
|
3
|
+
import 'fastify';
|
|
4
|
+
import 'vite';
|
package/dist/index.js
CHANGED
|
@@ -207,6 +207,53 @@ var TEMPLATE = {
|
|
|
207
207
|
defaultEntryServer: "entry-server",
|
|
208
208
|
defaultHtmlTemplate: "index.html"
|
|
209
209
|
};
|
|
210
|
+
var DEV_CSP_DIRECTIVES = {
|
|
211
|
+
"default-src": ["'self'"],
|
|
212
|
+
"connect-src": ["'self'", "ws:", "http:"],
|
|
213
|
+
"style-src": ["'self'", "'unsafe-inline'"],
|
|
214
|
+
"img-src": ["'self'", "data:"]
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// src/security/csp.ts
|
|
218
|
+
import crypto from "crypto";
|
|
219
|
+
var defaultGenerateCSP = (directives, nonce) => {
|
|
220
|
+
const merged = { ...directives };
|
|
221
|
+
merged["script-src"] = merged["script-src"] || ["'self'"];
|
|
222
|
+
if (!merged["script-src"].some((v) => v.startsWith("'nonce-"))) merged["script-src"].push(`'nonce-${nonce}'`);
|
|
223
|
+
if (process.env.NODE_ENV !== "production") {
|
|
224
|
+
const connect = merged["connect-src"] || ["'self'"];
|
|
225
|
+
if (!connect.includes("ws:")) connect.push("ws:");
|
|
226
|
+
if (!connect.includes("http:")) connect.push("http:");
|
|
227
|
+
merged["connect-src"] = connect;
|
|
228
|
+
const style = merged["style-src"] || ["'self'"];
|
|
229
|
+
if (!style.includes("'unsafe-inline'")) style.push("'unsafe-inline'");
|
|
230
|
+
merged["style-src"] = style;
|
|
231
|
+
}
|
|
232
|
+
return Object.entries(merged).map(([key, values]) => `${key} ${values.join(" ")}`).join("; ");
|
|
233
|
+
};
|
|
234
|
+
var generateNonce = () => crypto.randomBytes(16).toString("base64");
|
|
235
|
+
var cspHook = (options = {}) => (req, reply, done) => {
|
|
236
|
+
const nonce = generateNonce();
|
|
237
|
+
const directives = options.directives ?? DEV_CSP_DIRECTIVES;
|
|
238
|
+
const generate = options.generateCSP ?? defaultGenerateCSP;
|
|
239
|
+
const cspHeader = generate(directives, nonce);
|
|
240
|
+
reply.header("Content-Security-Policy", cspHeader);
|
|
241
|
+
if (typeof options.exposeNonce === "function") {
|
|
242
|
+
options.exposeNonce(req, nonce);
|
|
243
|
+
} else {
|
|
244
|
+
req.nonce = nonce;
|
|
245
|
+
}
|
|
246
|
+
done();
|
|
247
|
+
};
|
|
248
|
+
var applyCSP = (security, reply) => {
|
|
249
|
+
if (!security?.csp) return;
|
|
250
|
+
const nonce = generateNonce();
|
|
251
|
+
const { directives = {}, generateCSP = defaultGenerateCSP } = security.csp;
|
|
252
|
+
const header = generateCSP(directives, nonce);
|
|
253
|
+
reply.header("Content-Security-Policy", header);
|
|
254
|
+
reply.request.nonce = nonce;
|
|
255
|
+
return nonce;
|
|
256
|
+
};
|
|
210
257
|
|
|
211
258
|
// src/utils/Utils.ts
|
|
212
259
|
import { dirname, join } from "path";
|
|
@@ -420,6 +467,13 @@ var SSRServer = (0, import_fastify_plugin.default)(
|
|
|
420
467
|
root: baseClientRoot,
|
|
421
468
|
wildcard: false
|
|
422
469
|
});
|
|
470
|
+
app.addHook(
|
|
471
|
+
"onRequest",
|
|
472
|
+
cspHook({
|
|
473
|
+
directives: opts.security?.csp?.directives ?? DEV_CSP_DIRECTIVES,
|
|
474
|
+
generateCSP: opts.security?.csp?.generateCSP
|
|
475
|
+
})
|
|
476
|
+
);
|
|
423
477
|
if (isDevelopment) {
|
|
424
478
|
const { createServer } = await import("vite");
|
|
425
479
|
viteDevServer = await createServer({
|
|
@@ -477,6 +531,7 @@ var SSRServer = (0, import_fastify_plugin.default)(
|
|
|
477
531
|
if (/\.\w+$/.test(req.raw.url ?? "")) return reply.callNotFound();
|
|
478
532
|
const url = req.url ? new URL(req.url, `http://${req.headers.host}`).pathname : "/";
|
|
479
533
|
const matchedRoute = matchRoute(url, routes);
|
|
534
|
+
const nonce = applyCSP(opts.security, reply);
|
|
480
535
|
if (!matchedRoute) {
|
|
481
536
|
reply.callNotFound();
|
|
482
537
|
return;
|
|
@@ -518,12 +573,12 @@ var SSRServer = (0, import_fastify_plugin.default)(
|
|
|
518
573
|
if (renderType === RENDERTYPE.ssr) {
|
|
519
574
|
const { renderSSR } = renderModule;
|
|
520
575
|
const initialDataResolved = await initialDataPromise;
|
|
521
|
-
const initialDataScript = `<script>window.__INITIAL_DATA__ = ${JSON.stringify(initialDataResolved).replace(/</g, "\\u003c")}</script>`;
|
|
576
|
+
const initialDataScript = `<script nonce="${nonce}">window.__INITIAL_DATA__ = ${JSON.stringify(initialDataResolved).replace(/</g, "\\u003c")}</script>`;
|
|
522
577
|
const { headContent, appHtml } = await renderSSR(initialDataResolved, req.url, attr?.meta);
|
|
523
578
|
let aggregateHeadContent = headContent;
|
|
524
579
|
if (ssrManifest && preloadLink) aggregateHeadContent += preloadLink;
|
|
525
580
|
if (manifest && cssLink) aggregateHeadContent += cssLink;
|
|
526
|
-
const fullHtml = template.replace(SSRTAG.ssrHead, aggregateHeadContent).replace(SSRTAG.ssrHtml, `${appHtml}${initialDataScript}<script type="module" src="${bootstrapModule}" defer></script>`);
|
|
581
|
+
const fullHtml = template.replace(SSRTAG.ssrHead, aggregateHeadContent).replace(SSRTAG.ssrHtml, `${appHtml}${initialDataScript}<script nonce="${nonce}" type="module" src="${bootstrapModule}" defer></script>`);
|
|
527
582
|
return reply.status(200).header("Content-Type", "text/html").send(fullHtml);
|
|
528
583
|
} else {
|
|
529
584
|
const { renderStream } = renderModule;
|
|
@@ -538,7 +593,7 @@ var SSRServer = (0, import_fastify_plugin.default)(
|
|
|
538
593
|
reply.raw.write(`${beforeHead}${aggregateHeadContent}${afterHead}`);
|
|
539
594
|
},
|
|
540
595
|
onFinish: async (initialDataResolved) => {
|
|
541
|
-
reply.raw.write(`<script>window.__INITIAL_DATA__ = ${JSON.stringify(initialDataResolved).replace(/</g, "\\u003c")}</script>`);
|
|
596
|
+
reply.raw.write(`<script nonce="${nonce}">window.__INITIAL_DATA__ = ${JSON.stringify(initialDataResolved).replace(/</g, "\\u003c")}</script>`);
|
|
542
597
|
reply.raw.write(afterBody);
|
|
543
598
|
reply.raw.end();
|
|
544
599
|
},
|
|
@@ -565,12 +620,13 @@ var SSRServer = (0, import_fastify_plugin.default)(
|
|
|
565
620
|
const defaultConfig = processedConfigs[0];
|
|
566
621
|
if (!defaultConfig) throw new Error("No default configuration found.");
|
|
567
622
|
const { clientRoot } = defaultConfig;
|
|
623
|
+
const nonce = applyCSP(opts.security, reply);
|
|
568
624
|
let template = ensureNonNull(templates.get(clientRoot), `Template not found for clientRoot: ${clientRoot}`);
|
|
569
625
|
const cssLink = cssLinks.get(clientRoot);
|
|
570
626
|
const bootstrapModule = bootstrapModules.get(clientRoot);
|
|
571
627
|
template = template.replace(SSRTAG.ssrHead, "").replace(SSRTAG.ssrHtml, "");
|
|
572
628
|
if (!isDevelopment && cssLink) template = template.replace("</head>", `${cssLink}</head>`);
|
|
573
|
-
if (bootstrapModule) template = template.replace("</body>", `<script type="module" src="${bootstrapModule}" defer></script></body>`);
|
|
629
|
+
if (bootstrapModule) template = template.replace("</body>", `<script nonce="${nonce}" type="module" src="${bootstrapModule}" defer></script></body>`);
|
|
574
630
|
reply.status(200).type("text/html").send(template);
|
|
575
631
|
} catch (error) {
|
|
576
632
|
console.error("Failed to serve clientHtmlTemplate:", error);
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { ServerResponse } from 'node:http';
|
|
2
|
+
import { FastifyRequest, FastifyReply, HookHandlerDoneFunction, FastifyPluginAsync } from 'fastify';
|
|
3
|
+
import { PluginOption } from 'vite';
|
|
4
|
+
|
|
5
|
+
type CSPDirectives = Record<string, string[]>;
|
|
6
|
+
interface CSPOptions {
|
|
7
|
+
directives?: CSPDirectives;
|
|
8
|
+
exposeNonce?: (req: FastifyRequest, nonce: string) => void;
|
|
9
|
+
generateCSP?: (directives: CSPDirectives, nonce: string) => string;
|
|
10
|
+
}
|
|
11
|
+
declare const defaultGenerateCSP: (directives: CSPDirectives, nonce: string) => string;
|
|
12
|
+
declare const generateNonce: () => string;
|
|
13
|
+
declare const cspHook: (options?: CSPOptions) => (req: FastifyRequest, reply: FastifyReply, done: HookHandlerDoneFunction) => void;
|
|
14
|
+
declare const getRequestNonce: (req: FastifyRequest) => string | undefined;
|
|
15
|
+
declare const applyCSP: (security: SSRServerOptions["security"], reply: FastifyReply) => string | undefined;
|
|
16
|
+
|
|
17
|
+
declare const RENDERTYPE: {
|
|
18
|
+
ssr: string;
|
|
19
|
+
streaming: string;
|
|
20
|
+
};
|
|
21
|
+
declare const TEMPLATE: {
|
|
22
|
+
defaultEntryClient: string;
|
|
23
|
+
defaultEntryServer: string;
|
|
24
|
+
defaultHtmlTemplate: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
declare const createMaps: () => {
|
|
28
|
+
bootstrapModules: Map<string, string>;
|
|
29
|
+
cssLinks: Map<string, string>;
|
|
30
|
+
manifests: Map<string, Manifest>;
|
|
31
|
+
preloadLinks: Map<string, string>;
|
|
32
|
+
renderModules: Map<string, RenderModule>;
|
|
33
|
+
ssrManifests: Map<string, SSRManifest>;
|
|
34
|
+
templates: Map<string, string>;
|
|
35
|
+
};
|
|
36
|
+
declare const processConfigs: (configs: Config[], baseClientRoot: string, templateDefaults: typeof TEMPLATE) => ProcessedConfig[];
|
|
37
|
+
declare const SSRServer: FastifyPluginAsync<SSRServerOptions>;
|
|
38
|
+
type Config = {
|
|
39
|
+
appId: string;
|
|
40
|
+
entryPoint: string;
|
|
41
|
+
entryClient?: string;
|
|
42
|
+
entryServer?: string;
|
|
43
|
+
htmlTemplate?: string;
|
|
44
|
+
};
|
|
45
|
+
type ProcessedConfig = {
|
|
46
|
+
appId: string;
|
|
47
|
+
clientRoot: string;
|
|
48
|
+
entryClient: string;
|
|
49
|
+
entryPoint: string;
|
|
50
|
+
entryServer: string;
|
|
51
|
+
htmlTemplate: string;
|
|
52
|
+
plugins?: PluginOption[];
|
|
53
|
+
};
|
|
54
|
+
type SSRServerOptions = {
|
|
55
|
+
alias?: Record<string, string>;
|
|
56
|
+
clientRoot: string;
|
|
57
|
+
configs: Config[];
|
|
58
|
+
routes: Route<RouteParams>[];
|
|
59
|
+
serviceRegistry: ServiceRegistry;
|
|
60
|
+
security?: {
|
|
61
|
+
csp?: {
|
|
62
|
+
directives?: CSPDirectives;
|
|
63
|
+
generateCSP?: (directives: CSPDirectives, nonce: string) => string;
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
isDebug?: boolean;
|
|
67
|
+
};
|
|
68
|
+
type ServiceMethod = (params: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
69
|
+
type NamedService = Record<string, ServiceMethod>;
|
|
70
|
+
type ServiceRegistry = Record<string, NamedService>;
|
|
71
|
+
type RenderCallbacks = {
|
|
72
|
+
onHead: (headContent: string) => void;
|
|
73
|
+
onFinish: (initialDataResolved: unknown) => void;
|
|
74
|
+
onError: (error: unknown) => void;
|
|
75
|
+
};
|
|
76
|
+
type FetchConfig = {
|
|
77
|
+
url?: string;
|
|
78
|
+
options: RequestInit & {
|
|
79
|
+
params?: Record<string, unknown>;
|
|
80
|
+
};
|
|
81
|
+
serviceName?: string;
|
|
82
|
+
serviceMethod?: string;
|
|
83
|
+
};
|
|
84
|
+
type SSRManifest = {
|
|
85
|
+
[key: string]: string[];
|
|
86
|
+
};
|
|
87
|
+
type ManifestEntry = {
|
|
88
|
+
file: string;
|
|
89
|
+
src?: string;
|
|
90
|
+
isDynamicEntry?: boolean;
|
|
91
|
+
imports?: string[];
|
|
92
|
+
css?: string[];
|
|
93
|
+
assets?: string[];
|
|
94
|
+
};
|
|
95
|
+
type Manifest = {
|
|
96
|
+
[key: string]: ManifestEntry;
|
|
97
|
+
};
|
|
98
|
+
type RenderSSR = (initialDataResolved: Record<string, unknown>, location: string, meta?: Record<string, unknown>) => Promise<{
|
|
99
|
+
headContent: string;
|
|
100
|
+
appHtml: string;
|
|
101
|
+
initialDataScript: string;
|
|
102
|
+
}>;
|
|
103
|
+
type RenderStream = (serverResponse: ServerResponse, callbacks: RenderCallbacks, initialDataPromise: Promise<Record<string, unknown>>, location: string, bootstrapModules?: string, meta?: Record<string, unknown>) => void;
|
|
104
|
+
type RenderModule = {
|
|
105
|
+
renderSSR: RenderSSR;
|
|
106
|
+
renderStream: RenderStream;
|
|
107
|
+
};
|
|
108
|
+
type RouteAttributes<Params = {}> = {
|
|
109
|
+
fetch?: (params?: Params, options?: RequestInit & {
|
|
110
|
+
params?: Record<string, unknown>;
|
|
111
|
+
}) => Promise<FetchConfig>;
|
|
112
|
+
} & ({
|
|
113
|
+
render?: typeof RENDERTYPE.ssr;
|
|
114
|
+
meta?: Record<string, unknown>;
|
|
115
|
+
} | {
|
|
116
|
+
render: typeof RENDERTYPE.streaming;
|
|
117
|
+
meta: Record<string, unknown>;
|
|
118
|
+
});
|
|
119
|
+
type Route<Params = {}> = {
|
|
120
|
+
attr?: RouteAttributes<Params>;
|
|
121
|
+
path: string;
|
|
122
|
+
appId?: string;
|
|
123
|
+
};
|
|
124
|
+
interface InitialRouteParams extends Record<string, unknown> {
|
|
125
|
+
serviceName?: string;
|
|
126
|
+
serviceMethod?: string;
|
|
127
|
+
}
|
|
128
|
+
type RouteParams = InitialRouteParams & Record<string, unknown>;
|
|
129
|
+
type RoutePathsAndAttributes<Params = {}> = Omit<Route<Params>, 'element'>;
|
|
130
|
+
|
|
131
|
+
export { type Config as C, type CSPDirectives, type CSPOptions, type FetchConfig as F, type InitialRouteParams as I, type ManifestEntry as M, type NamedService as N, type ProcessedConfig as P, type RenderCallbacks as R, SSRServer as S, TEMPLATE as T, type SSRServerOptions as a, applyCSP, type ServiceMethod as b, createMaps as c, cspHook, type ServiceRegistry as d, defaultGenerateCSP, type SSRManifest as e, type Manifest as f, type RenderSSR as g, generateNonce, getRequestNonce, type RenderStream as h, type RenderModule as i, type RouteAttributes as j, type Route as k, type RouteParams as l, type RoutePathsAndAttributes as m, processConfigs as p };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// src/security/csp.ts
|
|
2
|
+
import crypto from "crypto";
|
|
3
|
+
|
|
4
|
+
// src/constants.ts
|
|
5
|
+
var DEV_CSP_DIRECTIVES = {
|
|
6
|
+
"default-src": ["'self'"],
|
|
7
|
+
"connect-src": ["'self'", "ws:", "http:"],
|
|
8
|
+
"style-src": ["'self'", "'unsafe-inline'"],
|
|
9
|
+
"img-src": ["'self'", "data:"]
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/security/csp.ts
|
|
13
|
+
var defaultGenerateCSP = (directives, nonce) => {
|
|
14
|
+
const merged = { ...directives };
|
|
15
|
+
merged["script-src"] = merged["script-src"] || ["'self'"];
|
|
16
|
+
if (!merged["script-src"].some((v) => v.startsWith("'nonce-"))) merged["script-src"].push(`'nonce-${nonce}'`);
|
|
17
|
+
if (process.env.NODE_ENV !== "production") {
|
|
18
|
+
const connect = merged["connect-src"] || ["'self'"];
|
|
19
|
+
if (!connect.includes("ws:")) connect.push("ws:");
|
|
20
|
+
if (!connect.includes("http:")) connect.push("http:");
|
|
21
|
+
merged["connect-src"] = connect;
|
|
22
|
+
const style = merged["style-src"] || ["'self'"];
|
|
23
|
+
if (!style.includes("'unsafe-inline'")) style.push("'unsafe-inline'");
|
|
24
|
+
merged["style-src"] = style;
|
|
25
|
+
}
|
|
26
|
+
return Object.entries(merged).map(([key, values]) => `${key} ${values.join(" ")}`).join("; ");
|
|
27
|
+
};
|
|
28
|
+
var generateNonce = () => crypto.randomBytes(16).toString("base64");
|
|
29
|
+
var cspHook = (options = {}) => (req, reply, done) => {
|
|
30
|
+
const nonce = generateNonce();
|
|
31
|
+
const directives = options.directives ?? DEV_CSP_DIRECTIVES;
|
|
32
|
+
const generate = options.generateCSP ?? defaultGenerateCSP;
|
|
33
|
+
const cspHeader = generate(directives, nonce);
|
|
34
|
+
reply.header("Content-Security-Policy", cspHeader);
|
|
35
|
+
if (typeof options.exposeNonce === "function") {
|
|
36
|
+
options.exposeNonce(req, nonce);
|
|
37
|
+
} else {
|
|
38
|
+
req.nonce = nonce;
|
|
39
|
+
}
|
|
40
|
+
done();
|
|
41
|
+
};
|
|
42
|
+
var getRequestNonce = (req) => req.nonce;
|
|
43
|
+
var applyCSP = (security, reply) => {
|
|
44
|
+
if (!security?.csp) return;
|
|
45
|
+
const nonce = generateNonce();
|
|
46
|
+
const { directives = {}, generateCSP = defaultGenerateCSP } = security.csp;
|
|
47
|
+
const header = generateCSP(directives, nonce);
|
|
48
|
+
reply.header("Content-Security-Policy", header);
|
|
49
|
+
reply.request.nonce = nonce;
|
|
50
|
+
return nonce;
|
|
51
|
+
};
|
|
52
|
+
export {
|
|
53
|
+
applyCSP,
|
|
54
|
+
cspHook,
|
|
55
|
+
defaultGenerateCSP,
|
|
56
|
+
generateNonce,
|
|
57
|
+
getRequestNonce
|
|
58
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taujs/server",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "taujs | τjs",
|
|
5
5
|
"author": "Aoede <taujs@aoede.uk.net> (https://www.aoede.uk.net)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,6 +33,10 @@
|
|
|
33
33
|
"import": "./dist/build.js",
|
|
34
34
|
"types": "./dist/build.d.ts"
|
|
35
35
|
},
|
|
36
|
+
"./csp": {
|
|
37
|
+
"import": "./dist/security/csp.js",
|
|
38
|
+
"types": "./dist/security/csp.d.ts"
|
|
39
|
+
},
|
|
36
40
|
"./package.json": "./package.json"
|
|
37
41
|
},
|
|
38
42
|
"files": [
|
|
@@ -49,6 +53,7 @@
|
|
|
49
53
|
"@changesets/cli": "^2.27.7",
|
|
50
54
|
"@types/node": "^20.14.9",
|
|
51
55
|
"@vitest/coverage-v8": "^2.1.0",
|
|
56
|
+
"@vitest/ui": "^2.1.9",
|
|
52
57
|
"fastify": "^5.3.3",
|
|
53
58
|
"jsdom": "^25.0.0",
|
|
54
59
|
"prettier": "^3.3.3",
|