@catmint/adapter-node 0.0.0-prealpha.2 → 0.0.0-prealpha.20

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/index.d.ts CHANGED
@@ -7,6 +7,13 @@ export interface NodeAdapterOptions {
7
7
  port?: number;
8
8
  /** Host to bind to (default: '0.0.0.0') */
9
9
  host?: string;
10
+ /**
11
+ * Maximum allowed request body size in bytes.
12
+ * Applies to server function RPC calls and API endpoints.
13
+ *
14
+ * @default 1_048_576 (1 MB)
15
+ */
16
+ maxBodySize?: number;
10
17
  }
11
18
  /**
12
19
  * Create a Node.js adapter for Catmint.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAA+B,MAAM,gBAAgB,CAAC;AAIlF;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,wCAAwC;IACxC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,OAAO,UAAU,WAAW,CACjC,OAAO,CAAC,EAAE,kBAAkB,GAC3B,cAAc,CA0BhB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,cAAc,EAIf,MAAM,gBAAgB,CAAC;AAIxB;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,wCAAwC;IACxC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,OAAO,UAAU,WAAW,CACjC,OAAO,CAAC,EAAE,kBAAkB,GAC3B,cAAc,CA+ChB"}
package/dist/index.js CHANGED
@@ -19,12 +19,31 @@ import { generateServerEntry } from "./server-template.js";
19
19
  export default function nodeAdapter(options) {
20
20
  const port = options?.port ?? 6468;
21
21
  const host = options?.host ?? "0.0.0.0";
22
+ const maxBodySize = options?.maxBodySize ?? 1_048_576;
22
23
  return {
23
24
  name: "@catmint/adapter-node",
25
+ /**
26
+ * Dev-time hook: provides the raw Node.js `req` and `res` objects as
27
+ * the platform context. This allows `getPlatform()` to return
28
+ * `{ req, res }` during development, matching the shape that
29
+ * Node.js-based production servers would provide.
30
+ */
31
+ dev() {
32
+ return {
33
+ getPlatform(_request, nodeReq, nodeRes) {
34
+ return { req: nodeReq, res: nodeRes };
35
+ },
36
+ };
37
+ },
24
38
  async adapt(context) {
25
39
  context.log("@catmint/adapter-node: Generating standalone Node.js server...");
26
40
  const manifest = context.manifest;
27
- const entryContent = generateServerEntry(manifest, { port, host });
41
+ const entryContent = generateServerEntry(manifest, {
42
+ port,
43
+ host,
44
+ maxBodySize,
45
+ headerPreset: manifest.config.security.headerPreset,
46
+ });
28
47
  // Write to dist/server/index.js (separate from the SSR bundle at dist/ssr/)
29
48
  // so `catmint start` can still load the SSR entry for its built-in server.
30
49
  await context.writeFile("server/index.js", entryContent);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,4DAA4D;AAI5D,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAY3D;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,OAAO,UAAU,WAAW,CACjC,OAA4B;IAE5B,MAAM,IAAI,GAAG,OAAO,EAAE,IAAI,IAAI,IAAI,CAAC;IACnC,MAAM,IAAI,GAAG,OAAO,EAAE,IAAI,IAAI,SAAS,CAAC;IAExC,OAAO;QACL,IAAI,EAAE,uBAAuB;QAC7B,KAAK,CAAC,KAAK,CAAC,OAAuB;YACjC,OAAO,CAAC,GAAG,CACT,gEAAgE,CACjE,CAAC;YAEF,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAuB,CAAC;YACjD,MAAM,YAAY,GAAG,mBAAmB,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;YAEnE,4EAA4E;YAC5E,2EAA2E;YAC3E,MAAM,OAAO,CAAC,SAAS,CAAC,iBAAiB,EAAE,YAAY,CAAC,CAAC;YAEzD,OAAO,CAAC,GAAG,CACT,oEAAoE,CACrE,CAAC;YACF,OAAO,CAAC,GAAG,CACT,iFAAiF,IAAI,IAAI,IAAI,GAAG,CACjG,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,4DAA4D;AAS5D,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAmB3D;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,OAAO,UAAU,WAAW,CACjC,OAA4B;IAE5B,MAAM,IAAI,GAAG,OAAO,EAAE,IAAI,IAAI,IAAI,CAAC;IACnC,MAAM,IAAI,GAAG,OAAO,EAAE,IAAI,IAAI,SAAS,CAAC;IACxC,MAAM,WAAW,GAAG,OAAO,EAAE,WAAW,IAAI,SAAS,CAAC;IAEtD,OAAO;QACL,IAAI,EAAE,uBAAuB;QAE7B;;;;;WAKG;QACH,GAAG;YACD,OAAO;gBACL,WAAW,CAAC,QAAiB,EAAE,OAAiB,EAAE,OAAiB;oBACjE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC;gBACxC,CAAC;aACF,CAAC;QACJ,CAAC;QAED,KAAK,CAAC,KAAK,CAAC,OAAuB;YACjC,OAAO,CAAC,GAAG,CACT,gEAAgE,CACjE,CAAC;YAEF,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAuB,CAAC;YACjD,MAAM,YAAY,GAAG,mBAAmB,CAAC,QAAQ,EAAE;gBACjD,IAAI;gBACJ,IAAI;gBACJ,WAAW;gBACX,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY;aACpD,CAAC,CAAC;YAEH,4EAA4E;YAC5E,2EAA2E;YAC3E,MAAM,OAAO,CAAC,SAAS,CAAC,iBAAiB,EAAE,YAAY,CAAC,CAAC;YAEzD,OAAO,CAAC,GAAG,CACT,oEAAoE,CACrE,CAAC;YACF,OAAO,CAAC,GAAG,CACT,iFAAiF,IAAI,IAAI,IAAI,GAAG,CACjG,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -2,6 +2,8 @@ import type { AppManifest } from "catmint/config";
2
2
  interface ServerOptions {
3
3
  port: number;
4
4
  host: string;
5
+ maxBodySize: number;
6
+ headerPreset: "baseline" | "none";
5
7
  }
6
8
  /**
7
9
  * Generate the content of the standalone Node.js server entry point.
@@ -1 +1 @@
1
- {"version":3,"file":"server-template.d.ts","sourceRoot":"","sources":["../src/server-template.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD,UAAU,aAAa;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,WAAW,EACtB,OAAO,EAAE,aAAa,GACrB,MAAM,CAoWR"}
1
+ {"version":3,"file":"server-template.d.ts","sourceRoot":"","sources":["../src/server-template.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD,UAAU,aAAa;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,UAAU,GAAG,MAAM,CAAC;CACnC;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,WAAW,EACtB,OAAO,EAAE,aAAa,GACrB,MAAM,CAunBR"}
@@ -12,6 +12,7 @@
12
12
  export function generateServerEntry(_manifest, options) {
13
13
  return `// Auto-generated by @catmint/adapter-node — do not edit
14
14
  import { createServer } from "node:http";
15
+ import { createHash } from "node:crypto";
15
16
  import { readFile, stat } from "node:fs/promises";
16
17
  import { join, extname, resolve } from "node:path";
17
18
  import { fileURLToPath } from "node:url";
@@ -19,6 +20,7 @@ import { fileURLToPath } from "node:url";
19
20
  const __dirname = fileURLToPath(new URL(".", import.meta.url));
20
21
  const CLIENT_DIR = resolve(__dirname, "../client");
21
22
  const STATIC_DIR = resolve(__dirname, "../static");
23
+ const PRERENDERED_DIR = resolve(__dirname, "../prerendered");
22
24
 
23
25
  const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : ${options.port};
24
26
  const HOST = process.env.HOST ?? ${JSON.stringify(options.host)};
@@ -161,14 +163,58 @@ function __catmintErrorPage(statusCode, detail) {
161
163
  }
162
164
 
163
165
  function collectBody(req) {
166
+ const MAX_BODY_SIZE = ${options.maxBodySize};
164
167
  return new Promise((resolve, reject) => {
165
168
  const chunks = [];
166
- req.on("data", (chunk) => chunks.push(chunk));
169
+ let totalLength = 0;
170
+ req.on("data", (chunk) => {
171
+ totalLength += chunk.length;
172
+ if (totalLength > MAX_BODY_SIZE) {
173
+ req.destroy();
174
+ reject(new Error("Request body too large"));
175
+ return;
176
+ }
177
+ chunks.push(chunk);
178
+ });
167
179
  req.on("end", () => resolve(Buffer.concat(chunks)));
168
180
  req.on("error", reject);
169
181
  });
170
182
  }
171
183
 
184
+ // ---------------------------------------------------------------------------
185
+ // Server function error detection helpers (duck-typing)
186
+ //
187
+ // The adapter cannot import from \`catmint\` at runtime. Instead we detect
188
+ // error types by checking for characteristic properties.
189
+ // ---------------------------------------------------------------------------
190
+
191
+ function __isClientSafeErrorLike(value) {
192
+ if (!value || typeof value !== "object") return false;
193
+ if (value instanceof Error && typeof value.statusCode === "number") {
194
+ var proto = Object.getPrototypeOf(value);
195
+ while (proto && proto !== Error.prototype) {
196
+ if (proto.constructor && proto.constructor.name === "ClientSafeError") return true;
197
+ proto = Object.getPrototypeOf(proto);
198
+ }
199
+ }
200
+ return false;
201
+ }
202
+
203
+ function __isRedirectErrorLike(value) {
204
+ if (!value || typeof value !== "object") return false;
205
+ return (
206
+ value instanceof Error &&
207
+ value.name === "RedirectError" &&
208
+ typeof value.url === "string" &&
209
+ typeof value.status === "number"
210
+ );
211
+ }
212
+
213
+ function __errorRef(err) {
214
+ var content = String(Date.now()) + ":" + (err instanceof Error ? err.message : String(err));
215
+ return createHash("sha256").update(content).digest("hex").slice(0, 8);
216
+ }
217
+
172
218
  // Import the RSC and SSR entries for the rendering pipeline
173
219
  const rscEntry = await import("../rsc/index.js");
174
220
  const ssrEntry = await import("../ssr/index.js");
@@ -189,14 +235,76 @@ async function pipeWebStreamToResponse(webStream, res) {
189
235
  }
190
236
  }
191
237
 
238
+ function nodeReqToWebRequest(req) {
239
+ const protocol = "http";
240
+ const host = req.headers.host || \`\${HOST}:\${PORT}\`;
241
+ const reqUrl = new URL(req.url || "/", \`\${protocol}://\${host}\`);
242
+ const headers = new Headers();
243
+ for (const [key, value] of Object.entries(req.headers)) {
244
+ if (value) {
245
+ if (Array.isArray(value)) {
246
+ for (const v of value) {
247
+ headers.append(key, v);
248
+ }
249
+ } else {
250
+ headers.set(key, value);
251
+ }
252
+ }
253
+ }
254
+ return new Request(reqUrl.href, {
255
+ method: req.method || "GET",
256
+ headers,
257
+ });
258
+ }
259
+
192
260
  const server = createServer(async (req, res) => {
261
+ ${options.headerPreset === "baseline"
262
+ ? ` // Baseline security headers
263
+ res.setHeader("X-Content-Type-Options", "nosniff");
264
+ res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
265
+ res.setHeader("X-Frame-Options", "SAMEORIGIN");
266
+ `
267
+ : ""}
193
268
  const url = new URL(req.url || "/", \`http://\${HOST}:\${PORT}\`);
194
269
  const pathname = url.pathname;
195
270
  const method = (req.method || "GET").toUpperCase();
196
271
 
272
+ // Establish the request-scoped AsyncLocalStorage context.
273
+ // This makes cookies(), headers(), getPlatform() etc. available
274
+ // to server functions, middleware, API endpoints, and RSC rendering.
275
+ const __webRequest = nodeReqToWebRequest(req);
276
+ const __store = ssrEntry.createRequestStore(__webRequest, { req, res });
277
+ await ssrEntry.runWithRequestContext(__store, async () => {
278
+
279
+ // Helper: flush pending Set-Cookie headers and accumulated response
280
+ // headers from the request store onto the Node.js response object.
281
+ function __flushPendingHeaders() {
282
+ // Apply accumulated response headers
283
+ __store.responseHeaders.forEach(function(value, key) {
284
+ res.setHeader(key, value);
285
+ });
286
+ // Apply pending Set-Cookie headers
287
+ var cookies = [];
288
+ __store.pendingCookies.forEach(function(pending) {
289
+ cookies.push(pending.serialized);
290
+ });
291
+ if (cookies.length > 0) {
292
+ res.setHeader("Set-Cookie", cookies);
293
+ }
294
+ }
295
+
197
296
  try {
198
- // 1. Serve static assets from dist/static/ (no cache)
297
+ // 1. Serve client assets from dist/client/ (immutable for hashed assets)
298
+ // These have hashed filenames and never conflict with app routes.
199
299
  {
300
+ const filePath = join(CLIENT_DIR, pathname);
301
+ const served = await serveStaticFile(res, filePath, pathname.includes("/assets/"));
302
+ if (served) return;
303
+ }
304
+
305
+ // 2. Pre-rendered static pages WITHOUT middleware: serve directly from
306
+ // dist/static/ as a fast path — no middleware execution needed.
307
+ if (method === "GET") {
200
308
  let staticPath = pathname;
201
309
  if (!extname(staticPath)) {
202
310
  staticPath = staticPath.endsWith("/")
@@ -208,41 +316,146 @@ const server = createServer(async (req, res) => {
208
316
  if (served) return;
209
317
  }
210
318
 
211
- // 2. Serve client assets from dist/client/ (immutable for hashed assets)
212
- {
213
- const filePath = join(CLIENT_DIR, pathname);
214
- const served = await serveStaticFile(res, filePath, pathname.includes("/assets/"));
215
- if (served) return;
319
+ // 3. Middleware execution runs for all remaining routes (server functions,
320
+ // endpoints, prerendered pages with middleware, RSC-rendered pages).
321
+ // For RSC flight requests, run middleware against the TARGET page path
322
+ // (from ?path= query param), not the literal /__catmint/rsc pathname.
323
+ var middlewareHeaders = null;
324
+ if (ssrEntry.executeMiddleware) {
325
+ try {
326
+ var middlewarePath = pathname;
327
+ if (pathname === "/__catmint/rsc") {
328
+ var rscTargetPath = url.searchParams.get("path");
329
+ if (rscTargetPath) middlewarePath = rscTargetPath;
330
+ }
331
+ var webRequest = nodeReqToWebRequest(req);
332
+ var mwResult = await ssrEntry.executeMiddleware(middlewarePath, webRequest);
333
+
334
+ if (mwResult.shortCircuit) {
335
+ // Middleware short-circuited — return its response directly
336
+ res.writeHead(mwResult.shortCircuit.status,
337
+ Object.fromEntries(mwResult.shortCircuit.headers));
338
+ __flushPendingHeaders();
339
+ var scBody = await mwResult.shortCircuit.text();
340
+ res.end(scBody);
341
+ return;
342
+ }
343
+
344
+ // Middleware passed — capture headers to merge into final response
345
+ if (mwResult.headers && typeof mwResult.headers.forEach === "function") {
346
+ middlewareHeaders = mwResult.headers;
347
+ }
348
+ } catch (err) {
349
+ console.error("Middleware error:", err);
350
+ if (!res.headersSent) {
351
+ __flushPendingHeaders();
352
+ res.writeHead(500);
353
+ res.end("Internal Server Error");
354
+ }
355
+ return;
356
+ }
357
+ }
358
+
359
+ // Helper: merge middleware headers into the response before sending.
360
+ function applyMiddlewareHeaders() {
361
+ if (middlewareHeaders) {
362
+ middlewareHeaders.forEach(function(value, key) {
363
+ res.setHeader(key, value);
364
+ });
365
+ }
216
366
  }
217
367
 
218
- // 3. Handle server function RPC calls (/__catmint/fn/*)
368
+ // 4. Handle server function RPC calls (/__catmint/fn/*)
219
369
  if (pathname.startsWith("/__catmint/fn/") && ssrEntry.handleServerFn) {
220
370
  try {
371
+ // Enforce POST method for server function calls
372
+ if (method !== "POST") {
373
+ __flushPendingHeaders();
374
+ sendJson(res, 405, { error: "Method " + method + " not allowed, expected POST" });
375
+ return;
376
+ }
377
+
378
+ // Enforce Content-Type: application/json
379
+ const ct = (req.headers["content-type"] || "").toLowerCase();
380
+ if (ct && !ct.startsWith("application/json")) {
381
+ __flushPendingHeaders();
382
+ sendJson(res, 415, { error: "Unsupported Content-Type, expected application/json" });
383
+ return;
384
+ }
385
+
386
+ // Validate Origin header to mitigate cross-site invocation
387
+ const origin = req.headers["origin"];
388
+ if (origin) {
389
+ const expected = \`http://\${HOST}:\${PORT}\`;
390
+ if (origin !== expected && origin !== \`https://\${HOST}:\${PORT}\`) {
391
+ __flushPendingHeaders();
392
+ sendJson(res, 403, { error: "Origin not allowed" });
393
+ return;
394
+ }
395
+ }
396
+
221
397
  const body = await collectBody(req);
222
398
  let parsed;
223
399
  try {
224
400
  parsed = body.length > 0 ? JSON.parse(body.toString("utf-8")) : undefined;
225
401
  } catch {
402
+ __flushPendingHeaders();
226
403
  sendJson(res, 400, { error: "Invalid JSON body" });
227
404
  return;
228
405
  }
229
406
  const result = await ssrEntry.handleServerFn(pathname, parsed);
230
407
  if (result) {
408
+ // Check if the result IS a ClientSafeError (returned, not thrown)
409
+ if (__isClientSafeErrorLike(result.result)) {
410
+ applyMiddlewareHeaders();
411
+ __flushPendingHeaders();
412
+ sendJson(res, 200, {
413
+ __clientSafeError: true,
414
+ error: result.result.message,
415
+ statusCode: result.result.statusCode,
416
+ data: result.result.data,
417
+ });
418
+ return;
419
+ }
420
+ applyMiddlewareHeaders();
421
+ __flushPendingHeaders();
231
422
  sendJson(res, 200, result.result);
232
423
  return;
233
424
  }
425
+ __flushPendingHeaders();
234
426
  sendJson(res, 404, { error: "Server function not found" });
235
427
  } catch (err) {
236
- const message = err instanceof Error ? err.message : String(err);
237
- sendJson(res, 500, { error: message });
428
+ // Always log full error server-side
429
+ console.error("[catmint] Server function error:", err);
430
+
431
+ // RedirectError — send redirect envelope
432
+ if (__isRedirectErrorLike(err)) {
433
+ __flushPendingHeaders();
434
+ sendJson(res, 200, { __redirect: true, url: err.url, status: err.status });
435
+ return;
436
+ }
437
+
438
+ // ClientSafeError — developer opted in, expose to client
439
+ if (__isClientSafeErrorLike(err)) {
440
+ __flushPendingHeaders();
441
+ sendJson(res, err.statusCode, { error: err.message, data: err.data });
442
+ return;
443
+ }
444
+
445
+ // Default: sanitize — generic message with correlation hash
446
+ const ref = __errorRef(err);
447
+ console.error("[catmint] Error reference [ref: " + ref + "] — see above for full error");
448
+ __flushPendingHeaders();
449
+ sendJson(res, 500, { error: "Internal Server Error [ref: " + ref + "]" });
238
450
  }
239
451
  return;
240
452
  }
241
453
 
242
- // 4. RSC flight stream for client-side navigation (/__catmint/rsc?path=...)
454
+ // 5. RSC flight stream for client-side navigation (/__catmint/rsc?path=...)
243
455
  if (pathname === "/__catmint/rsc" && method === "GET" && rscEntry.render) {
244
456
  const targetPath = url.searchParams.get("path");
245
457
  if (!targetPath) {
458
+ __flushPendingHeaders();
246
459
  res.setHeader("Content-Type", "application/json");
247
460
  res.writeHead(400);
248
461
  res.end(JSON.stringify({ error: "Missing ?path= parameter" }));
@@ -251,6 +464,8 @@ const server = createServer(async (req, res) => {
251
464
  try {
252
465
  const rscResult = await rscEntry.render(targetPath);
253
466
  if (rscResult) {
467
+ applyMiddlewareHeaders();
468
+ __flushPendingHeaders();
254
469
  res.writeHead(200, {
255
470
  "Content-Type": "text/x-component; charset=utf-8",
256
471
  "Cache-Control": "no-cache, no-store, must-revalidate",
@@ -258,12 +473,14 @@ const server = createServer(async (req, res) => {
258
473
  await pipeWebStreamToResponse(rscResult.stream, res);
259
474
  return;
260
475
  }
476
+ __flushPendingHeaders();
261
477
  res.setHeader("Content-Type", "application/json");
262
478
  res.writeHead(404);
263
479
  res.end(JSON.stringify({ error: "No matching route" }));
264
480
  } catch (err) {
265
481
  console.error("RSC navigation error:", err);
266
482
  if (!res.headersSent) {
483
+ __flushPendingHeaders();
267
484
  res.setHeader("Content-Type", "application/json");
268
485
  res.writeHead(500);
269
486
  res.end(JSON.stringify({ error: "RSC navigation error" }));
@@ -272,7 +489,7 @@ const server = createServer(async (req, res) => {
272
489
  return;
273
490
  }
274
491
 
275
- // 5. API endpoint handling
492
+ // 6. API endpoint handling
276
493
  if (ssrEntry.hasEndpoint && ssrEntry.hasEndpoint(pathname)) {
277
494
  try {
278
495
  const protocol = "http";
@@ -295,7 +512,11 @@ const server = createServer(async (req, res) => {
295
512
  const result = await ssrEntry.handleEndpoint(pathname, method, webRequest);
296
513
 
297
514
  if (result) {
298
- res.writeHead(result.response.status, Object.fromEntries(result.response.headers));
515
+ // Copy response headers then merge middleware headers
516
+ var epHeaders = Object.fromEntries(result.response.headers);
517
+ res.writeHead(result.response.status, epHeaders);
518
+ applyMiddlewareHeaders();
519
+ __flushPendingHeaders();
299
520
  if (result.response.body) {
300
521
  await pipeWebStreamToResponse(result.response.body, res);
301
522
  } else {
@@ -305,24 +526,64 @@ const server = createServer(async (req, res) => {
305
526
  }
306
527
 
307
528
  // Endpoint exists but method not handled
529
+ __flushPendingHeaders();
308
530
  res.writeHead(405, { "Content-Type": "text/plain" });
309
531
  res.end(\`Method \${method} not allowed\`);
310
532
  return;
311
533
  } catch (err) {
534
+ // redirect() throws a RedirectError — convert to a real HTTP redirect
535
+ if (__isRedirectErrorLike(err)) {
536
+ __flushPendingHeaders();
537
+ res.writeHead(err.status, { "Location": err.url });
538
+ res.end();
539
+ return;
540
+ }
312
541
  console.error("Endpoint error:", err);
313
542
  if (!res.headersSent) {
543
+ __flushPendingHeaders();
314
544
  sendText(res, 500, "Internal Server Error");
315
545
  }
316
546
  return;
317
547
  }
318
548
  }
319
549
 
320
- // 6. RSC SSR page rendering pipeline
550
+ // 7. Pre-rendered pages WITH middleware: serve from dist/prerendered/
551
+ // These are static routes that have middleware ancestors — their HTML
552
+ // was pre-rendered but they must go through the middleware pipeline.
553
+ if (method === "GET") {
554
+ let prerenderedPath = pathname;
555
+ if (!extname(prerenderedPath)) {
556
+ prerenderedPath = prerenderedPath.endsWith("/")
557
+ ? prerenderedPath + "index.html"
558
+ : prerenderedPath + ".html";
559
+ }
560
+ try {
561
+ const prFilePath = join(PRERENDERED_DIR, prerenderedPath);
562
+ const prStats = await stat(prFilePath);
563
+ if (prStats.isFile()) {
564
+ const content = await readFile(prFilePath);
565
+ applyMiddlewareHeaders();
566
+ __flushPendingHeaders();
567
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
568
+ res.setHeader("Content-Length", content.byteLength);
569
+ res.setHeader("Cache-Control", "public, max-age=0, must-revalidate");
570
+ res.writeHead(200);
571
+ res.end(content);
572
+ return;
573
+ }
574
+ } catch {
575
+ // No prerendered file — continue to RSC rendering
576
+ }
577
+ }
578
+
579
+ // 8. RSC → SSR page rendering pipeline
321
580
  if (method === "GET" && rscEntry.render) {
322
581
  try {
323
582
  const rscResult = await rscEntry.render(pathname);
324
583
  if (rscResult) {
325
584
  const htmlStream = await ssrEntry.renderToHtml(rscResult.stream, rscResult.headConfig);
585
+ applyMiddlewareHeaders();
586
+ __flushPendingHeaders();
326
587
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
327
588
  await pipeWebStreamToResponse(htmlStream, res);
328
589
  return;
@@ -330,18 +591,30 @@ const server = createServer(async (req, res) => {
330
591
  } catch (err) {
331
592
  console.error("RSC render error:", err);
332
593
  if (!res.headersSent) {
594
+ __flushPendingHeaders();
333
595
  await sendStatusPage(res, 500, pathname);
334
596
  }
335
597
  return;
336
598
  }
337
599
  }
338
600
 
339
- // 7. 404
601
+ // 9. Static assets fallback (dist/static/ for public files like favicon, etc.)
602
+ {
603
+ const filePath = join(STATIC_DIR, pathname);
604
+ const served = await serveStaticFile(res, filePath, false);
605
+ if (served) return;
606
+ }
607
+
608
+ // 10. 404
609
+ __flushPendingHeaders();
340
610
  await sendStatusPage(res, 404, pathname);
341
611
  } catch (err) {
342
612
  console.error("Request error:", err);
613
+ __flushPendingHeaders();
343
614
  await sendStatusPage(res, 500, pathname);
344
615
  }
616
+
617
+ }); // end runWithRequestContext
345
618
  });
346
619
 
347
620
  server.listen(PORT, HOST, () => {
@@ -1 +1 @@
1
- {"version":3,"file":"server-template.js","sourceRoot":"","sources":["../src/server-template.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAShE;;;;;;;;;GASG;AACH,MAAM,UAAU,mBAAmB,CACjC,SAAsB,EACtB,OAAsB;IAEtB,OAAO;;;;;;;;;;mEAU0D,OAAO,CAAC,IAAI;mCAC5C,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuV9D,CAAC;AACF,CAAC"}
1
+ {"version":3,"file":"server-template.js","sourceRoot":"","sources":["../src/server-template.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAWhE;;;;;;;;;GASG;AACH,MAAM,UAAU,mBAAmB,CACjC,SAAsB,EACtB,OAAsB;IAEtB,OAAO;;;;;;;;;;;;mEAY0D,OAAO,CAAC,IAAI;mCAC5C,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0BA4IrC,OAAO,CAAC,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgG3C,OAAO,CAAC,YAAY,KAAK,UAAU;QACjC,CAAC,CAAC;;;;CAIL;QACG,CAAC,CAAC,EACN;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqXC,CAAC;AACF,CAAC"}
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@catmint/adapter-node",
3
- "version": "0.0.0-prealpha.2",
3
+ "version": "0.0.0-prealpha.20",
4
4
  "type": "module",
5
5
  "private": false,
6
- "license": "MIT",
6
+ "license": "GPL-2.0",
7
7
  "files": [
8
8
  "dist"
9
9
  ],
@@ -14,7 +14,7 @@
14
14
  }
15
15
  },
16
16
  "dependencies": {
17
- "catmint": "0.0.0-prealpha.2"
17
+ "catmint": "0.0.0-prealpha.20"
18
18
  },
19
19
  "devDependencies": {
20
20
  "typescript": "^5.7.0"