@taujs/server 0.2.3 → 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.d.ts CHANGED
@@ -5,8 +5,8 @@ type Config = {
5
5
  entryPoint: string;
6
6
  plugins?: PluginOption[];
7
7
  };
8
- declare function taujsBuild({ config, projectRoot, clientBaseDir, isSSRBuild, }: {
9
- config: Config[];
8
+ declare function taujsBuild({ configs, projectRoot, clientBaseDir, isSSRBuild, }: {
9
+ configs: Config[];
10
10
  projectRoot: string;
11
11
  clientBaseDir: string;
12
12
  isSSRBuild?: boolean;
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);
@@ -589,24 +644,25 @@ var SSRServer = (0, import_fastify_plugin.default)(
589
644
 
590
645
  // src/build.ts
591
646
  async function taujsBuild({
592
- config,
647
+ configs,
593
648
  projectRoot,
594
649
  clientBaseDir,
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 fs.rm(distPath, { recursive: true, force: true });
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);
604
660
  }
605
661
  };
606
- const processedConfigs = processConfigs(config, clientBaseDir, TEMPLATE);
662
+ const processedConfigs = processConfigs(configs, clientBaseDir, TEMPLATE);
607
663
  if (!isSSRBuild) await deleteDist();
608
- for (const config2 of processedConfigs) {
609
- const { appId, entryPoint, clientRoot, entryClient, entryServer, htmlTemplate, plugins = [] } = config2;
664
+ for (const config of processedConfigs) {
665
+ const { appId, entryPoint, clientRoot, entryClient, entryServer, htmlTemplate, plugins = [] } = config;
610
666
  const outDir = path3.resolve(projectRoot, `dist/client/${entryPoint}`);
611
667
  const root = entryPoint ? path3.resolve(clientBaseDir, entryPoint) : clientBaseDir;
612
668
  const server = path3.resolve(clientRoot, `${entryServer}.tsx`);
@@ -632,7 +688,7 @@ async function taujsBuild({
632
688
  scss: { api: "modern-compiler" }
633
689
  }
634
690
  },
635
- plugins: [...config2.plugins ?? [], nodePolyfills({ include: ["fs", "stream"] })],
691
+ plugins: [...config.plugins ?? [], nodePolyfills({ include: ["fs", "stream"] })],
636
692
  publicDir: "public",
637
693
  resolve: {
638
694
  alias: {
package/dist/index.d.ts CHANGED
@@ -1,115 +1,4 @@
1
- import { ServerResponse } from 'node:http';
2
- import { FastifyPluginAsync } from 'fastify';
3
- import { PluginOption } from 'vite';
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 ServiceRegistry = {
51
- [serviceName: string]: {
52
- [methodName: string]: (params: Record<string, unknown>) => Promise<Record<string, unknown>>;
53
- };
54
- };
55
- type RenderCallbacks = {
56
- onHead: (headContent: string) => void;
57
- onFinish: (initialDataResolved: unknown) => void;
58
- onError: (error: unknown) => void;
59
- };
60
- type FetchConfig = {
61
- url?: string;
62
- options: RequestInit & {
63
- params?: Record<string, unknown>;
64
- };
65
- serviceName?: string;
66
- serviceMethod?: string;
67
- };
68
- type SSRManifest = {
69
- [key: string]: string[];
70
- };
71
- type ManifestEntry = {
72
- file: string;
73
- src?: string;
74
- isDynamicEntry?: boolean;
75
- imports?: string[];
76
- css?: string[];
77
- assets?: string[];
78
- };
79
- type Manifest = {
80
- [key: string]: ManifestEntry;
81
- };
82
- type RenderSSR = (initialDataResolved: Record<string, unknown>, location: string, meta?: Record<string, unknown>) => Promise<{
83
- headContent: string;
84
- appHtml: string;
85
- initialDataScript: string;
86
- }>;
87
- type RenderStream = (serverResponse: ServerResponse, callbacks: RenderCallbacks, initialDataPromise: Promise<Record<string, unknown>>, location: string, bootstrapModules?: string, meta?: Record<string, unknown>) => void;
88
- type RenderModule = {
89
- renderSSR: RenderSSR;
90
- renderStream: RenderStream;
91
- };
92
- type RouteAttributes<Params = {}> = {
93
- fetch?: (params?: Params, options?: RequestInit & {
94
- params?: Record<string, unknown>;
95
- }) => Promise<FetchConfig>;
96
- } & ({
97
- render?: typeof RENDERTYPE.ssr;
98
- meta?: Record<string, unknown>;
99
- } | {
100
- render: typeof RENDERTYPE.streaming;
101
- meta: Record<string, unknown>;
102
- });
103
- type Route<Params = {}> = {
104
- attr?: RouteAttributes<Params>;
105
- path: string;
106
- appId?: string;
107
- };
108
- interface InitialRouteParams extends Record<string, unknown> {
109
- serviceName?: string;
110
- serviceMethod?: string;
111
- }
112
- type RouteParams = InitialRouteParams & Record<string, unknown>;
113
- type RoutePathsAndAttributes<Params = {}> = Omit<Route<Params>, 'element'>;
114
-
115
- export { type Config, type FetchConfig, type InitialRouteParams, type Manifest, type ManifestEntry, type ProcessedConfig, type RenderCallbacks, type RenderModule, type RenderSSR, type RenderStream, type Route, type RouteAttributes, type RouteParams, type RoutePathsAndAttributes, type SSRManifest, SSRServer, type SSRServerOptions, 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",
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",