bonescript-compiler 0.5.2 → 0.5.4

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 (187) hide show
  1. package/LICENSE +21 -21
  2. package/dist/algorithm_catalog.js +166 -166
  3. package/dist/cli.d.ts +1 -2
  4. package/dist/cli.js +543 -75
  5. package/dist/cli.js.map +1 -1
  6. package/dist/emit_capability.d.ts +0 -13
  7. package/dist/emit_capability.js +128 -292
  8. package/dist/emit_capability.js.map +1 -1
  9. package/dist/emit_composition.js +3 -37
  10. package/dist/emit_composition.js.map +1 -1
  11. package/dist/emit_deploy.js +162 -162
  12. package/dist/emit_events.d.ts +0 -1
  13. package/dist/emit_events.js +275 -342
  14. package/dist/emit_events.js.map +1 -1
  15. package/dist/emit_full.js +106 -268
  16. package/dist/emit_full.js.map +1 -1
  17. package/dist/emit_maintenance.js +249 -249
  18. package/dist/emit_runtime.d.ts +11 -17
  19. package/dist/emit_runtime.js +688 -29
  20. package/dist/emit_runtime.js.map +1 -1
  21. package/dist/emit_sourcemap.js +66 -66
  22. package/dist/emit_tests.js +0 -37
  23. package/dist/emit_tests.js.map +1 -1
  24. package/dist/emitter.js +16 -82
  25. package/dist/emitter.js.map +1 -1
  26. package/dist/extension_manager.d.ts +2 -2
  27. package/dist/extension_manager.js +3 -6
  28. package/dist/extension_manager.js.map +1 -1
  29. package/dist/ir.d.ts +0 -4
  30. package/dist/lowering.d.ts +14 -5
  31. package/dist/lowering.js +417 -66
  32. package/dist/lowering.js.map +1 -1
  33. package/dist/module_loader.d.ts +2 -2
  34. package/dist/module_loader.js +23 -20
  35. package/dist/module_loader.js.map +1 -1
  36. package/dist/optimizer.js +1 -1
  37. package/dist/optimizer.js.map +1 -1
  38. package/dist/scaffold.d.ts +2 -2
  39. package/dist/scaffold.js +319 -315
  40. package/dist/scaffold.js.map +1 -1
  41. package/dist/source_map.js.map +1 -0
  42. package/dist/test.js.map +1 -0
  43. package/dist/test_typechecker.d.ts +5 -0
  44. package/dist/test_typechecker.js +126 -0
  45. package/dist/test_typechecker.js.map +1 -0
  46. package/dist/typechecker.d.ts +0 -5
  47. package/dist/typechecker.js +13 -68
  48. package/dist/typechecker.js.map +1 -1
  49. package/dist/verifier.d.ts +1 -5
  50. package/dist/verifier.js +35 -140
  51. package/dist/verifier.js.map +1 -1
  52. package/package.json +52 -62
  53. package/src/algorithm_catalog.ts +345 -345
  54. package/src/ast.d.ts +244 -0
  55. package/src/ast.ts +334 -334
  56. package/src/cli.ts +624 -98
  57. package/src/emit_batch.ts +140 -140
  58. package/src/emit_capability.ts +436 -617
  59. package/src/emit_composition.ts +196 -229
  60. package/src/emit_deploy.ts +190 -190
  61. package/src/emit_events.ts +307 -377
  62. package/src/emit_extras.ts +240 -240
  63. package/src/emit_full.ts +309 -475
  64. package/src/emit_maintenance.ts +459 -459
  65. package/src/emit_runtime.ts +730 -17
  66. package/src/emit_sourcemap.ts +140 -140
  67. package/src/emit_tests.ts +205 -246
  68. package/src/emit_websocket.ts +229 -229
  69. package/src/emitter.ts +578 -642
  70. package/src/extension_manager.ts +187 -189
  71. package/src/formatter.ts +297 -297
  72. package/src/index.ts +88 -88
  73. package/src/ir.ts +215 -216
  74. package/src/lexer.d.ts +195 -0
  75. package/src/lexer.ts +630 -630
  76. package/src/lowering.ts +556 -168
  77. package/src/module_loader.ts +114 -112
  78. package/src/optimizer.ts +196 -196
  79. package/src/parse_decls.d.ts +13 -0
  80. package/src/parse_decls.ts +409 -409
  81. package/src/parse_decls2.d.ts +13 -0
  82. package/src/parse_decls2.ts +244 -244
  83. package/src/parse_expr.d.ts +7 -0
  84. package/src/parse_expr.ts +197 -197
  85. package/src/parse_types.d.ts +6 -0
  86. package/src/parse_types.ts +54 -54
  87. package/src/parser.d.ts +10 -0
  88. package/src/parser.ts +1 -1
  89. package/src/parser_base.d.ts +19 -0
  90. package/src/parser_base.ts +57 -57
  91. package/src/parser_recovery.ts +153 -153
  92. package/src/scaffold.ts +375 -371
  93. package/src/solver.ts +330 -330
  94. package/src/typechecker.d.ts +52 -0
  95. package/src/typechecker.ts +591 -657
  96. package/src/types.d.ts +38 -0
  97. package/src/types.ts +122 -122
  98. package/src/verifier.ts +46 -152
  99. package/README.md +0 -382
  100. package/dist/commands/check.d.ts +0 -5
  101. package/dist/commands/check.js +0 -34
  102. package/dist/commands/check.js.map +0 -1
  103. package/dist/commands/compile.d.ts +0 -5
  104. package/dist/commands/compile.js +0 -215
  105. package/dist/commands/compile.js.map +0 -1
  106. package/dist/commands/debug.d.ts +0 -5
  107. package/dist/commands/debug.js +0 -59
  108. package/dist/commands/debug.js.map +0 -1
  109. package/dist/commands/diff.d.ts +0 -5
  110. package/dist/commands/diff.js +0 -125
  111. package/dist/commands/diff.js.map +0 -1
  112. package/dist/commands/fmt.d.ts +0 -5
  113. package/dist/commands/fmt.js +0 -49
  114. package/dist/commands/fmt.js.map +0 -1
  115. package/dist/commands/init.d.ts +0 -5
  116. package/dist/commands/init.js +0 -96
  117. package/dist/commands/init.js.map +0 -1
  118. package/dist/commands/ir.d.ts +0 -5
  119. package/dist/commands/ir.js +0 -27
  120. package/dist/commands/ir.js.map +0 -1
  121. package/dist/commands/lex.d.ts +0 -5
  122. package/dist/commands/lex.js +0 -21
  123. package/dist/commands/lex.js.map +0 -1
  124. package/dist/commands/parse.d.ts +0 -5
  125. package/dist/commands/parse.js +0 -30
  126. package/dist/commands/parse.js.map +0 -1
  127. package/dist/commands/test.d.ts +0 -5
  128. package/dist/commands/test.js +0 -61
  129. package/dist/commands/test.js.map +0 -1
  130. package/dist/commands/verify_determinism.d.ts +0 -5
  131. package/dist/commands/verify_determinism.js +0 -64
  132. package/dist/commands/verify_determinism.js.map +0 -1
  133. package/dist/commands/watch.d.ts +0 -5
  134. package/dist/commands/watch.js +0 -50
  135. package/dist/commands/watch.js.map +0 -1
  136. package/dist/emit_auth.d.ts +0 -18
  137. package/dist/emit_auth.js +0 -507
  138. package/dist/emit_auth.js.map +0 -1
  139. package/dist/emit_database.d.ts +0 -7
  140. package/dist/emit_database.js +0 -74
  141. package/dist/emit_database.js.map +0 -1
  142. package/dist/emit_index.d.ts +0 -6
  143. package/dist/emit_index.js +0 -202
  144. package/dist/emit_index.js.map +0 -1
  145. package/dist/emit_models.d.ts +0 -12
  146. package/dist/emit_models.js +0 -171
  147. package/dist/emit_models.js.map +0 -1
  148. package/dist/emit_openapi.d.ts +0 -9
  149. package/dist/emit_openapi.js +0 -308
  150. package/dist/emit_openapi.js.map +0 -1
  151. package/dist/emit_package.d.ts +0 -7
  152. package/dist/emit_package.js +0 -70
  153. package/dist/emit_package.js.map +0 -1
  154. package/dist/emit_router.d.ts +0 -12
  155. package/dist/emit_router.js +0 -390
  156. package/dist/emit_router.js.map +0 -1
  157. package/dist/lowering_channels.d.ts +0 -11
  158. package/dist/lowering_channels.js +0 -103
  159. package/dist/lowering_channels.js.map +0 -1
  160. package/dist/lowering_entities.d.ts +0 -11
  161. package/dist/lowering_entities.js +0 -232
  162. package/dist/lowering_entities.js.map +0 -1
  163. package/dist/lowering_helpers.d.ts +0 -13
  164. package/dist/lowering_helpers.js +0 -76
  165. package/dist/lowering_helpers.js.map +0 -1
  166. package/src/commands/check.ts +0 -33
  167. package/src/commands/compile.ts +0 -191
  168. package/src/commands/debug.ts +0 -33
  169. package/src/commands/diff.ts +0 -108
  170. package/src/commands/fmt.ts +0 -22
  171. package/src/commands/init.ts +0 -72
  172. package/src/commands/ir.ts +0 -23
  173. package/src/commands/lex.ts +0 -17
  174. package/src/commands/parse.ts +0 -24
  175. package/src/commands/test.ts +0 -36
  176. package/src/commands/verify_determinism.ts +0 -66
  177. package/src/commands/watch.ts +0 -25
  178. package/src/emit_auth.ts +0 -513
  179. package/src/emit_database.ts +0 -75
  180. package/src/emit_index.ts +0 -210
  181. package/src/emit_models.ts +0 -176
  182. package/src/emit_openapi.ts +0 -318
  183. package/src/emit_package.ts +0 -69
  184. package/src/emit_router.ts +0 -409
  185. package/src/lowering_channels.ts +0 -108
  186. package/src/lowering_entities.ts +0 -258
  187. package/src/lowering_helpers.ts +0 -75
package/src/emit_auth.ts DELETED
@@ -1,513 +0,0 @@
1
- /**
2
- * BoneScript Auth Emitter
3
- * Generates auth.ts — JWT, OAuth2, or API key middleware depending on the
4
- * resolved auth_method in the system's constraint resolution map.
5
- *
6
- * Auth method selection (in priority order):
7
- * 1. system.resolution["implied.auth_method"] — set by constraint solver
8
- * 2. Any api_service module's config.auth_method
9
- * 3. Fallback: jwt
10
- *
11
- * Generated strategies:
12
- * jwt — Bearer token verification via jsonwebtoken
13
- * oauth2 — Authorization Code flow: /auth/login redirect + /auth/callback
14
- * token exchange + refresh token rotation
15
- * apikey — X-API-Key header lookup against hashed keys in the database
16
- */
17
-
18
- import * as IR from "./ir";
19
-
20
- /** Resolve the auth method for the system. */
21
- function resolveAuthMethod(system: IR.IRSystem): "jwt" | "oauth2" | "apikey" {
22
- // Check resolution map first (set by constraint solver)
23
- // The solver writes keys like "UserService.auth_method" for each api_service module.
24
- // Also check the implied.auth_method key (set by domain defaults propagation).
25
- const resolved = system.resolution["implied.auth_method"]
26
- || system.resolution["system.auth_method"];
27
- if (resolved === "oauth2" || resolved === "apikey" || resolved === "jwt") {
28
- return resolved;
29
- }
30
-
31
- // Check per-module resolution keys written by the solver
32
- for (const [key, val] of Object.entries(system.resolution)) {
33
- if (key.endsWith(".auth_method") && (val === "oauth2" || val === "apikey" || val === "jwt")) {
34
- return val as "jwt" | "oauth2" | "apikey";
35
- }
36
- }
37
-
38
- // Fall back to any api_service module config
39
- for (const mod of system.modules) {
40
- const m = mod.config["auth_method"] as string | undefined;
41
- if (m === "oauth2" || m === "apikey" || m === "jwt") return m;
42
- }
43
- return "jwt";
44
- }
45
-
46
- export function emitAuthMiddleware(system: IR.IRSystem): string {
47
- const method = resolveAuthMethod(system);
48
- switch (method) {
49
- case "oauth2": return emitOAuth2Auth();
50
- case "apikey": return emitApiKeyAuth();
51
- default: return emitJwtAuth();
52
- }
53
- }
54
-
55
- // ─── JWT ──────────────────────────────────────────────────────────────────────
56
-
57
- function emitJwtAuth(): string {
58
- return `// Generated by BoneScript compiler. DO NOT EDIT.
59
- // Auth strategy: JWT (Bearer token)
60
- import { Request, Response, NextFunction } from "express";
61
- import jwt from "jsonwebtoken";
62
-
63
- // JWT_SECRET must be set in production. The server will refuse to start without it
64
- // when NODE_ENV is "production" to prevent accidental use of a weak fallback.
65
- const JWT_SECRET = (() => {
66
- const secret = process.env.JWT_SECRET;
67
- if (!secret) {
68
- if (process.env.NODE_ENV === "production") {
69
- console.error("[FATAL] JWT_SECRET environment variable is not set. Refusing to start in production.");
70
- process.exit(1);
71
- }
72
- console.warn("[WARN] JWT_SECRET is not set. Using insecure default — do not use in production.");
73
- return "bonescript-dev-secret-do-not-use-in-production";
74
- }
75
- if (secret.length < 32) {
76
- console.warn("[WARN] JWT_SECRET is shorter than 32 characters. Use a longer secret in production.");
77
- }
78
- return secret;
79
- })();
80
-
81
- export interface AuthContext {
82
- authenticated: boolean;
83
- actor_id: string | null;
84
- trace_id: string;
85
- }
86
-
87
- export function authMiddleware(req: Request, res: Response, next: NextFunction): void {
88
- const header = req.headers.authorization;
89
- if (!header || !header.startsWith("Bearer ")) {
90
- (req as any).auth = { authenticated: false, actor_id: null, trace_id: req.headers["x-trace-id"] as string || "" };
91
- next();
92
- return;
93
- }
94
- try {
95
- const token = header.slice(7);
96
- const decoded = jwt.verify(token, JWT_SECRET) as { sub: string };
97
- (req as any).auth = {
98
- authenticated: true,
99
- actor_id: decoded.sub,
100
- trace_id: req.headers["x-trace-id"] as string || "",
101
- };
102
- } catch {
103
- (req as any).auth = { authenticated: false, actor_id: null, trace_id: req.headers["x-trace-id"] as string || "" };
104
- }
105
- next();
106
- }
107
-
108
- export function requireAuth(req: Request, res: Response, next: NextFunction): void {
109
- const auth: AuthContext = (req as any).auth;
110
- if (!auth || !auth.authenticated) {
111
- res.status(401).json({ error: { code: "UNAUTHORIZED", message: "Authentication required" } });
112
- return;
113
- }
114
- next();
115
- }
116
-
117
- /** Issue a signed JWT for a given actor. Used by login/register routes. */
118
- export function issueToken(actorId: string, expiresIn: string = "24h"): string {
119
- return jwt.sign({ sub: actorId }, JWT_SECRET, { expiresIn } as any);
120
- }
121
- `;
122
- }
123
-
124
- // ─── OAuth2 ───────────────────────────────────────────────────────────────────
125
-
126
- function emitOAuth2Auth(): string {
127
- return `// Generated by BoneScript compiler. DO NOT EDIT.
128
- // Auth strategy: OAuth2 Authorization Code Flow with PKCE + refresh token rotation
129
- import { Request, Response, NextFunction, Router } from "express";
130
- import crypto from "crypto";
131
- import jwt from "jsonwebtoken";
132
-
133
- // ─── Configuration ────────────────────────────────────────────────────────────
134
-
135
- const OAUTH2_CLIENT_ID = process.env.OAUTH2_CLIENT_ID || "";
136
- const OAUTH2_CLIENT_SECRET = process.env.OAUTH2_CLIENT_SECRET || "";
137
- const OAUTH2_AUTH_URL = process.env.OAUTH2_AUTH_URL || "";
138
- const OAUTH2_TOKEN_URL = process.env.OAUTH2_TOKEN_URL || "";
139
- const OAUTH2_REDIRECT_URI = process.env.OAUTH2_REDIRECT_URI || "http://localhost:3000/auth/callback";
140
- const OAUTH2_SCOPES = (process.env.OAUTH2_SCOPES || "openid profile email").split(" ");
141
-
142
- const JWT_SECRET = (() => {
143
- const secret = process.env.JWT_SECRET;
144
- if (!secret) {
145
- if (process.env.NODE_ENV === "production") {
146
- console.error("[FATAL] JWT_SECRET environment variable is not set. Refusing to start in production.");
147
- process.exit(1);
148
- }
149
- console.warn("[WARN] JWT_SECRET is not set. Using insecure default — do not use in production.");
150
- return "bonescript-dev-secret-do-not-use-in-production";
151
- }
152
- return secret;
153
- })();
154
-
155
- if (process.env.NODE_ENV === "production") {
156
- if (!OAUTH2_CLIENT_ID || !OAUTH2_CLIENT_SECRET || !OAUTH2_AUTH_URL || !OAUTH2_TOKEN_URL) {
157
- console.error("[FATAL] OAuth2 configuration incomplete. Set OAUTH2_CLIENT_ID, OAUTH2_CLIENT_SECRET, OAUTH2_AUTH_URL, OAUTH2_TOKEN_URL.");
158
- process.exit(1);
159
- }
160
- }
161
-
162
- // ─── PKCE helpers ─────────────────────────────────────────────────────────────
163
-
164
- function generateCodeVerifier(): string {
165
- return crypto.randomBytes(32).toString("base64url");
166
- }
167
-
168
- function generateCodeChallenge(verifier: string): string {
169
- return crypto.createHash("sha256").update(verifier).digest("base64url");
170
- }
171
-
172
- // ─── In-memory state store (replace with Redis in production) ─────────────────
173
-
174
- const pendingStates = new Map<string, { verifier: string; createdAt: number }>();
175
-
176
- // Clean up stale states every 5 minutes
177
- setInterval(() => {
178
- const cutoff = Date.now() - 10 * 60 * 1000; // 10 min TTL
179
- for (const [k, v] of pendingStates) {
180
- if (v.createdAt < cutoff) pendingStates.delete(k);
181
- }
182
- }, 5 * 60 * 1000);
183
-
184
- // ─── Auth context ─────────────────────────────────────────────────────────────
185
-
186
- export interface AuthContext {
187
- authenticated: boolean;
188
- actor_id: string | null;
189
- trace_id: string;
190
- }
191
-
192
- // ─── Middleware ───────────────────────────────────────────────────────────────
193
-
194
- export function authMiddleware(req: Request, res: Response, next: NextFunction): void {
195
- const header = req.headers.authorization;
196
- if (!header || !header.startsWith("Bearer ")) {
197
- (req as any).auth = { authenticated: false, actor_id: null, trace_id: req.headers["x-trace-id"] as string || "" };
198
- next();
199
- return;
200
- }
201
- try {
202
- const token = header.slice(7);
203
- const decoded = jwt.verify(token, JWT_SECRET) as { sub: string };
204
- (req as any).auth = {
205
- authenticated: true,
206
- actor_id: decoded.sub,
207
- trace_id: req.headers["x-trace-id"] as string || "",
208
- };
209
- } catch {
210
- (req as any).auth = { authenticated: false, actor_id: null, trace_id: req.headers["x-trace-id"] as string || "" };
211
- }
212
- next();
213
- }
214
-
215
- export function requireAuth(req: Request, res: Response, next: NextFunction): void {
216
- const auth: AuthContext = (req as any).auth;
217
- if (!auth || !auth.authenticated) {
218
- res.status(401).json({ error: { code: "UNAUTHORIZED", message: "Authentication required" } });
219
- return;
220
- }
221
- next();
222
- }
223
-
224
- // ─── OAuth2 Routes ────────────────────────────────────────────────────────────
225
-
226
- export const authRouter = Router();
227
-
228
- /** GET /auth/login — redirect to OAuth2 provider */
229
- authRouter.get("/login", (_req: Request, res: Response) => {
230
- const state = crypto.randomBytes(16).toString("hex");
231
- const verifier = generateCodeVerifier();
232
- const challenge = generateCodeChallenge(verifier);
233
- pendingStates.set(state, { verifier, createdAt: Date.now() });
234
-
235
- const params = new URLSearchParams({
236
- response_type: "code",
237
- client_id: OAUTH2_CLIENT_ID,
238
- redirect_uri: OAUTH2_REDIRECT_URI,
239
- scope: OAUTH2_SCOPES.join(" "),
240
- state,
241
- code_challenge: challenge,
242
- code_challenge_method: "S256",
243
- });
244
- res.redirect(\`\${OAUTH2_AUTH_URL}?\${params}\`);
245
- });
246
-
247
- /** GET /auth/callback — exchange code for tokens, issue internal JWT */
248
- authRouter.get("/callback", async (req: Request, res: Response) => {
249
- const { code, state, error } = req.query as Record<string, string>;
250
-
251
- if (error) {
252
- return res.status(400).json({ error: { code: "OAUTH2_ERROR", message: error } });
253
- }
254
- if (!code || !state) {
255
- return res.status(400).json({ error: { code: "MISSING_PARAMS", message: "code and state are required" } });
256
- }
257
-
258
- const pending = pendingStates.get(state);
259
- if (!pending) {
260
- return res.status(400).json({ error: { code: "INVALID_STATE", message: "Unknown or expired state parameter" } });
261
- }
262
- pendingStates.delete(state);
263
-
264
- try {
265
- // Exchange authorization code for tokens
266
- const tokenRes = await fetch(OAUTH2_TOKEN_URL, {
267
- method: "POST",
268
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
269
- body: new URLSearchParams({
270
- grant_type: "authorization_code",
271
- code,
272
- redirect_uri: OAUTH2_REDIRECT_URI,
273
- client_id: OAUTH2_CLIENT_ID,
274
- client_secret: OAUTH2_CLIENT_SECRET,
275
- code_verifier: pending.verifier,
276
- }),
277
- });
278
-
279
- if (!tokenRes.ok) {
280
- const body = await tokenRes.text();
281
- console.error("[OAuth2] Token exchange failed:", body);
282
- return res.status(502).json({ error: { code: "TOKEN_EXCHANGE_FAILED", message: "Failed to exchange authorization code" } });
283
- }
284
-
285
- const tokens = await tokenRes.json() as {
286
- access_token: string;
287
- id_token?: string;
288
- refresh_token?: string;
289
- expires_in?: number;
290
- };
291
-
292
- // Extract subject from id_token or access_token
293
- let actorId: string;
294
- try {
295
- const payload = JSON.parse(Buffer.from((tokens.id_token || tokens.access_token).split(".")[1], "base64url").toString());
296
- actorId = payload.sub || payload.email || payload.preferred_username || "unknown";
297
- } catch {
298
- actorId = "unknown";
299
- }
300
-
301
- // Issue internal JWT (short-lived)
302
- const internalToken = jwt.sign({ sub: actorId }, JWT_SECRET, { expiresIn: "1h" } as any);
303
-
304
- res.json({
305
- token: internalToken,
306
- actor_id: actorId,
307
- expires_in: 3600,
308
- });
309
- } catch (err: any) {
310
- console.error("[OAuth2] Callback error:", err.message);
311
- res.status(500).json({ error: { code: "INTERNAL_ERROR", message: "OAuth2 callback failed" } });
312
- }
313
- });
314
-
315
- /** POST /auth/refresh — refresh an expired internal JWT */
316
- authRouter.post("/refresh", (req: Request, res: Response) => {
317
- const { token } = req.body as { token?: string };
318
- if (!token) {
319
- return res.status(400).json({ error: { code: "MISSING_TOKEN", message: "token is required" } });
320
- }
321
- try {
322
- // Allow expired tokens for refresh (ignoreExpiration)
323
- const decoded = jwt.verify(token, JWT_SECRET, { ignoreExpiration: true }) as { sub: string };
324
- const newToken = jwt.sign({ sub: decoded.sub }, JWT_SECRET, { expiresIn: "1h" } as any);
325
- res.json({ token: newToken, expires_in: 3600 });
326
- } catch {
327
- res.status(401).json({ error: { code: "INVALID_TOKEN", message: "Cannot refresh: token is invalid" } });
328
- }
329
- });
330
-
331
- /** POST /auth/logout — client-side only (stateless JWT); invalidation requires a denylist */
332
- authRouter.post("/logout", (_req: Request, res: Response) => {
333
- res.json({ ok: true, message: "Logged out. Discard the token on the client." });
334
- });
335
- `;
336
- }
337
-
338
- // ─── API Key ──────────────────────────────────────────────────────────────────
339
-
340
- function emitApiKeyAuth(): string {
341
- return `// Generated by BoneScript compiler. DO NOT EDIT.
342
- // Auth strategy: API Key (X-API-Key header, hashed keys stored in database)
343
- import { Request, Response, NextFunction, Router } from "express";
344
- import crypto from "crypto";
345
- import { query } from "./db";
346
-
347
- // ─── Auth context ─────────────────────────────────────────────────────────────
348
-
349
- export interface AuthContext {
350
- authenticated: boolean;
351
- actor_id: string | null;
352
- trace_id: string;
353
- }
354
-
355
- // ─── Key hashing ──────────────────────────────────────────────────────────────
356
-
357
- /** Hash an API key with SHA-256 for safe storage. Never store raw keys. */
358
- function hashApiKey(key: string): string {
359
- return crypto.createHash("sha256").update(key).digest("hex");
360
- }
361
-
362
- /** Generate a new API key. Returns the raw key (shown once) and its hash. */
363
- export function generateApiKey(): { key: string; hash: string; prefix: string } {
364
- const raw = "bsk_" + crypto.randomBytes(32).toString("base64url");
365
- return { key: raw, hash: hashApiKey(raw), prefix: raw.slice(0, 12) };
366
- }
367
-
368
- // ─── In-memory LRU cache (avoids a DB hit on every request) ──────────────────
369
-
370
- const KEY_CACHE_TTL_MS = 60_000; // 1 minute
371
- const keyCache = new Map<string, { actorId: string; expiresAt: number }>();
372
-
373
- function getCached(hash: string): string | null {
374
- const entry = keyCache.get(hash);
375
- if (!entry) return null;
376
- if (Date.now() > entry.expiresAt) { keyCache.delete(hash); return null; }
377
- return entry.actorId;
378
- }
379
-
380
- function setCached(hash: string, actorId: string): void {
381
- // Evict oldest entry if cache is too large
382
- if (keyCache.size >= 1000) {
383
- const oldest = [...keyCache.entries()].sort((a, b) => a[1].expiresAt - b[1].expiresAt)[0];
384
- if (oldest) keyCache.delete(oldest[0]);
385
- }
386
- keyCache.set(hash, { actorId, expiresAt: Date.now() + KEY_CACHE_TTL_MS });
387
- }
388
-
389
- // ─── Middleware ───────────────────────────────────────────────────────────────
390
-
391
- export async function authMiddleware(req: Request, res: Response, next: NextFunction): Promise<void> {
392
- const rawKey = req.headers["x-api-key"] as string | undefined;
393
- const traceId = req.headers["x-trace-id"] as string || "";
394
-
395
- if (!rawKey) {
396
- (req as any).auth = { authenticated: false, actor_id: null, trace_id: traceId };
397
- next();
398
- return;
399
- }
400
-
401
- const hash = hashApiKey(rawKey);
402
-
403
- // Check cache first
404
- const cached = getCached(hash);
405
- if (cached) {
406
- (req as any).auth = { authenticated: true, actor_id: cached, trace_id: traceId };
407
- next();
408
- return;
409
- }
410
-
411
- try {
412
- // Look up in database — api_keys table (see migration below)
413
- const rows = await query<{ actor_id: string; revoked: boolean }>(
414
- \`SELECT actor_id, revoked FROM api_keys WHERE key_hash = $1 AND expires_at > NOW()\`,
415
- [hash]
416
- );
417
- if (rows.length === 0 || rows[0].revoked) {
418
- (req as any).auth = { authenticated: false, actor_id: null, trace_id: traceId };
419
- next();
420
- return;
421
- }
422
- const actorId = rows[0].actor_id;
423
- setCached(hash, actorId);
424
- (req as any).auth = { authenticated: true, actor_id: actorId, trace_id: traceId };
425
- } catch (err: any) {
426
- console.error("[ApiKey] Auth lookup failed:", err.message);
427
- (req as any).auth = { authenticated: false, actor_id: null, trace_id: traceId };
428
- }
429
- next();
430
- }
431
-
432
- export function requireAuth(req: Request, res: Response, next: NextFunction): void {
433
- const auth: AuthContext = (req as any).auth;
434
- if (!auth || !auth.authenticated) {
435
- res.status(401).json({ error: { code: "UNAUTHORIZED", message: "Valid API key required (X-API-Key header)" } });
436
- return;
437
- }
438
- next();
439
- }
440
-
441
- // ─── Key Management Routes ────────────────────────────────────────────────────
442
-
443
- export const authRouter = Router();
444
-
445
- /** POST /auth/keys — create a new API key for the authenticated actor */
446
- authRouter.post("/keys", requireAuth, async (req: Request, res: Response) => {
447
- const auth: AuthContext = (req as any).auth;
448
- const { name, expires_in_days = 365 } = req.body as { name?: string; expires_in_days?: number };
449
-
450
- const { key, hash, prefix } = generateApiKey();
451
- const expiresAt = new Date(Date.now() + expires_in_days * 86_400_000);
452
-
453
- try {
454
- await query(
455
- \`INSERT INTO api_keys (actor_id, key_hash, key_prefix, name, expires_at) VALUES ($1, $2, $3, $4, $5)\`,
456
- [auth.actor_id, hash, prefix, name || "default", expiresAt]
457
- );
458
- // Return the raw key ONCE — it cannot be retrieved again
459
- res.status(201).json({ key, prefix, expires_at: expiresAt });
460
- } catch (err: any) {
461
- res.status(500).json({ error: { code: "KEY_CREATE_FAILED", message: err.message } });
462
- }
463
- });
464
-
465
- /** GET /auth/keys — list API keys for the authenticated actor (no raw keys) */
466
- authRouter.get("/keys", requireAuth, async (req: Request, res: Response) => {
467
- const auth: AuthContext = (req as any).auth;
468
- try {
469
- const rows = await query(
470
- \`SELECT id, key_prefix, name, created_at, expires_at, revoked FROM api_keys WHERE actor_id = $1 ORDER BY created_at DESC\`,
471
- [auth.actor_id]
472
- );
473
- res.json({ keys: rows });
474
- } catch (err: any) {
475
- res.status(500).json({ error: { code: "KEY_LIST_FAILED", message: err.message } });
476
- }
477
- });
478
-
479
- /** DELETE /auth/keys/:id — revoke an API key */
480
- authRouter.delete("/keys/:id", requireAuth, async (req: Request, res: Response) => {
481
- const auth: AuthContext = (req as any).auth;
482
- try {
483
- const result = await query(
484
- \`UPDATE api_keys SET revoked = true WHERE id = $1 AND actor_id = $2 RETURNING id\`,
485
- [req.params.id, auth.actor_id]
486
- );
487
- if (result.length === 0) {
488
- return res.status(404).json({ error: { code: "NOT_FOUND", message: "API key not found" } });
489
- }
490
- res.json({ ok: true });
491
- } catch (err: any) {
492
- res.status(500).json({ error: { code: "KEY_REVOKE_FAILED", message: err.message } });
493
- }
494
- });
495
-
496
- /*
497
- * Required migration — add to your migrations directory:
498
- *
499
- * CREATE TABLE IF NOT EXISTS api_keys (
500
- * id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
501
- * actor_id UUID NOT NULL,
502
- * key_hash VARCHAR(64) NOT NULL UNIQUE,
503
- * key_prefix VARCHAR(16) NOT NULL,
504
- * name VARCHAR(255) NOT NULL DEFAULT 'default',
505
- * created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
506
- * expires_at TIMESTAMPTZ NOT NULL,
507
- * revoked BOOLEAN NOT NULL DEFAULT false
508
- * );
509
- * CREATE INDEX IF NOT EXISTS idx_api_keys_actor ON api_keys (actor_id);
510
- * CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys (key_hash);
511
- */
512
- `;
513
- }
@@ -1,75 +0,0 @@
1
- /**
2
- * BoneScript Database Emitter
3
- * Generates db.ts (connection pool) and migrate.ts (migration runner).
4
- */
5
-
6
- import * as IR from "./ir";
7
-
8
- function toSnakeCase(s: string): string {
9
- return s.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
10
- }
11
-
12
- export function emitDbClient(system: IR.IRSystem): string {
13
- const name = toSnakeCase(system.name);
14
- return [
15
- "// Generated by BoneScript compiler. DO NOT EDIT.",
16
- `import { Pool, PoolClient } from "pg";`,
17
- "",
18
- "// Lazy pool — created on first use so DATABASE_URL is read after dotenv loads",
19
- "let _pool: Pool | null = null;",
20
- "function getPool(): Pool {",
21
- " if (!_pool) {",
22
- ` _pool = new Pool({ connectionString: process.env.DATABASE_URL || "postgresql://localhost:5432/${name}", max: 20 });`,
23
- ` _pool.on("error", (err: Error) => console.error("[DB] Pool error (non-fatal):", err.message));`,
24
- " }",
25
- " return _pool;",
26
- "}",
27
- "export const pool = new Proxy({} as Pool, { get(_t: any, p: any) { return (getPool() as any)[p]; } });",
28
- "",
29
- "export async function query<T = any>(text: string, params?: any[]): Promise<T[]> {",
30
- " try { const result = await pool.query(text, params); return result.rows as T[]; }",
31
- " catch (e: any) { throw new Error(`DB query failed: ${e.message}`); }",
32
- "}",
33
- "export async function queryOne<T = any>(text: string, params?: any[]): Promise<T | null> {",
34
- " try { const rows = await query<T>(text, params); return rows[0] || null; }",
35
- " catch (e: any) { throw new Error(`DB query failed: ${e.message}`); }",
36
- "}",
37
- "export async function execute(text: string, params?: any[]): Promise<number> {",
38
- " try { const result = await pool.query(text, params); return result.rowCount || 0; }",
39
- " catch (e: any) { throw new Error(`DB execute failed: ${e.message}`); }",
40
- "}",
41
- "export async function transaction<T>(fn: (client: PoolClient) => Promise<T>): Promise<T> {",
42
- " const client = await pool.connect();",
43
- ` try { await client.query("BEGIN"); const result = await fn(client); await client.query("COMMIT"); return result; }`,
44
- ` catch (e) { await client.query("ROLLBACK"); throw e; }`,
45
- " finally { client.release(); }",
46
- "}",
47
- ].join("\n");
48
- }
49
-
50
- export function emitMigration(system: IR.IRSystem, schemas: string[]): string {
51
- const lines: string[] = [];
52
- lines.push(`// Generated by BoneScript compiler. DO NOT EDIT.`);
53
- lines.push(`require("dotenv").config();`);
54
- lines.push(`import { pool } from "./db";`);
55
- lines.push(``);
56
- lines.push(`async function migrate() {`);
57
- lines.push(` console.log("Running migrations...");`);
58
- lines.push(` const client = await pool.connect();`);
59
- lines.push(` try {`);
60
-
61
- for (const schema of schemas) {
62
- const escaped = schema.replace(/`/g, "\\`").replace(/\$/g, "\\$");
63
- lines.push(` await client.query(\`${escaped}\`);`);
64
- }
65
-
66
- lines.push(` console.log("Migrations complete.");`);
67
- lines.push(` } finally {`);
68
- lines.push(` client.release();`);
69
- lines.push(` await pool.end();`);
70
- lines.push(` }`);
71
- lines.push(`}`);
72
- lines.push(``);
73
- lines.push(`migrate().catch(e => { console.error(e); process.exit(1); });`);
74
- return lines.join("\n");
75
- }