bonescript-compiler 0.4.0 → 0.5.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.
package/src/emit_full.ts CHANGED
@@ -23,7 +23,7 @@ import {
23
23
  emitFailureRules,
24
24
  emitMigrationDiff,
25
25
  } from "./emit_maintenance";
26
- import { emitFlowRuntime } from "./emit_extras";
26
+ import { emitFlowRuntime, emitChannelFilters } from "./emit_extras";
27
27
  import { emitAlgorithmsFile, collectUsedAlgorithms } from "./emit_composition";
28
28
  import { emitExtensionPointStub } from "./extension_manager";
29
29
  import * as AST from "./ast";
@@ -39,6 +39,22 @@ function toSnakeCase(s: string): string {
39
39
  return s.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
40
40
  }
41
41
 
42
+ /** Resolve the auth method for the system from the resolution map or module configs. */
43
+ function resolveSystemAuthMethod(system: IR.IRSystem): "jwt" | "oauth2" | "apikey" {
44
+ const direct = system.resolution["implied.auth_method"] || system.resolution["system.auth_method"];
45
+ if (direct === "oauth2" || direct === "apikey" || direct === "jwt") return direct as "jwt" | "oauth2" | "apikey";
46
+ for (const [key, val] of Object.entries(system.resolution)) {
47
+ if (key.endsWith(".auth_method") && (val === "oauth2" || val === "apikey" || val === "jwt")) {
48
+ return val as "jwt" | "oauth2" | "apikey";
49
+ }
50
+ }
51
+ for (const mod of system.modules) {
52
+ const m = mod.config["auth_method"] as string | undefined;
53
+ if (m === "oauth2" || m === "apikey" || m === "jwt") return m;
54
+ }
55
+ return "jwt";
56
+ }
57
+
42
58
  export class FullEmitter {
43
59
  private schemaEmitter = new Emitter();
44
60
 
@@ -56,6 +72,33 @@ export class FullEmitter {
56
72
  files.push({ path: "src/events.ts", content: emitDurableEventBus(system), language: "typescript", source_module: "infra" });
57
73
  // Outbox SQL schema
58
74
  files.push({ path: "migrations/event_outbox.sql", content: emitOutboxSchema(), language: "sql", source_module: "infra" });
75
+
76
+ // API key table migration (only when auth_method = apikey)
77
+ const authMethod = resolveSystemAuthMethod(system);
78
+ if (authMethod === "apikey") {
79
+ files.push({
80
+ path: "migrations/api_keys.sql",
81
+ content: [
82
+ "-- Generated by BoneScript compiler. DO NOT EDIT.",
83
+ "-- API key table for apikey auth strategy.",
84
+ "",
85
+ "CREATE TABLE IF NOT EXISTS api_keys (",
86
+ " id UUID PRIMARY KEY DEFAULT gen_random_uuid(),",
87
+ " actor_id UUID NOT NULL,",
88
+ " key_hash VARCHAR(64) NOT NULL UNIQUE,",
89
+ " key_prefix VARCHAR(16) NOT NULL,",
90
+ " name VARCHAR(255) NOT NULL DEFAULT 'default',",
91
+ " created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),",
92
+ " expires_at TIMESTAMPTZ NOT NULL,",
93
+ " revoked BOOLEAN NOT NULL DEFAULT false",
94
+ ");",
95
+ "CREATE INDEX IF NOT EXISTS idx_api_keys_actor ON api_keys (actor_id);",
96
+ "CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys (key_hash);",
97
+ ].join("\n"),
98
+ language: "sql",
99
+ source_module: "infra",
100
+ });
101
+ }
59
102
  // Typed event publisher functions (one per declared event)
60
103
  if (system.events.length > 0) {
61
104
  files.push({ path: "src/publishers.ts", content: emitTypedEventPublishers(system), language: "typescript", source_module: "infra" });
@@ -111,17 +154,24 @@ export class FullEmitter {
111
154
  "",
112
155
  ];
113
156
  for (const ep of system.extension_points) {
114
- const params = ep.params.map((p: { name: string; type: string }) => `${p.name}: ${p.type}`).join(", ");
115
- const returnType = ep.returns || "void";
116
- extLines.push(`/**`);
117
- extLines.push(` * Extension point: ${ep.name}`);
118
- extLines.push(` * ${ep.stable ? "STABLE: implementation required." : "Optional."}`);
119
- extLines.push(` */`);
120
- extLines.push(`export function ${ep.name}(${params}): ${returnType} {`);
121
- extLines.push(` // <bonescript:ext:${ep.name}:begin>`);
122
- extLines.push(` throw new Error("Not implemented: ${ep.name}");`);
123
- extLines.push(` // <bonescript:ext:${ep.name}:end>`);
124
- extLines.push(`}`);
157
+ // Use the shared emitExtensionPointStub so the format is consistent
158
+ // with what extension_manager.ts expects when merging on recompile.
159
+ const stub = emitExtensionPointStub({
160
+ kind: "ExtensionPointDecl",
161
+ loc: { line: 0, column: 0, offset: 0 },
162
+ name: ep.name,
163
+ params: ep.params.map(p => ({
164
+ kind: "Param" as const,
165
+ loc: { line: 0, column: 0, offset: 0 },
166
+ name: p.name,
167
+ type: { kind: "PrimitiveType" as const, loc: { line: 0, column: 0, offset: 0 }, name: p.type },
168
+ })),
169
+ returns: ep.returns
170
+ ? { kind: "PrimitiveType" as const, loc: { line: 0, column: 0, offset: 0 }, name: ep.returns }
171
+ : null,
172
+ stable: ep.stable,
173
+ });
174
+ extLines.push(stub);
125
175
  extLines.push("");
126
176
  }
127
177
  files.push({
@@ -144,6 +194,79 @@ export class FullEmitter {
144
194
  }
145
195
  }
146
196
 
197
+ // 3a. Derived field helpers (one file per entity that has derived fields)
198
+ for (const mod of system.modules) {
199
+ for (const model of mod.models) {
200
+ const derivedFields = model.fields.filter(f =>
201
+ f.default_value && f.default_value.startsWith("GENERATED ALWAYS AS")
202
+ );
203
+ if (derivedFields.length === 0) continue;
204
+ const lines: string[] = [
205
+ `// Generated by BoneScript compiler. DO NOT EDIT.`,
206
+ `// Derived field helpers for ${model.name}.`,
207
+ `// These mirror the GENERATED ALWAYS AS columns in the SQL migration.`,
208
+ `// Use them when you need the computed value in application code before a DB round-trip.`,
209
+ ``,
210
+ `export type ${model.name}Derived = {`,
211
+ ];
212
+ for (const f of derivedFields) {
213
+ lines.push(` ${f.name}: unknown;`);
214
+ }
215
+ lines.push(`};`);
216
+ lines.push(``);
217
+ lines.push(`export const ${model.name.toUpperCase()}_DERIVED_FIELDS = [${derivedFields.map(f => `"${f.name}"`).join(", ")}] as const;`);
218
+ lines.push(``);
219
+ lines.push(`/** Returns true if the field name is a derived (computed) field on ${model.name}. */`);
220
+ lines.push(`export function is${model.name}DerivedField(field: string): boolean {`);
221
+ lines.push(` return (${model.name.toUpperCase()}_DERIVED_FIELDS as readonly string[]).includes(field);`);
222
+ lines.push(`}`);
223
+ files.push({
224
+ path: `src/derived/${toSnakeCase(model.name)}.ts`,
225
+ content: lines.join("\n"),
226
+ language: "typescript",
227
+ source_module: mod.id,
228
+ });
229
+ }
230
+ }
231
+
232
+ // 3b. Channel filter predicates (only if any channel has a filter expression)
233
+ const realtimeMods = system.modules.filter(m => m.kind === "realtime_service" && m.config["filter"]);
234
+ if (realtimeMods.length > 0) {
235
+ const filterLines: string[] = [
236
+ `// Generated by BoneScript compiler. DO NOT EDIT.`,
237
+ `// Channel filter predicates — applied before delivering messages to participants.`,
238
+ ``,
239
+ `export const CHANNEL_FILTERS: Record<string, (event: any, participant: any) => boolean> = {`,
240
+ ];
241
+ for (const mod of realtimeMods) {
242
+ const filterExpr = String(mod.config["filter"] || "true");
243
+ // Translate bone field refs to JS: event.field and participant.field pass through,
244
+ // bare identifiers are assumed to be event properties.
245
+ const jsFilter = filterExpr
246
+ .replace(/\band\b/g, "&&")
247
+ .replace(/\bor\b/g, "||")
248
+ .replace(/\bnot\b/g, "!")
249
+ .replace(/\b==\b/g, "===")
250
+ .replace(/\b!=\b/g, "!==")
251
+ .replace(/\bcontains\b/g, "?.includes");
252
+ filterLines.push(` "${mod.name}": (event, participant) => {`);
253
+ filterLines.push(` try { return Boolean(${jsFilter}); } catch { return true; }`);
254
+ filterLines.push(` },`);
255
+ }
256
+ filterLines.push(`};`);
257
+ filterLines.push(``);
258
+ filterLines.push(`export function shouldDeliver(channel: string, event: any, participant: any): boolean {`);
259
+ filterLines.push(` const filter = CHANNEL_FILTERS[channel];`);
260
+ filterLines.push(` return filter ? filter(event, participant) : true;`);
261
+ filterLines.push(`}`);
262
+ files.push({
263
+ path: "src/channel_filters.ts",
264
+ content: filterLines.join("\n"),
265
+ language: "typescript",
266
+ source_module: "infra",
267
+ });
268
+ }
269
+
147
270
  // 4. Source: route files (CRUD + capabilities)
148
271
  for (const mod of system.modules) {
149
272
  if (mod.kind === "api_service" && mod.models.length > 0) {
@@ -186,6 +309,7 @@ export class FullEmitter {
186
309
  }
187
310
 
188
311
  // 6. SQL migrations — run schema emitter ONCE, then match by model name
312
+ // Includes both api_service entities AND data_store schemas.
189
313
  const schemas: string[] = [];
190
314
  const allSchemaFiles = this.schemaEmitter.emit(system);
191
315
  for (const mod of system.modules) {
package/src/emit_index.ts CHANGED
@@ -1,161 +1,210 @@
1
- /**
2
- * BoneScript Server Entry Point Emitter
3
- * Generates src/index.ts — the Express server with all middleware and routes wired.
4
- */
5
-
6
- import * as IR from "./ir";
7
- import { toSnakeCase, toCamelCase } from "./emit_router";
8
-
9
- export function emitIndex(system: IR.IRSystem): string {
10
- const apiModules = system.modules.filter(m => m.kind === "api_service");
11
- const hasWebSocket = system.modules.some(m => m.kind === "realtime_service");
12
- const hasBatch = system.modules.some(m =>
13
- m.interfaces.some(i => i.methods.some(mth => mth.sync === "batch"))
14
- );
15
- const lines: string[] = [];
16
-
17
- lines.push(`// Generated by BoneScript compiler. DO NOT EDIT.`);
18
- lines.push(`// System: ${system.name}`);
19
- lines.push(`require("dotenv").config();`);
20
- lines.push(`import express from "express";`);
21
- lines.push(`import { createServer } from "http";`);
22
- lines.push(`import cors from "cors";`);
23
- lines.push(`import helmet from "helmet";`);
24
- lines.push(`import rateLimit from "express-rate-limit";`);
25
- lines.push(`import { authMiddleware } from "./auth";`);
26
- lines.push(`import { healthRouter } from "./health";`);
27
- lines.push(`import { logger } from "./logger";`);
28
- lines.push(`import { eventBus } from "./events";`);
29
- lines.push(`import { pool } from "./db";`);
30
- if (hasBatch) lines.push(`import { startBatchWorker } from "./batch";`);
31
- if (hasWebSocket) lines.push(`import { setupWebSocketServer } from "./websocket";`);
32
- lines.push(``);
33
-
34
- for (const mod of apiModules) {
35
- const model = mod.models[0];
36
- if (!model) continue;
37
- const routerName = toCamelCase(toSnakeCase(model.name) + "s") + "Router";
38
- lines.push(`import { ${routerName} } from "./routes/${toSnakeCase(model.name)}";`);
39
- }
40
-
41
- lines.push(``);
42
- lines.push(`const app = express();`);
43
- lines.push(`const httpServer = createServer(app);`);
44
- lines.push(`const PORT = parseInt(process.env.PORT || "3000");`);
45
- lines.push(``);
46
-
47
- // Middleware
48
- lines.push(`// Middleware`);
49
- lines.push(`app.use(helmet());`);
50
- lines.push(`// CORS: restrict to ALLOWED_ORIGINS env var (comma-separated). Defaults to same-origin only.`);
51
- lines.push(`const __allowedOrigins = (process.env.ALLOWED_ORIGINS || "").split(",").map(s => s.trim()).filter(Boolean);`);
52
- lines.push(`app.use(cors({`);
53
- lines.push(` origin: __allowedOrigins.length > 0`);
54
- lines.push(` ? (origin, cb) => { if (!origin || __allowedOrigins.includes(origin)) cb(null, true); else cb(new Error("Not allowed by CORS")); }`);
55
- lines.push(` : false,`);
56
- lines.push(` credentials: true,`);
57
- lines.push(`}));`);
58
- lines.push(`app.use(express.json({ limit: "1mb" }));`);
59
- lines.push(`app.use(express.urlencoded({ extended: false, limit: "1mb" }));`);
60
- lines.push(`app.use(authMiddleware);`);
61
- lines.push(``);
62
-
63
- // Rate limiting
64
- const gw = system.modules.find(m => m.kind === "gateway");
65
- const rateVal = gw?.config["rate_limit"] || 300;
66
- lines.push(`// Global rate limit (default ${rateVal} req/min, override with RATE_LIMIT_MAX env var)`);
67
- lines.push(`const __globalRateLimit = rateLimit({`);
68
- lines.push(` windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || "60000"),`);
69
- lines.push(` max: parseInt(process.env.RATE_LIMIT_MAX || "${rateVal}"),`);
70
- lines.push(` standardHeaders: true,`);
71
- lines.push(` legacyHeaders: false,`);
72
- lines.push(` message: { error: { code: "RATE_LIMITED", message: "Too many requests, please slow down." } },`);
73
- lines.push(`});`);
74
- lines.push(`app.use(__globalRateLimit);`);
75
- lines.push(``);
76
- lines.push(`// Strict rate limit on auth endpoints (20 req/min per IP)`);
77
- lines.push(`const __authRateLimit = rateLimit({`);
78
- lines.push(` windowMs: 60000,`);
79
- lines.push(` max: parseInt(process.env.AUTH_RATE_LIMIT_MAX || "20"),`);
80
- lines.push(` standardHeaders: true,`);
81
- lines.push(` legacyHeaders: false,`);
82
- lines.push(` message: { error: { code: "RATE_LIMITED", message: "Too many auth attempts." } },`);
83
- lines.push(`});`);
84
- lines.push(``);
85
-
86
- // Request timeout
87
- lines.push(`// Request timeout (default 30s, override per-route)`);
88
- lines.push(`app.use((req: any, res: any, next: any) => {`);
89
- lines.push(` const timeout = parseInt(process.env.REQUEST_TIMEOUT_MS || "30000");`);
90
- lines.push(` const timer = setTimeout(() => {`);
91
- lines.push(` if (!res.headersSent) {`);
92
- lines.push(` res.status(503).json({ error: { code: "REQUEST_TIMEOUT", message: "Request timed out" } });`);
93
- lines.push(` }`);
94
- lines.push(` }, timeout);`);
95
- lines.push(` res.on("finish", () => clearTimeout(timer));`);
96
- lines.push(` next();`);
97
- lines.push(`});`);
98
- lines.push(``);
99
-
100
- // Routes
101
- lines.push(`// Health & metrics`);
102
- lines.push(`app.use("/health", healthRouter);`);
103
- lines.push(``);
104
- lines.push(`// Routes`);
105
- for (const mod of apiModules) {
106
- const model = mod.models[0];
107
- if (!model) continue;
108
- const routerName = toCamelCase(toSnakeCase(model.name) + "s") + "Router";
109
- const routePath = `/${toSnakeCase(model.name)}s`;
110
- lines.push(`app.use("${routePath}", ${routerName});`);
111
- lines.push(`app.use("${routePath}/login", __authRateLimit);`);
112
- lines.push(`app.use("${routePath}/register", __authRateLimit);`);
113
- }
114
- lines.push(``);
115
-
116
- if (hasWebSocket) {
117
- lines.push(`// WebSocket`);
118
- lines.push(`setupWebSocketServer(httpServer);`);
119
- lines.push(``);
120
- }
121
-
122
- // Startup
123
- lines.push(`// Start`);
124
- lines.push(`httpServer.listen(PORT, () => {`);
125
- lines.push(` logger.info("server_started", { event: "startup", metadata: { port: PORT } });`);
126
- lines.push(` eventBus.startWorker(parseInt(process.env.EVENT_WORKER_INTERVAL_MS || "1000"));`);
127
- if (hasBatch) lines.push(` startBatchWorker();`);
128
- lines.push(` console.log(\`[${system.name}] Running on port \${PORT}\`);`);
129
- lines.push(` console.log(\` HTTP routes:\`);`);
130
- for (const mod of apiModules) {
131
- const model = mod.models[0];
132
- if (!model) continue;
133
- lines.push(` console.log(\` /${toSnakeCase(model.name)}s\`);`);
134
- }
135
- if (hasWebSocket) lines.push(` console.log(\` WebSocket: /ws?channel=<name>&token=<jwt>\`);`);
136
- lines.push(` console.log(\` Health: /health/live, /health/ready, /health/metrics\`);`);
137
- lines.push(`});`);
138
- lines.push(``);
139
-
140
- // Graceful shutdown
141
- lines.push(`// Graceful shutdown`);
142
- lines.push(`const shutdown = async (signal: string) => {`);
143
- lines.push(` logger.info("server_stopping", { event: "shutdown", metadata: { signal } });`);
144
- lines.push(` httpServer.close(async () => {`);
145
- lines.push(` try {`);
146
- lines.push(` await pool.end();`);
147
- lines.push(` logger.info("server_stopped", { event: "shutdown", status: "success" });`);
148
- lines.push(` } catch (e: any) {`);
149
- lines.push(` logger.error("shutdown_error", { event: "shutdown", metadata: { error: e.message } });`);
150
- lines.push(` }`);
151
- lines.push(` process.exit(0);`);
152
- lines.push(` });`);
153
- lines.push(` setTimeout(() => { logger.error("shutdown_timeout", { event: "shutdown" }); process.exit(1); }, 10000);`);
154
- lines.push(`};`);
155
- lines.push(`process.on("SIGTERM", () => shutdown("SIGTERM"));`);
156
- lines.push(`process.on("SIGINT", () => shutdown("SIGINT"));`);
157
- lines.push(``);
158
- lines.push(`export { app, httpServer };`);
159
-
160
- return lines.join("\n");
161
- }
1
+ /**
2
+ * BoneScript Server Entry Point Emitter
3
+ * Generates src/index.ts — the Express server with all middleware and routes wired.
4
+ */
5
+
6
+ import * as IR from "./ir";
7
+ import { toSnakeCase, toCamelCase } from "./emit_router";
8
+
9
+ export function emitIndex(system: IR.IRSystem): string {
10
+ const apiModules = system.modules.filter(m => m.kind === "api_service");
11
+ const hasWebSocket = system.modules.some(m => m.kind === "realtime_service");
12
+ const hasBatch = system.modules.some(m =>
13
+ m.interfaces.some(i => i.methods.some(mth => mth.sync === "batch"))
14
+ );
15
+
16
+ // Collect policy settings from the gateway config (populated by lowering from PolicyDecl)
17
+ const gw = system.modules.find(m => m.kind === "gateway");
18
+ const encryptionMode = (gw?.config["encryption"] as string | undefined) || "none";
19
+ const enforceHttps = encryptionMode === "in_transit" || encryptionMode === "both";
20
+
21
+ const lines: string[] = [];
22
+
23
+ lines.push(`// Generated by BoneScript compiler. DO NOT EDIT.`);
24
+ lines.push(`// System: ${system.name}`);
25
+ lines.push(`require("dotenv").config();`);
26
+ lines.push(`import express from "express";`);
27
+ lines.push(`import { createServer } from "http";`);
28
+ lines.push(`import cors from "cors";`);
29
+ lines.push(`import helmet from "helmet";`);
30
+ lines.push(`import rateLimit from "express-rate-limit";`);
31
+ lines.push(`import { authMiddleware } from "./auth";`);
32
+ lines.push(`import { healthRouter } from "./health";`);
33
+ lines.push(`import { logger } from "./logger";`);
34
+ lines.push(`import { eventBus } from "./events";`);
35
+ lines.push(`import { pool } from "./db";`);
36
+ if (hasBatch) lines.push(`import { startBatchWorker } from "./batch";`);
37
+ if (hasWebSocket) lines.push(`import { setupWebSocketServer } from "./websocket";`);
38
+
39
+ // Auth router import — must be at top-level with other imports
40
+ const authMethod = (() => {
41
+ const direct = system.resolution["implied.auth_method"] || system.resolution["system.auth_method"];
42
+ if (direct === "oauth2" || direct === "apikey" || direct === "jwt") return direct;
43
+ for (const [key, val] of Object.entries(system.resolution)) {
44
+ if (key.endsWith(".auth_method") && (val === "oauth2" || val === "apikey" || val === "jwt")) return val;
45
+ }
46
+ for (const mod of system.modules) {
47
+ const m = mod.config["auth_method"] as string | undefined;
48
+ if (m === "oauth2" || m === "apikey" || m === "jwt") return m;
49
+ }
50
+ return "jwt";
51
+ })();
52
+ if (authMethod === "oauth2" || authMethod === "apikey") {
53
+ lines.push(`import { authRouter } from "./auth";`);
54
+ }
55
+ lines.push(``);
56
+
57
+ for (const mod of apiModules) {
58
+ const model = mod.models[0];
59
+ if (!model) continue;
60
+ const routerName = toCamelCase(toSnakeCase(model.name) + "s") + "Router";
61
+ lines.push(`import { ${routerName} } from "./routes/${toSnakeCase(model.name)}";`);
62
+ }
63
+
64
+ lines.push(``);
65
+ lines.push(`const app = express();`);
66
+ lines.push(`const httpServer = createServer(app);`);
67
+ lines.push(`const PORT = parseInt(process.env.PORT || "3000");`);
68
+ lines.push(``);
69
+
70
+ // Middleware
71
+ lines.push(`// Middleware`);
72
+ lines.push(`app.use(helmet());`);
73
+
74
+ // HTTPS enforcement (in_transit or both encryption mode)
75
+ if (enforceHttps) {
76
+ lines.push(`// Encryption policy: in_transit redirect HTTP to HTTPS in production`);
77
+ lines.push(`app.use((req: any, res: any, next: any) => {`);
78
+ lines.push(` if (process.env.NODE_ENV === "production" && req.headers["x-forwarded-proto"] !== "https") {`);
79
+ lines.push(` return res.redirect(301, \`https://\${req.headers.host}\${req.url}\`);`);
80
+ lines.push(` }`);
81
+ lines.push(` next();`);
82
+ lines.push(`});`);
83
+ }
84
+
85
+ // At-rest encryption warning (at_rest or both)
86
+ if (encryptionMode === "at_rest" || encryptionMode === "both") {
87
+ lines.push(`// Encryption policy: at_rest ensure DATABASE_URL uses SSL and disk encryption is enabled`);
88
+ lines.push(`if (process.env.NODE_ENV === "production" && process.env.DATABASE_URL && !process.env.DATABASE_URL.includes("sslmode=require")) {`);
89
+ lines.push(` console.warn("[WARN] Encryption policy requires at-rest encryption. Add ?sslmode=require to DATABASE_URL.");`);
90
+ lines.push(`}`);
91
+ }
92
+ lines.push(`// CORS: restrict to ALLOWED_ORIGINS env var (comma-separated). Defaults to same-origin only.`);
93
+ lines.push(`const __allowedOrigins = (process.env.ALLOWED_ORIGINS || "").split(",").map(s => s.trim()).filter(Boolean);`);
94
+ lines.push(`app.use(cors({`);
95
+ lines.push(` origin: __allowedOrigins.length > 0`);
96
+ lines.push(` ? (origin, cb) => { if (!origin || __allowedOrigins.includes(origin)) cb(null, true); else cb(new Error("Not allowed by CORS")); }`);
97
+ lines.push(` : false,`);
98
+ lines.push(` credentials: true,`);
99
+ lines.push(`}));`);
100
+ lines.push(`app.use(express.json({ limit: "1mb" }));`);
101
+ lines.push(`app.use(express.urlencoded({ extended: false, limit: "1mb" }));`);
102
+ lines.push(`app.use(authMiddleware);`);
103
+ lines.push(``);
104
+
105
+ // Rate limiting
106
+ const rateVal = gw?.config["rate_limit"] || 300;
107
+ lines.push(`// Global rate limit (default ${rateVal} req/min, override with RATE_LIMIT_MAX env var)`);
108
+ lines.push(`const __globalRateLimit = rateLimit({`);
109
+ lines.push(` windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || "60000"),`);
110
+ lines.push(` max: parseInt(process.env.RATE_LIMIT_MAX || "${rateVal}"),`);
111
+ lines.push(` standardHeaders: true,`);
112
+ lines.push(` legacyHeaders: false,`);
113
+ lines.push(` message: { error: { code: "RATE_LIMITED", message: "Too many requests, please slow down." } },`);
114
+ lines.push(`});`);
115
+ lines.push(`app.use(__globalRateLimit);`);
116
+ lines.push(``);
117
+ lines.push(`// Strict rate limit on auth endpoints (20 req/min per IP)`);
118
+ lines.push(`const __authRateLimit = rateLimit({`);
119
+ lines.push(` windowMs: 60000,`);
120
+ lines.push(` max: parseInt(process.env.AUTH_RATE_LIMIT_MAX || "20"),`);
121
+ lines.push(` standardHeaders: true,`);
122
+ lines.push(` legacyHeaders: false,`);
123
+ lines.push(` message: { error: { code: "RATE_LIMITED", message: "Too many auth attempts." } },`);
124
+ lines.push(`});`);
125
+ lines.push(``);
126
+
127
+ // Request timeout
128
+ lines.push(`// Request timeout (default 30s, override per-route)`);
129
+ lines.push(`app.use((req: any, res: any, next: any) => {`);
130
+ lines.push(` const timeout = parseInt(process.env.REQUEST_TIMEOUT_MS || "30000");`);
131
+ lines.push(` const timer = setTimeout(() => {`);
132
+ lines.push(` if (!res.headersSent) {`);
133
+ lines.push(` res.status(503).json({ error: { code: "REQUEST_TIMEOUT", message: "Request timed out" } });`);
134
+ lines.push(` }`);
135
+ lines.push(` }, timeout);`);
136
+ lines.push(` res.on("finish", () => clearTimeout(timer));`);
137
+ lines.push(` next();`);
138
+ lines.push(`});`);
139
+ lines.push(``);
140
+
141
+ // Routes
142
+ lines.push(`// Health & metrics`);
143
+ lines.push(`app.use("/health", healthRouter);`);
144
+ lines.push(``);
145
+
146
+ // Auth routes for OAuth2 and API key strategies
147
+ if (authMethod === "oauth2" || authMethod === "apikey") {
148
+ lines.push(`// Auth routes (${authMethod})`);
149
+ lines.push(`app.use("/auth", __authRateLimit, authRouter);`);
150
+ lines.push(``);
151
+ }
152
+
153
+ lines.push(`// Routes`);
154
+ for (const mod of apiModules) {
155
+ const model = mod.models[0];
156
+ if (!model) continue;
157
+ const routerName = toCamelCase(toSnakeCase(model.name) + "s") + "Router";
158
+ const routePath = `/${toSnakeCase(model.name)}s`;
159
+ lines.push(`app.use("${routePath}", ${routerName});`);
160
+ lines.push(`app.use("${routePath}/login", __authRateLimit);`);
161
+ lines.push(`app.use("${routePath}/register", __authRateLimit);`);
162
+ }
163
+ lines.push(``);
164
+
165
+ if (hasWebSocket) {
166
+ lines.push(`// WebSocket`);
167
+ lines.push(`setupWebSocketServer(httpServer);`);
168
+ lines.push(``);
169
+ }
170
+
171
+ // Startup
172
+ lines.push(`// Start`);
173
+ lines.push(`httpServer.listen(PORT, () => {`);
174
+ lines.push(` logger.info("server_started", { event: "startup", metadata: { port: PORT } });`);
175
+ lines.push(` eventBus.startWorker(parseInt(process.env.EVENT_WORKER_INTERVAL_MS || "1000"));`);
176
+ if (hasBatch) lines.push(` startBatchWorker();`);
177
+ lines.push(` console.log(\`[${system.name}] Running on port \${PORT}\`);`);
178
+ lines.push(` console.log(\` HTTP routes:\`);`);
179
+ for (const mod of apiModules) {
180
+ const model = mod.models[0];
181
+ if (!model) continue;
182
+ lines.push(` console.log(\` /${toSnakeCase(model.name)}s\`);`);
183
+ }
184
+ if (hasWebSocket) lines.push(` console.log(\` WebSocket: /ws?channel=<name>&token=<jwt>\`);`);
185
+ lines.push(` console.log(\` Health: /health/live, /health/ready, /health/metrics\`);`);
186
+ lines.push(`});`);
187
+ lines.push(``);
188
+
189
+ // Graceful shutdown
190
+ lines.push(`// Graceful shutdown`);
191
+ lines.push(`const shutdown = async (signal: string) => {`);
192
+ lines.push(` logger.info("server_stopping", { event: "shutdown", metadata: { signal } });`);
193
+ lines.push(` httpServer.close(async () => {`);
194
+ lines.push(` try {`);
195
+ lines.push(` await pool.end();`);
196
+ lines.push(` logger.info("server_stopped", { event: "shutdown", status: "success" });`);
197
+ lines.push(` } catch (e: any) {`);
198
+ lines.push(` logger.error("shutdown_error", { event: "shutdown", metadata: { error: e.message } });`);
199
+ lines.push(` }`);
200
+ lines.push(` process.exit(0);`);
201
+ lines.push(` });`);
202
+ lines.push(` setTimeout(() => { logger.error("shutdown_timeout", { event: "shutdown" }); process.exit(1); }, 10000);`);
203
+ lines.push(`};`);
204
+ lines.push(`process.on("SIGTERM", () => shutdown("SIGTERM"));`);
205
+ lines.push(`process.on("SIGINT", () => shutdown("SIGINT"));`);
206
+ lines.push(``);
207
+ lines.push(`export { app, httpServer };`);
208
+
209
+ return lines.join("\n");
210
+ }