fastscript 1.0.0 → 2.0.0

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.
Files changed (109) hide show
  1. package/CHANGELOG.md +32 -7
  2. package/LICENSE +33 -21
  3. package/README.md +567 -73
  4. package/node_modules/@fastscript/core-private/BOUNDARY.json +15 -0
  5. package/node_modules/@fastscript/core-private/README.md +5 -0
  6. package/node_modules/@fastscript/core-private/package.json +34 -0
  7. package/node_modules/@fastscript/core-private/src/asset-optimizer.mjs +67 -0
  8. package/node_modules/@fastscript/core-private/src/audit-log.mjs +50 -0
  9. package/node_modules/@fastscript/core-private/src/auth-flows.mjs +29 -0
  10. package/node_modules/@fastscript/core-private/src/auth.mjs +115 -0
  11. package/node_modules/@fastscript/core-private/src/bench.mjs +45 -0
  12. package/node_modules/@fastscript/core-private/src/build.mjs +670 -0
  13. package/node_modules/@fastscript/core-private/src/cache.mjs +248 -0
  14. package/node_modules/@fastscript/core-private/src/check.mjs +22 -0
  15. package/node_modules/@fastscript/core-private/src/cli.mjs +95 -0
  16. package/node_modules/@fastscript/core-private/src/compat.mjs +128 -0
  17. package/node_modules/@fastscript/core-private/src/create.mjs +278 -0
  18. package/node_modules/@fastscript/core-private/src/csp.mjs +26 -0
  19. package/node_modules/@fastscript/core-private/src/db-cli.mjs +185 -0
  20. package/node_modules/@fastscript/core-private/src/db-postgres-collection.mjs +110 -0
  21. package/node_modules/@fastscript/core-private/src/db-postgres.mjs +40 -0
  22. package/node_modules/@fastscript/core-private/src/db.mjs +103 -0
  23. package/node_modules/@fastscript/core-private/src/deploy.mjs +662 -0
  24. package/node_modules/@fastscript/core-private/src/dev.mjs +5 -0
  25. package/node_modules/@fastscript/core-private/src/docs-search.mjs +35 -0
  26. package/node_modules/@fastscript/core-private/src/env.mjs +118 -0
  27. package/node_modules/@fastscript/core-private/src/export.mjs +83 -0
  28. package/node_modules/@fastscript/core-private/src/fs-diagnostics.mjs +70 -0
  29. package/node_modules/@fastscript/core-private/src/fs-error-codes.mjs +141 -0
  30. package/node_modules/@fastscript/core-private/src/fs-formatter.mjs +66 -0
  31. package/node_modules/@fastscript/core-private/src/fs-linter.mjs +274 -0
  32. package/node_modules/@fastscript/core-private/src/fs-normalize.mjs +91 -0
  33. package/node_modules/@fastscript/core-private/src/fs-parser.mjs +980 -0
  34. package/node_modules/@fastscript/core-private/src/generated/docs-search-index.mjs +3182 -0
  35. package/node_modules/@fastscript/core-private/src/i18n.mjs +25 -0
  36. package/node_modules/@fastscript/core-private/src/interop.mjs +16 -0
  37. package/node_modules/@fastscript/core-private/src/jobs.mjs +378 -0
  38. package/node_modules/@fastscript/core-private/src/logger.mjs +27 -0
  39. package/node_modules/@fastscript/core-private/src/metrics.mjs +45 -0
  40. package/node_modules/@fastscript/core-private/src/middleware.mjs +14 -0
  41. package/node_modules/@fastscript/core-private/src/migrate.mjs +81 -0
  42. package/node_modules/@fastscript/core-private/src/migration-wizard.mjs +16 -0
  43. package/node_modules/@fastscript/core-private/src/module-loader.mjs +46 -0
  44. package/node_modules/@fastscript/core-private/src/oauth-providers.mjs +103 -0
  45. package/node_modules/@fastscript/core-private/src/observability.mjs +21 -0
  46. package/node_modules/@fastscript/core-private/src/plugins.mjs +194 -0
  47. package/node_modules/@fastscript/core-private/src/retention.mjs +57 -0
  48. package/node_modules/@fastscript/core-private/src/routes.mjs +178 -0
  49. package/node_modules/@fastscript/core-private/src/scheduler.mjs +104 -0
  50. package/node_modules/@fastscript/core-private/src/security.mjs +233 -0
  51. package/node_modules/@fastscript/core-private/src/server-runtime.mjs +849 -0
  52. package/node_modules/@fastscript/core-private/src/serverless-handler.mjs +20 -0
  53. package/node_modules/@fastscript/core-private/src/session-policy.mjs +38 -0
  54. package/node_modules/@fastscript/core-private/src/start.mjs +10 -0
  55. package/node_modules/@fastscript/core-private/src/storage.mjs +155 -0
  56. package/node_modules/@fastscript/core-private/src/style-primitives.mjs +538 -0
  57. package/node_modules/@fastscript/core-private/src/style-system.mjs +461 -0
  58. package/node_modules/@fastscript/core-private/src/tenant.mjs +55 -0
  59. package/node_modules/@fastscript/core-private/src/typecheck.mjs +1464 -0
  60. package/node_modules/@fastscript/core-private/src/validate.mjs +22 -0
  61. package/node_modules/@fastscript/core-private/src/validation.mjs +88 -0
  62. package/node_modules/@fastscript/core-private/src/webhook.mjs +81 -0
  63. package/node_modules/@fastscript/core-private/src/worker.mjs +24 -0
  64. package/package.json +86 -13
  65. package/src/asset-optimizer.mjs +67 -0
  66. package/src/audit-log.mjs +50 -0
  67. package/src/auth.mjs +1 -115
  68. package/src/bench.mjs +20 -7
  69. package/src/build.mjs +1 -234
  70. package/src/cache.mjs +210 -20
  71. package/src/cli.mjs +29 -5
  72. package/src/compat.mjs +8 -10
  73. package/src/create.mjs +71 -17
  74. package/src/csp.mjs +26 -0
  75. package/src/db-cli.mjs +152 -8
  76. package/src/db-postgres-collection.mjs +110 -0
  77. package/src/deploy.mjs +1 -65
  78. package/src/docs-search.mjs +35 -0
  79. package/src/env.mjs +34 -5
  80. package/src/fs-diagnostics.mjs +70 -0
  81. package/src/fs-error-codes.mjs +126 -0
  82. package/src/fs-formatter.mjs +66 -0
  83. package/src/fs-linter.mjs +274 -0
  84. package/src/fs-normalize.mjs +21 -238
  85. package/src/fs-parser.mjs +1 -0
  86. package/src/generated/docs-search-index.mjs +3220 -0
  87. package/src/i18n.mjs +25 -0
  88. package/src/jobs.mjs +283 -32
  89. package/src/metrics.mjs +45 -0
  90. package/src/migration-wizard.mjs +16 -0
  91. package/src/module-loader.mjs +11 -12
  92. package/src/oauth-providers.mjs +103 -0
  93. package/src/plugins.mjs +194 -0
  94. package/src/retention.mjs +57 -0
  95. package/src/routes.mjs +178 -0
  96. package/src/scheduler.mjs +104 -0
  97. package/src/security.mjs +197 -19
  98. package/src/server-runtime.mjs +1 -339
  99. package/src/serverless-handler.mjs +20 -0
  100. package/src/session-policy.mjs +38 -0
  101. package/src/storage.mjs +1 -56
  102. package/src/style-system.mjs +461 -0
  103. package/src/tenant.mjs +55 -0
  104. package/src/typecheck.mjs +1 -0
  105. package/src/validate.mjs +5 -1
  106. package/src/validation.mjs +14 -5
  107. package/src/webhook.mjs +1 -71
  108. package/src/worker.mjs +23 -4
  109. package/src/language-spec.mjs +0 -58
@@ -0,0 +1,849 @@
1
+ import { createServer } from "node:http";
2
+ import { existsSync, readFileSync, statSync, watch } from "node:fs";
3
+ import { extname, join, resolve } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { runBuild } from "./build.mjs";
6
+ import { parseCookies, serializeCookie, createSessionManager, requireUser } from "./auth.mjs";
7
+ import { createFileDatabase } from "./db.mjs";
8
+ import { createPostgresCollectionDatabase } from "./db-postgres-collection.mjs";
9
+ import { composeMiddleware } from "./middleware.mjs";
10
+ import { readJsonBody, validateShape } from "./validation.mjs";
11
+ import { loadEnv, validateAppEnv } from "./env.mjs";
12
+ import { createLogger } from "./logger.mjs";
13
+ import { createDistributedJobQueue } from "./jobs.mjs";
14
+ import { abuseGuard, requestQuota, securityHeaders, rateLimit, csrf } from "./security.mjs";
15
+ import { createFileCache, createRedisCache } from "./cache.mjs";
16
+ import { createTracer } from "./observability.mjs";
17
+ import { createLocalStorage, createS3CompatibleStorage } from "./storage.mjs";
18
+ import { createPluginRuntime } from "./plugins.mjs";
19
+ import { createMetricsStore } from "./metrics.mjs";
20
+ import { resolveSessionPolicy } from "./session-policy.mjs";
21
+ import { createAuditLog } from "./audit-log.mjs";
22
+ import { scopeCacheByTenant, scopeDbByTenant, resolveTenantId } from "./tenant.mjs";
23
+ import { getI18nConfig, resolveLocaleFromPath } from "./i18n.mjs";
24
+ import { transformStylePrimitives } from "./style-primitives.mjs";
25
+
26
+ const DIST_DIR = resolve("dist");
27
+ const DB_DIR = resolve(".fastscript");
28
+
29
+ function setupAppWatcher(onChange, logger) {
30
+ const appRoot = resolve("app");
31
+ if (!existsSync(appRoot)) return () => {};
32
+ const shouldIgnore = (filename) => {
33
+ const rel = String(filename || "").replace(/\\/g, "/");
34
+ return rel.endsWith("styles.generated.css");
35
+ };
36
+ const handleChange = (eventType, filename) => {
37
+ if (shouldIgnore(filename)) return;
38
+ onChange(eventType, filename);
39
+ };
40
+ try {
41
+ const watcher = watch(appRoot, { recursive: true }, handleChange);
42
+ return () => watcher.close();
43
+ } catch (error) {
44
+ logger.warn("recursive_watch_unavailable", { error: error?.message || String(error) });
45
+ const watcher = watch(appRoot, handleChange);
46
+ return () => watcher.close();
47
+ }
48
+ }
49
+
50
+ function contentType(path) {
51
+ const ext = extname(path);
52
+ if (ext === ".html") return "text/html; charset=utf-8";
53
+ if (ext === ".js") return "application/javascript; charset=utf-8";
54
+ if (ext === ".css") return "text/css; charset=utf-8";
55
+ if (ext === ".json") return "application/json; charset=utf-8";
56
+ if (ext === ".map") return "application/json; charset=utf-8";
57
+ return "text/plain; charset=utf-8";
58
+ }
59
+
60
+ function readManifest() {
61
+ const path = join(DIST_DIR, "fastscript-manifest.json");
62
+ return JSON.parse(readFileSync(path, "utf8"));
63
+ }
64
+
65
+ function readAssetManifest() {
66
+ const path = join(DIST_DIR, "asset-manifest.json");
67
+ if (!existsSync(path)) return { mapping: {} };
68
+ return JSON.parse(readFileSync(path, "utf8"));
69
+ }
70
+
71
+ function assetPath(name, mapping = {}) {
72
+ return `/${mapping[name] || name}`;
73
+ }
74
+
75
+ function parseRouteToken(token) {
76
+ const m = /^:([A-Za-z_$][\w$]*)(\*)?(\?)?$/.exec(token || "");
77
+ if (!m) return null;
78
+ return { name: m[1], catchAll: Boolean(m[2]), optional: Boolean(m[3]) };
79
+ }
80
+
81
+ function match(routePath, pathname) {
82
+ const routeParts = routePath.split("/").filter(Boolean);
83
+ const pathParts = pathname.split("/").filter(Boolean);
84
+ const params = {};
85
+ let ri = 0;
86
+ let pi = 0;
87
+
88
+ while (ri < routeParts.length) {
89
+ const token = routeParts[ri];
90
+ const dyn = parseRouteToken(token);
91
+
92
+ if (dyn?.catchAll) {
93
+ const rest = pathParts.slice(pi);
94
+ if (!rest.length && !dyn.optional) return null;
95
+ params[dyn.name] = rest;
96
+ pi = pathParts.length;
97
+ ri = routeParts.length;
98
+ break;
99
+ }
100
+
101
+ if (dyn) {
102
+ const value = pathParts[pi];
103
+ if (value === undefined) {
104
+ if (dyn.optional) {
105
+ params[dyn.name] = undefined;
106
+ ri += 1;
107
+ continue;
108
+ }
109
+ return null;
110
+ }
111
+ params[dyn.name] = value;
112
+ ri += 1;
113
+ pi += 1;
114
+ continue;
115
+ }
116
+
117
+ if (pathParts[pi] !== token) return null;
118
+ ri += 1;
119
+ pi += 1;
120
+ }
121
+
122
+ if (pi !== pathParts.length) return null;
123
+ return params;
124
+ }
125
+
126
+ function routePriorityScore(routePath) {
127
+ const parts = String(routePath || "/").split("/").filter(Boolean);
128
+ if (!parts.length) return 1000;
129
+ let score = parts.length;
130
+ for (const part of parts) {
131
+ const dyn = parseRouteToken(part);
132
+ if (!dyn) score += 40;
133
+ else if (dyn.catchAll && dyn.optional) score += 5;
134
+ else if (dyn.catchAll) score += 10;
135
+ else if (dyn.optional) score += 20;
136
+ else score += 30;
137
+ }
138
+ return score;
139
+ }
140
+
141
+ function resolveRoute(routes, pathname) {
142
+ let best = null;
143
+ for (const route of routes) {
144
+ const params = match(route.path, pathname);
145
+ if (!params) continue;
146
+ if (!best) {
147
+ best = { route, params, score: routePriorityScore(route.path) };
148
+ continue;
149
+ }
150
+ const score = routePriorityScore(route.path);
151
+ if (score > best.score) best = { route, params, score };
152
+ }
153
+ return best ? { route: best.route, params: best.params } : null;
154
+ }
155
+
156
+ async function importDist(modulePath) {
157
+ const abs = join(DIST_DIR, modulePath.replace(/^\.\//, ""));
158
+ const url = `${pathToFileURL(abs).href}?t=${Date.now()}`;
159
+ return import(url);
160
+ }
161
+
162
+ function createHelpers(res, { secureCookies = false, defaultSameSite = "Lax", defaultPath = "/" } = {}) {
163
+ return {
164
+ json(body, status = 200, headers = {}) {
165
+ return { status, json: body, headers };
166
+ },
167
+ text(body, status = 200, headers = {}) {
168
+ return { status, body, headers };
169
+ },
170
+ redirect(location, status = 302) {
171
+ return { status, headers: { location } };
172
+ },
173
+ setCookie(name, value, opts = {}) {
174
+ const current = res.getHeader("set-cookie");
175
+ const next = serializeCookie(name, value, {
176
+ sameSite: defaultSameSite,
177
+ secure: secureCookies,
178
+ path: defaultPath,
179
+ ...opts,
180
+ });
181
+ if (!current) res.setHeader("set-cookie", [next]);
182
+ else res.setHeader("set-cookie", Array.isArray(current) ? [...current, next] : [String(current), next]);
183
+ },
184
+ };
185
+ }
186
+
187
+ function writeResponse(res, payload) {
188
+ if (!payload) {
189
+ res.writeHead(204);
190
+ res.end();
191
+ return;
192
+ }
193
+ const status = payload.status ?? 200;
194
+ const headers = payload.headers ?? {};
195
+ if (payload.cookies && payload.cookies.length) headers["set-cookie"] = payload.cookies;
196
+ if (payload.json !== undefined) {
197
+ res.writeHead(status, { "content-type": "application/json; charset=utf-8", ...headers });
198
+ res.end(JSON.stringify(payload.json));
199
+ return;
200
+ }
201
+ res.writeHead(status, { "content-type": "text/plain; charset=utf-8", ...headers });
202
+ res.end(payload.body ?? "");
203
+ }
204
+
205
+ function htmlDoc(content, ssrData, { hasStyles = false, stylesHref = "/styles.css", routerHref = "/router.js" } = {}) {
206
+ const safe = JSON.stringify(ssrData ?? {}).replace(/</g, "\\u003c");
207
+ const faviconSvg = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='8' fill='%23000'/><text x='50%25' y='54%25' dominant-baseline='middle' text-anchor='middle' font-family='system-ui,sans-serif' font-weight='800' font-size='16' fill='%23fff'>FS</text></svg>`;
208
+ const faviconUrl = `data:image/svg+xml,${faviconSvg}`;
209
+ return `<!doctype html>
210
+ <html>
211
+ <head>
212
+ <meta charset="utf-8" />
213
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
214
+ <title>FastScript</title>
215
+ <meta name="theme-color" content="#000000" />
216
+ <link rel="icon" type="image/svg+xml" href="${faviconUrl}" />
217
+ ${hasStyles ? `<link rel="stylesheet" href="${stylesHref}" />` : ""}
218
+ </head>
219
+ <body>
220
+ <div id="app">${content}</div>
221
+ <script>window.__FASTSCRIPT_SSR=${safe}</script>
222
+ <script type="module" src="${routerHref}"></script>
223
+ <script>
224
+ (function () {
225
+ if (!("serviceWorker" in navigator)) return;
226
+ const host = (location && location.hostname) || "";
227
+ const isLocalhost = host === "localhost" || host === "127.0.0.1" || host === "::1";
228
+ if (!isLocalhost) return;
229
+ navigator.serviceWorker.getRegistrations()
230
+ .then((regs) => Promise.all(regs.map((r) => r.unregister())))
231
+ .catch(() => {});
232
+ if (window.caches && typeof window.caches.keys === "function") {
233
+ window.caches.keys()
234
+ .then((keys) => Promise.all(keys.filter((k) => String(k).indexOf("fastscript-") === 0).map((k) => window.caches.delete(k))))
235
+ .catch(() => {});
236
+ }
237
+ })();
238
+ </script>
239
+ </body>
240
+ </html>`;
241
+ }
242
+
243
+ export async function runServer({ mode = "development", watchMode = false, buildOnStart = true, port = 4173, listen = true } = {}) {
244
+ loadEnv({ mode });
245
+ await validateAppEnv();
246
+
247
+ const isProduction = (mode || process.env.NODE_ENV || "development") === "production";
248
+ const sessionPolicy = resolveSessionPolicy({ mode });
249
+
250
+ const logger = createLogger({ service: "fastscript-server" });
251
+ const tracer = createTracer({ service: "fastscript-server" });
252
+ const plugins = await createPluginRuntime({ logger });
253
+ const metrics = createMetricsStore({ dir: DB_DIR, name: "metrics" });
254
+ const audit = createAuditLog({ file: join(DB_DIR, "audit.log") });
255
+ const i18n = getI18nConfig(process.env);
256
+ if (buildOnStart) await runBuild();
257
+
258
+ const sessions = createSessionManager({
259
+ dir: DB_DIR,
260
+ cookieName: sessionPolicy.cookie.name,
261
+ secret: sessionPolicy.secret,
262
+ });
263
+ let db = createFileDatabase({ dir: DB_DIR, name: "appdb" });
264
+ if ((process.env.DB_DRIVER || "").toLowerCase() === "postgres") {
265
+ try {
266
+ db = await createPostgresCollectionDatabase({ connectionString: process.env.DATABASE_URL });
267
+ logger.info("db_driver", { driver: "postgres" });
268
+ } catch (error) {
269
+ logger.warn("db_driver_fallback", { driver: "file", error: error?.message || String(error) });
270
+ db = createFileDatabase({ dir: DB_DIR, name: "appdb" });
271
+ }
272
+ }
273
+ const queue = await createDistributedJobQueue({ dir: DB_DIR, driver: process.env.JOBS_DRIVER || "file" });
274
+
275
+ let cache = createFileCache({ dir: join(DB_DIR, "cache") });
276
+ if ((process.env.CACHE_DRIVER || "").toLowerCase() === "redis") {
277
+ try {
278
+ cache = await createRedisCache({ url: process.env.REDIS_URL });
279
+ logger.info("cache_driver", { driver: "redis" });
280
+ } catch (error) {
281
+ logger.warn("cache_driver_fallback", { driver: "file", error: error?.message || String(error) });
282
+ }
283
+ }
284
+
285
+ let storage = createLocalStorage({ dir: join(DB_DIR, "storage") });
286
+ if ((process.env.STORAGE_DRIVER || "").toLowerCase() === "s3") {
287
+ const bucket = process.env.STORAGE_S3_BUCKET;
288
+ const endpoint = process.env.STORAGE_S3_ENDPOINT;
289
+ const presignBaseUrl = process.env.STORAGE_S3_PRESIGN_BASE_URL;
290
+ if (bucket && endpoint && presignBaseUrl) {
291
+ storage = createS3CompatibleStorage({ bucket, endpoint, presignBaseUrl, region: process.env.STORAGE_S3_REGION || "auto" });
292
+ logger.info("storage_driver", { driver: "s3-compatible" });
293
+ } else {
294
+ logger.warn("storage_driver_fallback", { driver: "local", reason: "missing_s3_env" });
295
+ }
296
+ }
297
+ let stopWatching = null;
298
+ const hmrClients = new Set();
299
+
300
+ function pushHmr(event = { type: "reload", ts: Date.now() }) {
301
+ const payload = `data: ${JSON.stringify(event)}\n\n`;
302
+ for (const res of hmrClients) {
303
+ try {
304
+ res.write(payload);
305
+ } catch {}
306
+ }
307
+ }
308
+
309
+ if (watchMode) {
310
+ let timer = null;
311
+ let rebuildRunning = false;
312
+ let rebuildQueued = false;
313
+ stopWatching = setupAppWatcher(() => {
314
+ clearTimeout(timer);
315
+ timer = setTimeout(async () => {
316
+ if (rebuildRunning) {
317
+ rebuildQueued = true;
318
+ return;
319
+ }
320
+ rebuildRunning = true;
321
+ try {
322
+ await runBuild();
323
+ logger.info("rebuild complete");
324
+ pushHmr({ type: "rebuild", ts: Date.now() });
325
+ } catch (error) {
326
+ logger.error("rebuild failed", { error: error.message });
327
+ pushHmr({ type: "rebuild_error", error: error?.message || String(error), ts: Date.now() });
328
+ } finally {
329
+ rebuildRunning = false;
330
+ if (rebuildQueued) {
331
+ rebuildQueued = false;
332
+ setTimeout(() => {
333
+ try {
334
+ pushHmr({ type: "reload", ts: Date.now() });
335
+ } catch {}
336
+ }, 10);
337
+ }
338
+ }
339
+ }, 120);
340
+ }, logger);
341
+ }
342
+
343
+ const server = createServer(async (req, res) => {
344
+ const requestId = logger.requestId();
345
+ const start = Date.now();
346
+ const span = tracer.span("request", { requestId, path: req.url, method: req.method });
347
+ res.setHeader("x-request-id", requestId);
348
+ let responseStatus = 500;
349
+ let responseKind = "error";
350
+ let responseError = null;
351
+ let requestCtx = null;
352
+
353
+ try {
354
+ const url = new URL(req.url || "/", "http://localhost");
355
+ const originalPathname = url.pathname;
356
+ const localized = resolveLocaleFromPath(originalPathname, i18n);
357
+ const pathname = localized.pathname;
358
+ const method = (req.method || "GET").toUpperCase();
359
+ const manifest = readManifest();
360
+ const assets = readAssetManifest();
361
+ const requestHost = String(req.headers.host || req.headers["x-forwarded-host"] || "").toLowerCase();
362
+ const localhostCookieRelaxed = requestHost.startsWith("localhost") || requestHost.startsWith("127.0.0.1") || requestHost.startsWith("[::1]");
363
+ const cookieSecure = localhostCookieRelaxed ? false : sessionPolicy.cookie.secure;
364
+ const helpers = createHelpers(res, {
365
+ secureCookies: cookieSecure,
366
+ defaultSameSite: sessionPolicy.cookie.sameSite,
367
+ defaultPath: sessionPolicy.cookie.path,
368
+ });
369
+ requestCtx = { requestId, pathname: originalPathname, method, mode, locale: localized.locale };
370
+ await plugins.onRequestStart(requestCtx);
371
+ const cookies = parseCookies(req.headers.cookie || "");
372
+ const session = sessions.read(cookies[sessions.cookieName]);
373
+ sessions.sweepExpired();
374
+ if (sessionPolicy.rotateOnRead && cookies[sessions.cookieName]) {
375
+ const rotated = sessions.rotate(cookies[sessions.cookieName], sessionPolicy.cookie.maxAgeSec);
376
+ if (rotated) {
377
+ helpers.setCookie(sessions.cookieName, rotated, {
378
+ path: sessionPolicy.cookie.path,
379
+ httpOnly: sessionPolicy.cookie.httpOnly,
380
+ sameSite: sessionPolicy.cookie.sameSite,
381
+ secure: cookieSecure,
382
+ maxAge: sessionPolicy.cookie.maxAgeSec,
383
+ });
384
+ }
385
+ }
386
+ const tenantId = resolveTenantId(req, { fallback: process.env.DEFAULT_TENANT_ID || "public" });
387
+ const tenantDb = scopeDbByTenant(db, tenantId);
388
+ const tenantCache = scopeCacheByTenant(cache, tenantId);
389
+
390
+ const ctx = {
391
+ req,
392
+ res,
393
+ requestId,
394
+ tenantId,
395
+ locale: localized.locale,
396
+ originalPathname,
397
+ pathname,
398
+ method,
399
+ params: {},
400
+ query: Object.fromEntries(url.searchParams.entries()),
401
+ cookies,
402
+ user: session?.user ?? null,
403
+ db: tenantDb,
404
+ queue,
405
+ cache: tenantCache,
406
+ storage,
407
+ auth: {
408
+ login: (user, opts = {}) => {
409
+ const maxAge = opts.maxAge ?? sessionPolicy.cookie.maxAgeSec;
410
+ const token = sessions.create(user, maxAge);
411
+ helpers.setCookie(sessions.cookieName, token, {
412
+ path: sessionPolicy.cookie.path,
413
+ httpOnly: sessionPolicy.cookie.httpOnly,
414
+ sameSite: sessionPolicy.cookie.sameSite,
415
+ secure: cookieSecure,
416
+ maxAge,
417
+ });
418
+ audit.append({ action: "auth.login", actor: user?.id || "anonymous", tenantId, requestId, pathname });
419
+ return token;
420
+ },
421
+ logout: () => {
422
+ sessions.delete(cookies[sessions.cookieName]);
423
+ helpers.setCookie(sessions.cookieName, "", {
424
+ path: sessionPolicy.cookie.path,
425
+ httpOnly: sessionPolicy.cookie.httpOnly,
426
+ sameSite: sessionPolicy.cookie.sameSite,
427
+ secure: cookieSecure,
428
+ maxAge: 0,
429
+ });
430
+ audit.append({ action: "auth.logout", actor: session?.user?.id || "anonymous", tenantId, requestId, pathname });
431
+ },
432
+ requireUser: () => requireUser(session?.user ?? null),
433
+ rotate: (opts = {}) => {
434
+ const maxAge = opts.maxAge ?? sessionPolicy.cookie.maxAgeSec;
435
+ const token = sessions.rotate(cookies[sessions.cookieName], maxAge);
436
+ if (token) helpers.setCookie(sessions.cookieName, token, {
437
+ path: sessionPolicy.cookie.path,
438
+ httpOnly: sessionPolicy.cookie.httpOnly,
439
+ sameSite: sessionPolicy.cookie.sameSite,
440
+ secure: cookieSecure,
441
+ maxAge,
442
+ });
443
+ if (token) audit.append({ action: "auth.rotate", actor: session?.user?.id || "anonymous", tenantId, requestId, pathname });
444
+ return token;
445
+ },
446
+ },
447
+ input: {
448
+ body: null,
449
+ query: Object.fromEntries(url.searchParams.entries()),
450
+ async readJson() {
451
+ if (ctx.input.body !== null) return ctx.input.body;
452
+ ctx.input.body = await readJsonBody(req, { maxBytes: Number(process.env.MAX_BODY_BYTES || 1024 * 1024) });
453
+ return ctx.input.body;
454
+ },
455
+ validateQuery(schema) {
456
+ return validateShape(schema, ctx.query, "query").value;
457
+ },
458
+ async validateBody(schema) {
459
+ const body = await ctx.input.readJson();
460
+ return validateShape(schema, body, "body").value;
461
+ },
462
+ },
463
+ helpers,
464
+ };
465
+
466
+ const isBodyMethod = !["GET", "HEAD"].includes(ctx.method);
467
+ const contentTypeHeader = String(req.headers["content-type"] || "");
468
+ if (isBodyMethod && contentTypeHeader.includes("application/json")) {
469
+ ctx.input.body = await ctx.input.readJson();
470
+ }
471
+
472
+ const target = join(DIST_DIR, pathname === "/" ? "index.html" : pathname.slice(1));
473
+ const isStaticAssetRequest =
474
+ /\.[a-zA-Z0-9]+$/.test(pathname) ||
475
+ pathname === "/manifest.webmanifest" ||
476
+ pathname === "/service-worker.js" ||
477
+ pathname === "/fastscript-manifest.json" ||
478
+ pathname === "/asset-manifest.json";
479
+ if (pathname === "/__hmr" && watchMode) {
480
+ res.writeHead(200, {
481
+ "content-type": "text/event-stream",
482
+ "cache-control": "no-cache, no-transform",
483
+ connection: "keep-alive",
484
+ });
485
+ res.write("event: ready\ndata: {}\n\n");
486
+ hmrClients.add(res);
487
+ req.on("close", () => hmrClients.delete(res));
488
+ responseStatus = 200;
489
+ responseKind = "hmr";
490
+ span.end({ status: 200, kind: "hmr" });
491
+ return;
492
+ }
493
+ if (pathname === "/__metrics" && process.env.METRICS_PUBLIC === "1") {
494
+ writeResponse(res, { status: 200, json: metrics.snapshot() });
495
+ responseStatus = 200;
496
+ responseKind = "metrics";
497
+ span.end({ status: 200, kind: "metrics" });
498
+ return;
499
+ }
500
+ if (pathname === "/__cache/invalidate" && method === "POST") {
501
+ const body = await ctx.input.readJson();
502
+ const tag = String(body.tag || "").trim();
503
+ if (!tag) {
504
+ writeResponse(res, { status: 400, json: { ok: false, error: "missing_tag" } });
505
+ responseStatus = 400;
506
+ responseKind = "cache";
507
+ span.end({ status: 400, kind: "cache" });
508
+ return;
509
+ }
510
+ const count = await tenantCache.invalidateTag(tag);
511
+ audit.append({ action: "cache.invalidate", actor: session?.user?.id || "anonymous", tenantId, requestId, pathname, tag, count });
512
+ writeResponse(res, { status: 200, json: { ok: true, tag, count } });
513
+ responseStatus = 200;
514
+ responseKind = "cache";
515
+ span.end({ status: 200, kind: "cache" });
516
+ return;
517
+ }
518
+ if (pathname === "/__observability" && process.env.OBSERVABILITY_UI !== "0") {
519
+ const snap = metrics.snapshot();
520
+ const html = `<!doctype html><html><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><title>FastScript Observability</title><style>body{font:14px/1.4 ui-sans-serif,system-ui;background:#070707;color:#fff;padding:20px}pre{background:#111;padding:16px;border-radius:10px;overflow:auto}h1{font-size:18px}</style></head><body><h1>Observability Dashboard</h1><h2>Metrics</h2><pre>${JSON.stringify(snap, null, 2)}</pre><h2>Audit Chain</h2><pre>${JSON.stringify(audit.verify(), null, 2)}</pre></body></html>`;
521
+ writeResponse(res, { status: 200, body: html, headers: { "content-type": "text/html; charset=utf-8" } });
522
+ responseStatus = 200;
523
+ responseKind = "observability";
524
+ span.end({ status: 200, kind: "observability" });
525
+ return;
526
+ }
527
+ if (pathname === "/__alerts" && process.env.OBSERVABILITY_UI !== "0") {
528
+ const snap = metrics.snapshot();
529
+ const total = snap.counters.requests_total || 0;
530
+ const errors = snap.counters.requests_error_total || 0;
531
+ const p95 = snap.timings.request_duration_ms?.max || 0;
532
+ const alert = {
533
+ status: errors > Math.max(5, total * 0.05) || p95 > Number(process.env.SLO_P95_MS || 1200) ? "degraded" : "healthy",
534
+ triage: errors > 0 ? "check /__observability and recent deploys; inspect dead-letter queue growth" : "no incident signal",
535
+ counters: snap.counters,
536
+ timings: snap.timings,
537
+ };
538
+ writeResponse(res, { status: 200, json: alert });
539
+ responseStatus = 200;
540
+ responseKind = "alerts";
541
+ span.end({ status: 200, kind: "alerts" });
542
+ return;
543
+ }
544
+ if (pathname.startsWith("/__storage/")) {
545
+ if (pathname === "/__storage/signed") {
546
+ const token = String(url.searchParams.get("token") || "");
547
+ const payload = storage.verifySignedUrl ? storage.verifySignedUrl(token) : null;
548
+ if (!payload || payload.action !== "get") {
549
+ writeResponse(res, { status: 401, body: "Invalid signed URL" });
550
+ responseStatus = 401;
551
+ responseKind = "storage";
552
+ span.end({ status: 401, kind: "storage" });
553
+ return;
554
+ }
555
+ const record = storage.get(payload.key);
556
+ if (!record) {
557
+ writeResponse(res, { status: 404, body: "Not found" });
558
+ responseStatus = 404;
559
+ responseKind = "storage";
560
+ span.end({ status: 404, kind: "storage" });
561
+ return;
562
+ }
563
+ res.writeHead(200, { "content-type": "application/octet-stream" });
564
+ res.end(record);
565
+ responseStatus = 200;
566
+ responseKind = "storage";
567
+ span.end({ status: 200, kind: "storage" });
568
+ return;
569
+ }
570
+ const key = decodeURIComponent(pathname.slice("/__storage/".length));
571
+ const record = storage.get(key);
572
+ if (!record) {
573
+ writeResponse(res, { status: 404, body: "Not found" });
574
+ responseStatus = 404;
575
+ responseKind = "storage";
576
+ span.end({ status: 404, kind: "storage" });
577
+ return;
578
+ }
579
+ const meta = storage.meta ? storage.meta(key) : { acl: "public" };
580
+ if (meta.acl !== "public") {
581
+ writeResponse(res, { status: 403, body: "Forbidden" });
582
+ responseStatus = 403;
583
+ responseKind = "storage";
584
+ span.end({ status: 403, kind: "storage" });
585
+ return;
586
+ }
587
+ res.writeHead(200, { "content-type": "application/octet-stream" });
588
+ res.end(record);
589
+ responseStatus = 200;
590
+ responseKind = "storage";
591
+ span.end({ status: 200, kind: "storage" });
592
+ return;
593
+ }
594
+ if (isStaticAssetRequest && existsSync(target) && statSync(target).isFile() && !pathname.endsWith(".html")) {
595
+ const body = readFileSync(target);
596
+ const immutable = /\.[a-f0-9]{8}\.(js|css)$/.test(pathname);
597
+ res.writeHead(200, {
598
+ "content-type": contentType(target),
599
+ "cache-control": immutable ? "public, max-age=31536000, immutable" : "public, max-age=300",
600
+ });
601
+ res.end(body);
602
+ logger.info("static", { requestId, path: pathname, status: 200, ms: Date.now() - start });
603
+ responseStatus = 200;
604
+ responseKind = "static";
605
+ span.end({ status: 200, kind: "static" });
606
+ return;
607
+ }
608
+
609
+ const middlewareList = [];
610
+ middlewareList.push(securityHeaders(), rateLimit(), requestQuota(), abuseGuard());
611
+ if (process.env.CSRF_PROTECT !== "0") middlewareList.push(csrf());
612
+ const pluginMiddleware = plugins.middleware();
613
+ if (pluginMiddleware.length) middlewareList.push(...pluginMiddleware);
614
+ if (manifest.middleware) {
615
+ const mm = await importDist(manifest.middleware);
616
+ if (Array.isArray(mm.middlewares)) middlewareList.push(...mm.middlewares);
617
+ else if (typeof mm.middleware === "function") middlewareList.push(mm.middleware);
618
+ else if (typeof mm.default === "function") middlewareList.push(mm.default);
619
+ }
620
+ const runWithMiddleware = composeMiddleware(middlewareList);
621
+
622
+ const out = await runWithMiddleware(ctx, async () => {
623
+ async function renderParallelSlots(params, data) {
624
+ const slots = {};
625
+ for (const route of manifest.parallelRoutes || []) {
626
+ const slotHit = match(route.path, pathname);
627
+ if (!slotHit) continue;
628
+ const slotMod = await importDist(route.module);
629
+ let slotData = {};
630
+ if (typeof slotMod.load === "function") {
631
+ slotData = (await slotMod.load({ ...ctx, pathname, locale: ctx.locale, params, slot: route.slot })) || {};
632
+ }
633
+ slots[route.slot || "default"] = slotMod.default
634
+ ? slotMod.default({ ...slotData, params, pathname, locale: ctx.locale, slot: route.slot })
635
+ : "";
636
+ }
637
+ return slots;
638
+ }
639
+
640
+ async function applyLayouts(route, html, params, data, slots = {}) {
641
+ const layoutList = route.layouts && route.layouts.length
642
+ ? route.layouts
643
+ : (manifest.layout ? [manifest.layout] : []);
644
+ let outHtml = html;
645
+ for (const layoutPath of layoutList) {
646
+ const layout = await importDist(layoutPath);
647
+ outHtml = layout.default
648
+ ? layout.default({ content: outHtml, pathname, locale: ctx.locale, user: ctx.user, params, data, slots, tenantId: ctx.tenantId })
649
+ : outHtml;
650
+ }
651
+ return outHtml;
652
+ }
653
+
654
+ if (pathname.startsWith("/api/")) {
655
+ const apiHit = resolveRoute(manifest.apiRoutes, pathname);
656
+ if (!apiHit) return { status: 404, body: "API route not found" };
657
+ ctx.params = apiHit.params;
658
+ const mod = await importDist(apiHit.route.module);
659
+ const handler = mod[ctx.method];
660
+ if (typeof handler !== "function") return { status: 405, body: `Method ${ctx.method} not allowed` };
661
+ if (mod.schemas?.[ctx.method]) {
662
+ ctx.input.body = await ctx.input.readJson();
663
+ validateShape(mod.schemas[ctx.method], ctx.input.body, "body");
664
+ }
665
+ return handler(ctx, helpers);
666
+ }
667
+
668
+ const hit = resolveRoute(manifest.routes, pathname);
669
+ if (!hit) {
670
+ if (manifest.notFound) {
671
+ const nfMod = await importDist(manifest.notFound);
672
+ const body = nfMod.default ? transformStylePrimitives(nfMod.default({ pathname, locale: ctx.locale })) : "<h1>404</h1>";
673
+ return { status: 404, html: body, data: null };
674
+ }
675
+ return { status: 404, body: "Not found" };
676
+ }
677
+
678
+ ctx.params = hit.params;
679
+ const mod = await importDist(hit.route.module);
680
+
681
+ if (!["GET", "HEAD"].includes(ctx.method) && typeof mod[ctx.method] === "function") {
682
+ if (mod.schemas?.[ctx.method]) {
683
+ ctx.input.body = await ctx.input.readJson();
684
+ validateShape(mod.schemas[ctx.method], ctx.input.body, "body");
685
+ }
686
+ return mod[ctx.method](ctx, helpers);
687
+ }
688
+
689
+ const revalidateSec = Number(mod.revalidate || 0);
690
+ const isrEnabled = Number.isFinite(revalidateSec) && revalidateSec > 0 && ["GET", "HEAD"].includes(ctx.method);
691
+ const isrKey = `isr:${pathname}`;
692
+ if (isrEnabled) {
693
+ const cached = await tenantCache.get(isrKey);
694
+ if (cached && cached.html && cached.generatedAt && Date.now() - cached.generatedAt < revalidateSec * 1000) {
695
+ return { status: 200, html: cached.html, data: cached.data || {}, isr: true };
696
+ }
697
+ }
698
+
699
+ if (typeof mod.stream === "function" && ["GET", "HEAD"].includes(ctx.method)) {
700
+ return {
701
+ status: 200,
702
+ stream: mod.stream({ ...ctx, params: hit.params, pathname, locale: ctx.locale }),
703
+ route: hit.route,
704
+ mod,
705
+ };
706
+ }
707
+
708
+ let data = {};
709
+ if (typeof mod.load === "function") data = (await mod.load({ ...ctx, params: hit.params, pathname, locale: ctx.locale })) || {};
710
+ let html = mod.default ? mod.default({ ...data, params: hit.params, pathname, locale: ctx.locale, user: ctx.user }) : "";
711
+ const slots = await renderParallelSlots(hit.params, data);
712
+ html = await applyLayouts(hit.route, html, hit.params, data, slots);
713
+ html = transformStylePrimitives(html);
714
+
715
+ if (isrEnabled) {
716
+ await tenantCache.setWithTags(isrKey, { html, data, generatedAt: Date.now() }, {
717
+ ttlMs: revalidateSec * 1000,
718
+ tags: [`route:${pathname}`, "isr"],
719
+ });
720
+ }
721
+ return { status: 200, html, data };
722
+ });
723
+
724
+ if (out?.stream) {
725
+ const hasStyles = existsSync(join(DIST_DIR, "styles.css")) || Boolean(assets.mapping["styles.css"]);
726
+ const stylesHref = assetPath("styles.css", assets.mapping || {});
727
+ const routerHref = assetPath("router.js", assets.mapping || {});
728
+ const safe = JSON.stringify({ pathname, data: null }).replace(/</g, "\\u003c");
729
+ responseStatus = out.status ?? 200;
730
+ responseKind = "stream";
731
+ res.writeHead(responseStatus, { "content-type": "text/html; charset=utf-8", "transfer-encoding": "chunked" });
732
+ const faviconSvg = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='8' fill='%23000'/><text x='50%25' y='54%25' dominant-baseline='middle' text-anchor='middle' font-family='system-ui,sans-serif' font-weight='800' font-size='16' fill='%23fff'>FS</text></svg>`;
733
+ const faviconUrl = `data:image/svg+xml,${faviconSvg}`;
734
+ res.write(`<!doctype html><html><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><title>FastScript</title><meta name="theme-color" content="#000000"/><link rel="icon" type="image/svg+xml" href="${faviconUrl}"/>${hasStyles ? `<link rel="stylesheet" href="${stylesHref}" />` : ""}</head><body><div id="app">`);
735
+ for await (const chunk of out.stream) {
736
+ res.write(String(chunk ?? ""));
737
+ }
738
+ res.write(`</div><script>window.__FASTSCRIPT_SSR=${safe}</script><script type="module" src="${routerHref}"></script><script>(function(){if(!("serviceWorker" in navigator))return;const host=(location&&location.hostname)||"";const isLocalhost=host==="localhost"||host==="127.0.0.1"||host==="::1";if(!isLocalhost)return;navigator.serviceWorker.getRegistrations().then((regs)=>Promise.all(regs.map((r)=>r.unregister()))).catch(()=>{});if(window.caches&&typeof window.caches.keys==="function"){window.caches.keys().then((keys)=>Promise.all(keys.filter((k)=>String(k).indexOf("fastscript-")===0).map((k)=>window.caches.delete(k)))).catch(()=>{});}})();</script></body></html>`);
739
+ res.end();
740
+ logger.info("ssr_stream", { requestId, path: pathname, status: responseStatus, ms: Date.now() - start });
741
+ span.end({ status: responseStatus, kind: "stream" });
742
+ return;
743
+ }
744
+
745
+ if (out?.html !== undefined) {
746
+ const hasStyles = existsSync(join(DIST_DIR, "styles.css")) || Boolean(assets.mapping["styles.css"]);
747
+ const payload = { pathname, data: out.data ?? null };
748
+ const stylesHref = assetPath("styles.css", assets.mapping || {});
749
+ const routerHref = assetPath("router.js", assets.mapping || {});
750
+ responseStatus = out.status ?? 200;
751
+ responseKind = out?.isr ? "isr" : "ssr";
752
+ res.writeHead(responseStatus, { "content-type": "text/html; charset=utf-8" });
753
+ res.end(htmlDoc(out.html, payload, { hasStyles, stylesHref, routerHref }));
754
+ logger.info("ssr", { requestId, path: pathname, status: responseStatus, ms: Date.now() - start, isr: Boolean(out?.isr) });
755
+ span.end({ status: responseStatus, kind: out?.isr ? "isr" : "ssr" });
756
+ return;
757
+ }
758
+
759
+ writeResponse(res, out);
760
+ responseStatus = out?.status ?? 200;
761
+ responseKind = "response";
762
+ logger.info("response", { requestId, path: pathname, status: responseStatus, ms: Date.now() - start });
763
+ span.end({ status: responseStatus, kind: "response" });
764
+ } catch (error) {
765
+ const status = error?.status && Number.isInteger(error.status) ? error.status : 500;
766
+ const payload = {
767
+ ok: false,
768
+ error: {
769
+ message: error?.message || "Unknown error",
770
+ status,
771
+ details: error?.details || null,
772
+ },
773
+ };
774
+ const wantsJson = (req.headers.accept || "").includes("application/json") || (req.url || "").startsWith("/api/");
775
+ if (wantsJson) {
776
+ res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
777
+ res.end(JSON.stringify(payload));
778
+ } else {
779
+ res.writeHead(status, { "content-type": "text/html; charset=utf-8" });
780
+ if (!isProduction) {
781
+ const mapped = String(error?.stack || "").replace(/\bdist[\\/]/g, "app/");
782
+ res.end(`<!doctype html><html><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><title>FastScript Dev Error</title><style>body{background:#080808;color:#fff;font:14px/1.5 ui-monospace,Menlo,monospace;padding:20px}pre{background:#121212;padding:12px;border-radius:8px;overflow:auto}</style></head><body><h1>FastScript Dev Error</h1><p>Request ID: <code>${requestId}</code></p><pre>${mapped.replace(/</g, "&lt;")}</pre></body></html>`);
783
+ } else {
784
+ res.end(`<!doctype html><html><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><title>Error</title><style>body{background:#050505;color:#fff;font:16px/1.6 ui-sans-serif,system-ui;padding:40px}code{color:#9f92ff}</style></head><body><h1>Something went wrong</h1><p>Please retry or roll back to the previous deploy.</p><p>Request ID: <code>${requestId}</code></p></body></html>`);
785
+ }
786
+ }
787
+ responseStatus = status;
788
+ responseKind = "error";
789
+ responseError = payload.error.message;
790
+ logger.error("request_error", { requestId, status, path: req.url, error: payload.error.message });
791
+ span.end({ status, error: payload.error.message, kind: "error" });
792
+ } finally {
793
+ const durationMs = Date.now() - start;
794
+ metrics.inc("requests_total", 1);
795
+ metrics.inc(`requests_status_${responseStatus}`, 1);
796
+ metrics.inc(`requests_kind_${responseKind}`, 1);
797
+ if (responseError) metrics.inc("requests_error_total", 1);
798
+ metrics.observe("request_duration_ms", durationMs);
799
+ try {
800
+ await plugins.onRequestEnd({
801
+ ...(requestCtx || { requestId, pathname: req.url || "/", method: req.method || "GET", mode }),
802
+ status: responseStatus,
803
+ kind: responseKind,
804
+ error: responseError,
805
+ durationMs,
806
+ });
807
+ } catch (hookError) {
808
+ logger.warn("plugin_on_request_end_failed", { error: hookError?.message || String(hookError) });
809
+ }
810
+ }
811
+ });
812
+ const requestTimeoutMs = Number(process.env.REQUEST_TIMEOUT_MS || 15000);
813
+ if (Number.isFinite(requestTimeoutMs) && requestTimeoutMs > 0) {
814
+ server.requestTimeout = requestTimeoutMs;
815
+ }
816
+
817
+ if (listen) {
818
+ server.listen(port, () => {
819
+ logger.info("server_started", { mode, port, watchMode });
820
+ });
821
+ }
822
+
823
+ function cleanup() {
824
+ if (stopWatching) {
825
+ stopWatching();
826
+ stopWatching = null;
827
+ }
828
+ for (const client of hmrClients) {
829
+ try { client.end(); } catch {}
830
+ }
831
+ hmrClients.clear();
832
+ if (db && typeof db.flush === "function") {
833
+ Promise.resolve(db.flush()).catch(() => {});
834
+ }
835
+ if (db && typeof db.close === "function") {
836
+ Promise.resolve(db.close()).catch(() => {});
837
+ }
838
+ if (cache && typeof cache.close === "function") {
839
+ Promise.resolve(cache.close()).catch(() => {});
840
+ }
841
+ if (queue && typeof queue.close === "function") {
842
+ Promise.resolve(queue.close()).catch(() => {});
843
+ }
844
+ }
845
+ process.once("SIGINT", cleanup);
846
+ process.once("SIGTERM", cleanup);
847
+
848
+ return server;
849
+ }