bonescript-compiler 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +382 -0
- package/dist/emit_auth.d.ts +14 -2
- package/dist/emit_auth.js +498 -60
- package/dist/emit_auth.js.map +1 -1
- package/dist/emit_full.js +133 -11
- package/dist/emit_full.js.map +1 -1
- package/dist/emit_index.js +46 -1
- package/dist/emit_index.js.map +1 -1
- package/dist/emitter.js +47 -0
- package/dist/emitter.js.map +1 -1
- package/dist/ir.d.ts +4 -0
- package/dist/lowering_channels.js +1 -0
- package/dist/lowering_channels.js.map +1 -1
- package/dist/lowering_entities.js +11 -1
- package/dist/lowering_entities.js.map +1 -1
- package/dist/optimizer.js +1 -1
- package/dist/optimizer.js.map +1 -1
- package/dist/scaffold.js +0 -1
- package/dist/scaffold.js.map +1 -1
- package/dist/typechecker.d.ts +5 -0
- package/dist/typechecker.js +36 -0
- package/dist/typechecker.js.map +1 -1
- package/package.json +1 -1
- package/src/emit_auth.ts +513 -67
- package/src/emit_full.ts +136 -12
- package/src/emit_index.ts +210 -161
- package/src/emitter.ts +642 -592
- package/src/ir.ts +1 -0
- package/src/lowering_channels.ts +1 -0
- package/src/lowering_entities.ts +258 -248
- package/src/optimizer.ts +1 -1
- package/src/scaffold.ts +0 -1
- package/src/typechecker.ts +657 -606
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
lines
|
|
22
|
-
|
|
23
|
-
lines.push(
|
|
24
|
-
lines.push(
|
|
25
|
-
lines.push(`
|
|
26
|
-
lines.push(`import
|
|
27
|
-
lines.push(`import {
|
|
28
|
-
lines.push(`import
|
|
29
|
-
lines.push(`import
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
lines.push(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
lines.push(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
lines.push(
|
|
67
|
-
lines.push(`const
|
|
68
|
-
lines.push(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
lines.push(`
|
|
72
|
-
lines.push(`
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
lines.push(
|
|
93
|
-
lines.push(`
|
|
94
|
-
lines.push(`
|
|
95
|
-
lines.push(`
|
|
96
|
-
lines.push(`
|
|
97
|
-
lines.push(`
|
|
98
|
-
lines.push(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
lines.push(
|
|
102
|
-
lines.push(`app.use(
|
|
103
|
-
lines.push(``);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
lines.push(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
lines.push(
|
|
124
|
-
lines.push(`
|
|
125
|
-
lines.push(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
lines.push(
|
|
129
|
-
lines.push(`
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (!
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
lines.push(`
|
|
137
|
-
lines.push(`
|
|
138
|
-
lines.push(
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
lines.push(
|
|
143
|
-
lines.push(`
|
|
144
|
-
lines.push(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
lines.push(`
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
+
}
|