@taujs/server 0.2.7 → 0.3.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 CHANGED
@@ -1,6 +1,8 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) Aoede Ltd 2024
3
+ taujs [ τjs ] Orchestration System
4
+ Author: John Smith
5
+ Copyright (c) Aoede Ltd 2024-present
4
6
 
5
7
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
8
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # @taujs/server
2
2
 
3
+ This package is part of the taujs [ τjs ] orchestration system, authored by John Smith | Aoede, 2024-present. Attribution is appreciated.
4
+
3
5
  `npm install @taujs/server`
4
6
 
5
7
  `yarn add @taujs/server`
@@ -0,0 +1,150 @@
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 createCSPHook: (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 TEMPLATE: {
18
+ readonly defaultEntryClient: "entry-client";
19
+ readonly defaultEntryServer: "entry-server";
20
+ readonly defaultHtmlTemplate: "index.html";
21
+ };
22
+
23
+ /**
24
+ * taujs [ τjs ] Orchestration System
25
+ * (c) 2024-present Aoede Ltd
26
+ * Author: John Smith
27
+ *
28
+ * Licensed under the MIT License — attribution appreciated.
29
+ * Part of the taujs [ τjs ] system for declarative, build-time orchestration of microfrontend applications,
30
+ * including CSR, SSR, streaming, and middleware composition.
31
+ */
32
+
33
+ declare const createMaps: () => {
34
+ bootstrapModules: Map<string, string>;
35
+ cssLinks: Map<string, string>;
36
+ manifests: Map<string, Manifest>;
37
+ preloadLinks: Map<string, string>;
38
+ renderModules: Map<string, RenderModule>;
39
+ ssrManifests: Map<string, SSRManifest>;
40
+ templates: Map<string, string>;
41
+ };
42
+ declare const processConfigs: (configs: Config[], baseClientRoot: string, templateDefaults: typeof TEMPLATE) => ProcessedConfig[];
43
+ declare const SSRServer: FastifyPluginAsync<SSRServerOptions>;
44
+ type Config = {
45
+ appId: string;
46
+ entryPoint: string;
47
+ entryClient?: string;
48
+ entryServer?: string;
49
+ htmlTemplate?: string;
50
+ };
51
+ type ProcessedConfig = {
52
+ appId: string;
53
+ clientRoot: string;
54
+ entryClient: string;
55
+ entryPoint: string;
56
+ entryServer: string;
57
+ htmlTemplate: string;
58
+ plugins?: PluginOption[];
59
+ };
60
+ type SSRServerOptions = {
61
+ alias?: Record<string, string>;
62
+ clientRoot: string;
63
+ configs: Config[];
64
+ routes: Route<RouteParams>[];
65
+ serviceRegistry: ServiceRegistry;
66
+ security?: {
67
+ csp?: {
68
+ directives?: CSPDirectives;
69
+ generateCSP?: (directives: CSPDirectives, nonce: string) => string;
70
+ };
71
+ };
72
+ isDebug?: boolean;
73
+ };
74
+ type ServiceMethod = (params: Record<string, unknown>) => Promise<Record<string, unknown>>;
75
+ type NamedService = Record<string, ServiceMethod>;
76
+ type ServiceRegistry = Record<string, NamedService>;
77
+ type RenderCallbacks = {
78
+ onHead: (headContent: string) => void;
79
+ onFinish: (initialDataResolved: unknown) => void;
80
+ onError: (error: unknown) => void;
81
+ };
82
+ type FetchConfig = {
83
+ url?: string;
84
+ options: RequestInit & {
85
+ params?: Record<string, unknown>;
86
+ };
87
+ serviceName?: string;
88
+ serviceMethod?: string;
89
+ };
90
+ type SSRManifest = {
91
+ [key: string]: string[];
92
+ };
93
+ type ManifestEntry = {
94
+ file: string;
95
+ src?: string;
96
+ isDynamicEntry?: boolean;
97
+ imports?: string[];
98
+ css?: string[];
99
+ assets?: string[];
100
+ };
101
+ type Manifest = {
102
+ [key: string]: ManifestEntry;
103
+ };
104
+ type RenderSSR = (initialDataResolved: Record<string, unknown>, location: string, meta?: Record<string, unknown>) => Promise<{
105
+ headContent: string;
106
+ appHtml: string;
107
+ }>;
108
+ type RenderStream = (serverResponse: ServerResponse, callbacks: RenderCallbacks, initialDataPromise: Promise<Record<string, unknown>>, location: string, bootstrapModules?: string, meta?: Record<string, unknown>) => void;
109
+ type RenderModule = {
110
+ renderSSR: RenderSSR;
111
+ renderStream: RenderStream;
112
+ };
113
+ type BaseMiddleware = {
114
+ auth?: {
115
+ required: boolean;
116
+ redirect?: string;
117
+ roles?: string[];
118
+ strategy?: string;
119
+ };
120
+ };
121
+ type RouteAttributes<Params = {}, Middleware = BaseMiddleware> = {
122
+ render: 'ssr';
123
+ hydrate?: boolean;
124
+ meta?: Record<string, unknown>;
125
+ middleware?: Middleware;
126
+ fetch?: (params?: Params, options?: RequestInit & {
127
+ params?: Record<string, unknown>;
128
+ }) => Promise<FetchConfig>;
129
+ } | {
130
+ render: 'streaming';
131
+ hydrate?: never;
132
+ meta: Record<string, unknown>;
133
+ middleware?: Middleware;
134
+ fetch?: (params?: Params, options?: RequestInit & {
135
+ params?: Record<string, unknown>;
136
+ }) => Promise<FetchConfig>;
137
+ };
138
+ type Route<Params = {}> = {
139
+ attr?: RouteAttributes<Params>;
140
+ path: string;
141
+ appId?: string;
142
+ };
143
+ interface InitialRouteParams extends Record<string, unknown> {
144
+ serviceName?: string;
145
+ serviceMethod?: string;
146
+ }
147
+ type RouteParams = InitialRouteParams & Record<string, unknown>;
148
+ type RoutePathsAndAttributes<Params = {}> = Omit<Route<Params>, 'element'>;
149
+
150
+ export { type BaseMiddleware as B, type Config as C, type FetchConfig as F, type InitialRouteParams as I, type ManifestEntry as M, type NamedService as N, type ProcessedConfig as P, type Route as R, SSRServer as S, TEMPLATE as T, type RouteParams as a, type RouteAttributes as b, createMaps as c, type SSRServerOptions as d, type ServiceMethod as e, type ServiceRegistry as f, type RenderCallbacks as g, type SSRManifest as h, type Manifest as i, type RenderSSR as j, type RenderStream as k, type RenderModule as l, type RoutePathsAndAttributes as m, type CSPDirectives as n, type CSPOptions as o, processConfigs as p, defaultGenerateCSP as q, generateNonce as r, createCSPHook as s, getRequestNonce as t, applyCSP as u };
package/dist/build.d.ts CHANGED
@@ -1,15 +1,24 @@
1
- import { PluginOption } from 'vite';
1
+ import { AppConfig } from './config.js';
2
+ import 'vite';
3
+ import './SSRServer-C7MMCfVq.js';
4
+ import 'node:http';
5
+ import 'fastify';
6
+
7
+ /**
8
+ * taujs [ τjs ] Orchestration System
9
+ * (c) 2024-present Aoede Ltd
10
+ * Author: John Smith
11
+ *
12
+ * Licensed under the MIT License — attribution appreciated.
13
+ * Part of the taujs [ τjs ] system for declarative, build-time orchestration of microfrontend applications,
14
+ * including CSR, SSR, streaming, and middleware composition.
15
+ */
2
16
 
3
- type Config = {
4
- appId: string;
5
- entryPoint: string;
6
- plugins?: PluginOption[];
7
- };
8
17
  declare function taujsBuild({ configs, projectRoot, clientBaseDir, isSSRBuild, }: {
9
- configs: Config[];
18
+ configs: AppConfig[];
10
19
  projectRoot: string;
11
20
  clientBaseDir: string;
12
21
  isSSRBuild?: boolean;
13
22
  }): Promise<void>;
14
23
 
15
- export { type Config, taujsBuild };
24
+ export { taujsBuild };
package/dist/build.js CHANGED
@@ -198,6 +198,26 @@ var import_picocolors = __toESM(require_picocolors(), 1);
198
198
  import { readFile } from "fs/promises";
199
199
  import path2 from "path";
200
200
 
201
+ // src/utils/Logger.ts
202
+ var createLogger = (debug) => ({
203
+ log: (...args) => {
204
+ if (debug) console.log(...args);
205
+ },
206
+ warn: (...args) => {
207
+ if (debug) console.warn(...args);
208
+ },
209
+ error: (...args) => {
210
+ if (debug) console.error(...args);
211
+ }
212
+ });
213
+ var debugLog = (logger, message, req) => {
214
+ const prefix = "[\u03C4js]";
215
+ const method = req?.method ?? "";
216
+ const url = req?.url ?? "";
217
+ const tag = method && url ? `${method} ${url}` : "";
218
+ logger.log(`${prefix} ${tag} ${message}`);
219
+ };
220
+
201
221
  // src/constants.ts
202
222
  var RENDERTYPE = {
203
223
  ssr: "ssr",
@@ -219,6 +239,32 @@ var DEV_CSP_DIRECTIVES = {
219
239
  "img-src": ["'self'", "data:"]
220
240
  };
221
241
 
242
+ // src/security/auth.ts
243
+ function createAuthHook(routes, isDebug) {
244
+ const logger = createLogger(Boolean(isDebug));
245
+ return async function authHook(req, reply) {
246
+ const url = new URL(req.url, `http://${req.headers.host}`).pathname;
247
+ const matched = routes.find((r) => r.path === url);
248
+ const authConfig = matched?.attr?.middleware?.auth;
249
+ if (!authConfig?.required) {
250
+ debugLog(logger, "Auth not required for route", req);
251
+ return;
252
+ }
253
+ if (typeof req.server.authenticate !== "function") {
254
+ req.log.warn('Route requires auth but no "authenticate" decorator is defined on Fastify.');
255
+ return reply.status(500).send("Server misconfiguration: auth decorator missing.");
256
+ }
257
+ try {
258
+ debugLog(logger, "Invoking authenticate(...)", req);
259
+ await req.server.authenticate(req, reply);
260
+ debugLog(logger, "Authentication successful", req);
261
+ } catch (err) {
262
+ debugLog(logger, "Authentication failed", req);
263
+ return reply.send(err);
264
+ }
265
+ };
266
+ }
267
+
222
268
  // src/security/csp.ts
223
269
  import crypto from "crypto";
224
270
  var defaultGenerateCSP = (directives, nonce) => {
@@ -237,7 +283,7 @@ var defaultGenerateCSP = (directives, nonce) => {
237
283
  return Object.entries(merged).map(([key, values]) => `${key} ${values.join(" ")}`).join("; ");
238
284
  };
239
285
  var generateNonce = () => crypto.randomBytes(16).toString("base64");
240
- var cspHook = (options = {}) => (req, reply, done) => {
286
+ var createCSPHook = (options = {}) => (req, reply, done) => {
241
287
  const nonce = generateNonce();
242
288
  const directives = options.directives ?? DEV_CSP_DIRECTIVES;
243
289
  const generate = options.generateCSP ?? defaultGenerateCSP;
@@ -260,6 +306,26 @@ var applyCSP = (security, reply) => {
260
306
  return nonce;
261
307
  };
262
308
 
309
+ // src/security/verifyMiddleware.ts
310
+ var isAuthRequired = (r) => r.attr?.middleware?.auth?.required === true;
311
+ var hasAuthenticate = (app) => typeof app.authenticate === "function";
312
+ var verifyContracts = (app, routes, contracts, isDebug) => {
313
+ const logger = createLogger(Boolean(isDebug));
314
+ for (const contract of contracts) {
315
+ const isUsed = routes.some(contract.required);
316
+ if (!isUsed) {
317
+ debugLog(logger, `Middleware "${contract.key}" not used in any routes`);
318
+ continue;
319
+ }
320
+ if (!contract.verify(app)) {
321
+ const error = new Error(`[\u03C4js] ${contract.errorMessage}`);
322
+ logger.error(error.message);
323
+ throw error;
324
+ }
325
+ debugLog(logger, `Middleware "${contract.key}" verified \u2713`);
326
+ }
327
+ };
328
+
263
329
  // src/utils/Utils.ts
264
330
  import { dirname, join } from "path";
265
331
  import "path";
@@ -432,6 +498,7 @@ var processConfigs = (configs, baseClientRoot, templateDefaults) => {
432
498
  };
433
499
  var SSRServer = (0, import_fastify_plugin.default)(
434
500
  async (app, opts) => {
501
+ const logger = createLogger(opts.isDebug ?? false);
435
502
  const { alias, configs, routes, serviceRegistry, isDebug, clientRoot: baseClientRoot } = opts;
436
503
  const { bootstrapModules, cssLinks, manifests, preloadLinks, renderModules, ssrManifests, templates } = createMaps();
437
504
  const processedConfigs = processConfigs(configs, baseClientRoot, TEMPLATE);
@@ -465,6 +532,19 @@ var SSRServer = (0, import_fastify_plugin.default)(
465
532
  }
466
533
  }
467
534
  let viteDevServer;
535
+ verifyContracts(
536
+ app,
537
+ routes,
538
+ [
539
+ {
540
+ key: "auth",
541
+ required: isAuthRequired,
542
+ verify: hasAuthenticate,
543
+ errorMessage: "Routes require auth but Fastify instance is missing `.authenticate` decorator."
544
+ }
545
+ ],
546
+ opts.isDebug
547
+ );
468
548
  await app.register(import("@fastify/static"), {
469
549
  index: false,
470
550
  prefix: "/",
@@ -473,11 +553,12 @@ var SSRServer = (0, import_fastify_plugin.default)(
473
553
  });
474
554
  app.addHook(
475
555
  "onRequest",
476
- cspHook({
556
+ createCSPHook({
477
557
  directives: opts.security?.csp?.directives,
478
558
  generateCSP: opts.security?.csp?.generateCSP
479
559
  })
480
560
  );
561
+ app.addHook("onRequest", createAuthHook(routes));
481
562
  if (isDevelopment) {
482
563
  const { createServer } = await import("vite");
483
564
  viteDevServer = await createServer({
@@ -495,10 +576,10 @@ var SSRServer = (0, import_fastify_plugin.default)(
495
576
  {
496
577
  name: "taujs-development-server-debug-logging",
497
578
  configureServer(server) {
498
- console.log(import_picocolors.default.green("\u03C4js development server debug started."));
579
+ logger.log(import_picocolors.default.green("\u03C4js development server debug started."));
499
580
  server.middlewares.use((req, res, next) => {
500
- console.log(import_picocolors.default.cyan(`\u2190 rx: ${req.url}`));
501
- res.on("finish", () => console.log(import_picocolors.default.yellow(`\u2192 tx: ${req.url}`)));
581
+ logger.log(import_picocolors.default.cyan(`\u2190 rx: ${req.url}`));
582
+ res.on("finish", () => logger.log(import_picocolors.default.yellow(`\u2192 tx: ${req.url}`)));
502
583
  next();
503
584
  });
504
585
  }
@@ -582,7 +663,9 @@ var SSRServer = (0, import_fastify_plugin.default)(
582
663
  let aggregateHeadContent = headContent;
583
664
  if (ssrManifest && preloadLink) aggregateHeadContent += preloadLink;
584
665
  if (manifest && cssLink) aggregateHeadContent += cssLink;
585
- const fullHtml = template.replace(SSRTAG.ssrHead, aggregateHeadContent).replace(SSRTAG.ssrHtml, `${appHtml}${initialDataScript}<script nonce="${nonce}" type="module" src="${bootstrapModule}" defer></script>`);
666
+ const shouldHydrate = attr?.hydrate !== false;
667
+ const bootstrapScriptTag = shouldHydrate ? `<script nonce="${nonce}" type="module" src="${bootstrapModule}" defer></script>` : "";
668
+ const fullHtml = template.replace(SSRTAG.ssrHead, aggregateHeadContent).replace(SSRTAG.ssrHtml, `${appHtml}${initialDataScript}${bootstrapScriptTag}`);
586
669
  return reply.status(200).header("Content-Type", "text/html").send(fullHtml);
587
670
  } else {
588
671
  const { renderStream } = renderModule;
@@ -0,0 +1,37 @@
1
+ import { PluginOption } from 'vite';
2
+ import { R as Route, a as RouteParams, b as RouteAttributes } from './SSRServer-C7MMCfVq.js';
3
+ import 'node:http';
4
+ import 'fastify';
5
+
6
+ /**
7
+ * taujs [ τjs ] Orchestration System
8
+ * (c) 2024-present Aoede Ltd
9
+ * Author: John Smith
10
+ *
11
+ * Licensed under the MIT License — attribution appreciated.
12
+ * Part of the taujs [ τjs ] system for declarative, build-time orchestration of microfrontend applications,
13
+ * including CSR, SSR, streaming, and middleware composition.
14
+ */
15
+
16
+ type AppRoute = Omit<Route<RouteParams>, 'appId'> & {
17
+ attr?: RouteAttributes;
18
+ };
19
+ type AppConfig = {
20
+ appId: string;
21
+ entryPoint: string;
22
+ plugins?: PluginOption[];
23
+ routes?: AppRoute[];
24
+ };
25
+ type TaujsConfig = {
26
+ apps: AppConfig[];
27
+ };
28
+ declare const extractBuildConfigs: (config: {
29
+ apps: {
30
+ appId: string;
31
+ entryPoint: string;
32
+ plugins?: PluginOption[];
33
+ }[];
34
+ }) => AppConfig[];
35
+ declare const extractRoutes: (taujsConfig: TaujsConfig) => Route<RouteParams>[];
36
+
37
+ export { type AppConfig, type AppRoute, type TaujsConfig, extractBuildConfigs, extractRoutes };
package/dist/config.js ADDED
@@ -0,0 +1,146 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __commonJS = (cb, mod) => function __require() {
8
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
19
+ // If the importer is in node compatibility mode or this is not an ESM
20
+ // file that has been converted to a CommonJS file using a Babel-
21
+ // compatible transform (i.e. "__esModule" has not been set), then set
22
+ // "default" to the CommonJS "module.exports" for node compatibility.
23
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
24
+ mod
25
+ ));
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
+
100
+ // src/config.ts
101
+ var import_picocolors = __toESM(require_picocolors(), 1);
102
+ import { performance } from "perf_hooks";
103
+ var extractBuildConfigs = (config) => {
104
+ return config.apps.map(({ appId, entryPoint, plugins }) => ({
105
+ appId,
106
+ entryPoint,
107
+ plugins
108
+ }));
109
+ };
110
+ var extractRoutes = (taujsConfig) => {
111
+ console.log(import_picocolors.default.bold("Preparing taujs [ \u03C4js ]"));
112
+ const t0 = performance.now();
113
+ try {
114
+ const allRoutes = [];
115
+ const pathTracker = /* @__PURE__ */ new Map();
116
+ let totalRoutes = 0;
117
+ for (const app of taujsConfig.apps) {
118
+ const appRoutes = (app.routes ?? []).map((route) => {
119
+ const fullRoute = { ...route, appId: app.appId };
120
+ if (!pathTracker.has(route.path)) pathTracker.set(route.path, []);
121
+ pathTracker.get(route.path).push(app.appId);
122
+ return fullRoute;
123
+ });
124
+ console.log(import_picocolors.default.gray(` \u2022 ${app.appId}: ${appRoutes.length} route(s)`));
125
+ allRoutes.push(...appRoutes);
126
+ totalRoutes += appRoutes.length;
127
+ }
128
+ for (const [path, appIds] of pathTracker.entries()) {
129
+ if (appIds.length > 1) console.warn(import_picocolors.default.yellow(`\u26A0\uFE0F Route path "${path}" is declared in multiple apps: ${appIds.join(", ")} \u2013 order may affect matching`));
130
+ }
131
+ const sortedRoutes = allRoutes.sort((a, b) => computeScore(b.path) - computeScore(a.path));
132
+ const t1 = performance.now();
133
+ console.log(import_picocolors.default.green(`Prepared ${totalRoutes} route(s) in ${(t1 - t0).toFixed(1)}ms`));
134
+ return sortedRoutes;
135
+ } catch (err) {
136
+ console.log(import_picocolors.default.red("Failed to prepare routes"));
137
+ throw err;
138
+ }
139
+ };
140
+ var computeScore = (path) => {
141
+ return path.split("/").filter(Boolean).reduce((score, segment) => score + (segment.startsWith(":") ? 1 : 10), 0);
142
+ };
143
+ export {
144
+ extractBuildConfigs,
145
+ extractRoutes
146
+ };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,24 @@
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';
1
+ import { FastifyReply } from 'fastify';
2
+ export { B as BaseMiddleware, C as Config, F as FetchConfig, I as InitialRouteParams, i as Manifest, M as ManifestEntry, N as NamedService, P as ProcessedConfig, g as RenderCallbacks, l as RenderModule, j as RenderSSR, k as RenderStream, R as Route, b as RouteAttributes, a as RouteParams, m as RoutePathsAndAttributes, h as SSRManifest, S as SSRServer, d as SSRServerOptions, e as ServiceMethod, f as ServiceRegistry, T as TEMPLATE, c as createMaps, p as processConfigs } from './SSRServer-C7MMCfVq.js';
2
3
  import 'node:http';
3
- import 'fastify';
4
4
  import 'vite';
5
+
6
+ declare module 'fastify' {
7
+ interface FastifyRequest {
8
+ nonce?: string;
9
+ }
10
+ interface FastifyInstance {
11
+ /**
12
+ * Optional authentication hook to be used by the TauJS SSRServer.
13
+ * This method must be decorated by the user when using auth middleware in `taujs.config.ts`.
14
+ *
15
+ * Example usage:
16
+ * ```ts
17
+ * fastify.decorate('authenticate', async function (req, reply) {
18
+ * await req.jwtVerify();
19
+ * });
20
+ * ```
21
+ */
22
+ authenticate: (req: FastifyRequest, reply: FastifyReply) => Promise<void>;
23
+ }
24
+ }
package/dist/index.js CHANGED
@@ -187,12 +187,35 @@ var require_picocolors = __commonJS({
187
187
  }
188
188
  });
189
189
 
190
+ // src/types.d.ts
191
+ import "fastify";
192
+
190
193
  // src/SSRServer.ts
191
194
  var import_fastify_plugin = __toESM(require_plugin(), 1);
192
195
  var import_picocolors = __toESM(require_picocolors(), 1);
193
196
  import { readFile } from "fs/promises";
194
197
  import path2 from "path";
195
198
 
199
+ // src/utils/Logger.ts
200
+ var createLogger = (debug) => ({
201
+ log: (...args) => {
202
+ if (debug) console.log(...args);
203
+ },
204
+ warn: (...args) => {
205
+ if (debug) console.warn(...args);
206
+ },
207
+ error: (...args) => {
208
+ if (debug) console.error(...args);
209
+ }
210
+ });
211
+ var debugLog = (logger, message, req) => {
212
+ const prefix = "[\u03C4js]";
213
+ const method = req?.method ?? "";
214
+ const url = req?.url ?? "";
215
+ const tag = method && url ? `${method} ${url}` : "";
216
+ logger.log(`${prefix} ${tag} ${message}`);
217
+ };
218
+
196
219
  // src/constants.ts
197
220
  var RENDERTYPE = {
198
221
  ssr: "ssr",
@@ -214,6 +237,32 @@ var DEV_CSP_DIRECTIVES = {
214
237
  "img-src": ["'self'", "data:"]
215
238
  };
216
239
 
240
+ // src/security/auth.ts
241
+ function createAuthHook(routes, isDebug) {
242
+ const logger = createLogger(Boolean(isDebug));
243
+ return async function authHook(req, reply) {
244
+ const url = new URL(req.url, `http://${req.headers.host}`).pathname;
245
+ const matched = routes.find((r) => r.path === url);
246
+ const authConfig = matched?.attr?.middleware?.auth;
247
+ if (!authConfig?.required) {
248
+ debugLog(logger, "Auth not required for route", req);
249
+ return;
250
+ }
251
+ if (typeof req.server.authenticate !== "function") {
252
+ req.log.warn('Route requires auth but no "authenticate" decorator is defined on Fastify.');
253
+ return reply.status(500).send("Server misconfiguration: auth decorator missing.");
254
+ }
255
+ try {
256
+ debugLog(logger, "Invoking authenticate(...)", req);
257
+ await req.server.authenticate(req, reply);
258
+ debugLog(logger, "Authentication successful", req);
259
+ } catch (err) {
260
+ debugLog(logger, "Authentication failed", req);
261
+ return reply.send(err);
262
+ }
263
+ };
264
+ }
265
+
217
266
  // src/security/csp.ts
218
267
  import crypto from "crypto";
219
268
  var defaultGenerateCSP = (directives, nonce) => {
@@ -232,7 +281,7 @@ var defaultGenerateCSP = (directives, nonce) => {
232
281
  return Object.entries(merged).map(([key, values]) => `${key} ${values.join(" ")}`).join("; ");
233
282
  };
234
283
  var generateNonce = () => crypto.randomBytes(16).toString("base64");
235
- var cspHook = (options = {}) => (req, reply, done) => {
284
+ var createCSPHook = (options = {}) => (req, reply, done) => {
236
285
  const nonce = generateNonce();
237
286
  const directives = options.directives ?? DEV_CSP_DIRECTIVES;
238
287
  const generate = options.generateCSP ?? defaultGenerateCSP;
@@ -255,6 +304,26 @@ var applyCSP = (security, reply) => {
255
304
  return nonce;
256
305
  };
257
306
 
307
+ // src/security/verifyMiddleware.ts
308
+ var isAuthRequired = (r) => r.attr?.middleware?.auth?.required === true;
309
+ var hasAuthenticate = (app) => typeof app.authenticate === "function";
310
+ var verifyContracts = (app, routes, contracts, isDebug) => {
311
+ const logger = createLogger(Boolean(isDebug));
312
+ for (const contract of contracts) {
313
+ const isUsed = routes.some(contract.required);
314
+ if (!isUsed) {
315
+ debugLog(logger, `Middleware "${contract.key}" not used in any routes`);
316
+ continue;
317
+ }
318
+ if (!contract.verify(app)) {
319
+ const error = new Error(`[\u03C4js] ${contract.errorMessage}`);
320
+ logger.error(error.message);
321
+ throw error;
322
+ }
323
+ debugLog(logger, `Middleware "${contract.key}" verified \u2713`);
324
+ }
325
+ };
326
+
258
327
  // src/utils/Utils.ts
259
328
  import { dirname, join } from "path";
260
329
  import "path";
@@ -427,6 +496,7 @@ var processConfigs = (configs, baseClientRoot, templateDefaults) => {
427
496
  };
428
497
  var SSRServer = (0, import_fastify_plugin.default)(
429
498
  async (app, opts) => {
499
+ const logger = createLogger(opts.isDebug ?? false);
430
500
  const { alias, configs, routes, serviceRegistry, isDebug, clientRoot: baseClientRoot } = opts;
431
501
  const { bootstrapModules, cssLinks, manifests, preloadLinks, renderModules, ssrManifests, templates } = createMaps();
432
502
  const processedConfigs = processConfigs(configs, baseClientRoot, TEMPLATE);
@@ -460,6 +530,19 @@ var SSRServer = (0, import_fastify_plugin.default)(
460
530
  }
461
531
  }
462
532
  let viteDevServer;
533
+ verifyContracts(
534
+ app,
535
+ routes,
536
+ [
537
+ {
538
+ key: "auth",
539
+ required: isAuthRequired,
540
+ verify: hasAuthenticate,
541
+ errorMessage: "Routes require auth but Fastify instance is missing `.authenticate` decorator."
542
+ }
543
+ ],
544
+ opts.isDebug
545
+ );
463
546
  await app.register(import("@fastify/static"), {
464
547
  index: false,
465
548
  prefix: "/",
@@ -468,11 +551,12 @@ var SSRServer = (0, import_fastify_plugin.default)(
468
551
  });
469
552
  app.addHook(
470
553
  "onRequest",
471
- cspHook({
554
+ createCSPHook({
472
555
  directives: opts.security?.csp?.directives,
473
556
  generateCSP: opts.security?.csp?.generateCSP
474
557
  })
475
558
  );
559
+ app.addHook("onRequest", createAuthHook(routes));
476
560
  if (isDevelopment) {
477
561
  const { createServer } = await import("vite");
478
562
  viteDevServer = await createServer({
@@ -490,10 +574,10 @@ var SSRServer = (0, import_fastify_plugin.default)(
490
574
  {
491
575
  name: "taujs-development-server-debug-logging",
492
576
  configureServer(server) {
493
- console.log(import_picocolors.default.green("\u03C4js development server debug started."));
577
+ logger.log(import_picocolors.default.green("\u03C4js development server debug started."));
494
578
  server.middlewares.use((req, res, next) => {
495
- console.log(import_picocolors.default.cyan(`\u2190 rx: ${req.url}`));
496
- res.on("finish", () => console.log(import_picocolors.default.yellow(`\u2192 tx: ${req.url}`)));
579
+ logger.log(import_picocolors.default.cyan(`\u2190 rx: ${req.url}`));
580
+ res.on("finish", () => logger.log(import_picocolors.default.yellow(`\u2192 tx: ${req.url}`)));
497
581
  next();
498
582
  });
499
583
  }
@@ -577,7 +661,9 @@ var SSRServer = (0, import_fastify_plugin.default)(
577
661
  let aggregateHeadContent = headContent;
578
662
  if (ssrManifest && preloadLink) aggregateHeadContent += preloadLink;
579
663
  if (manifest && cssLink) aggregateHeadContent += cssLink;
580
- const fullHtml = template.replace(SSRTAG.ssrHead, aggregateHeadContent).replace(SSRTAG.ssrHtml, `${appHtml}${initialDataScript}<script nonce="${nonce}" type="module" src="${bootstrapModule}" defer></script>`);
664
+ const shouldHydrate = attr?.hydrate !== false;
665
+ const bootstrapScriptTag = shouldHydrate ? `<script nonce="${nonce}" type="module" src="${bootstrapModule}" defer></script>` : "";
666
+ const fullHtml = template.replace(SSRTAG.ssrHead, aggregateHeadContent).replace(SSRTAG.ssrHtml, `${appHtml}${initialDataScript}${bootstrapScriptTag}`);
581
667
  return reply.status(200).header("Content-Type", "text/html").send(fullHtml);
582
668
  } else {
583
669
  const { renderStream } = renderModule;
@@ -1,130 +1,4 @@
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
- }>;
102
- type RenderStream = (serverResponse: ServerResponse, callbacks: RenderCallbacks, initialDataPromise: Promise<Record<string, unknown>>, location: string, bootstrapModules?: string, meta?: Record<string, unknown>) => void;
103
- type RenderModule = {
104
- renderSSR: RenderSSR;
105
- renderStream: RenderStream;
106
- };
107
- type RouteAttributes<Params = {}> = {
108
- fetch?: (params?: Params, options?: RequestInit & {
109
- params?: Record<string, unknown>;
110
- }) => Promise<FetchConfig>;
111
- } & ({
112
- render?: typeof RENDERTYPE.ssr;
113
- meta?: Record<string, unknown>;
114
- } | {
115
- render: typeof RENDERTYPE.streaming;
116
- meta: Record<string, unknown>;
117
- });
118
- type Route<Params = {}> = {
119
- attr?: RouteAttributes<Params>;
120
- path: string;
121
- appId?: string;
122
- };
123
- interface InitialRouteParams extends Record<string, unknown> {
124
- serviceName?: string;
125
- serviceMethod?: string;
126
- }
127
- type RouteParams = InitialRouteParams & Record<string, unknown>;
128
- type RoutePathsAndAttributes<Params = {}> = Omit<Route<Params>, 'element'>;
129
-
130
- 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 };
1
+ import 'fastify';
2
+ export { n as CSPDirectives, o as CSPOptions, u as applyCSP, s as createCSPHook, q as defaultGenerateCSP, r as generateNonce, t as getRequestNonce } from '../SSRServer-C7MMCfVq.js';
3
+ import 'node:http';
4
+ import 'vite';
@@ -26,7 +26,7 @@ var defaultGenerateCSP = (directives, nonce) => {
26
26
  return Object.entries(merged).map(([key, values]) => `${key} ${values.join(" ")}`).join("; ");
27
27
  };
28
28
  var generateNonce = () => crypto.randomBytes(16).toString("base64");
29
- var cspHook = (options = {}) => (req, reply, done) => {
29
+ var createCSPHook = (options = {}) => (req, reply, done) => {
30
30
  const nonce = generateNonce();
31
31
  const directives = options.directives ?? DEV_CSP_DIRECTIVES;
32
32
  const generate = options.generateCSP ?? defaultGenerateCSP;
@@ -51,7 +51,7 @@ var applyCSP = (security, reply) => {
51
51
  };
52
52
  export {
53
53
  applyCSP,
54
- cspHook,
54
+ createCSPHook,
55
55
  defaultGenerateCSP,
56
56
  generateNonce,
57
57
  getRequestNonce
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@taujs/server",
3
- "version": "0.2.7",
4
- "description": "taujs | τjs",
5
- "author": "Aoede <taujs@aoede.uk.net> (https://www.aoede.uk.net)",
3
+ "version": "0.3.1",
4
+ "description": "taujs [ τjs ]",
5
+ "author": "John Smith | Aoede <taujs@aoede.uk.net> (https://www.aoede.uk.net)",
6
6
  "license": "MIT",
7
7
  "homepage": "https://github.com/aoede3/taujs-server",
8
8
  "repository": {
@@ -33,6 +33,10 @@
33
33
  "import": "./dist/build.js",
34
34
  "types": "./dist/build.d.ts"
35
35
  },
36
+ "./config": {
37
+ "import": "./dist/config.js",
38
+ "types": "./dist/config.d.ts"
39
+ },
36
40
  "./csp": {
37
41
  "import": "./dist/security/csp.js",
38
42
  "types": "./dist/security/csp.d.ts"