bonescript-compiler 0.5.3 → 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.
- package/LICENSE +21 -21
- package/dist/algorithm_catalog.js +166 -166
- package/dist/cli.d.ts +1 -2
- package/dist/cli.js +543 -75
- package/dist/cli.js.map +1 -1
- package/dist/emit_capability.d.ts +0 -13
- package/dist/emit_capability.js +134 -296
- package/dist/emit_capability.js.map +1 -1
- package/dist/emit_composition.js +3 -37
- package/dist/emit_composition.js.map +1 -1
- package/dist/emit_deploy.js +167 -165
- package/dist/emit_deploy.js.map +1 -1
- package/dist/emit_events.d.ts +0 -1
- package/dist/emit_events.js +275 -325
- package/dist/emit_events.js.map +1 -1
- package/dist/emit_extras.js +5 -3
- package/dist/emit_extras.js.map +1 -1
- package/dist/emit_full.js +112 -272
- package/dist/emit_full.js.map +1 -1
- package/dist/emit_maintenance.js +249 -249
- package/dist/emit_runtime.d.ts +11 -17
- package/dist/emit_runtime.js +688 -29
- package/dist/emit_runtime.js.map +1 -1
- package/dist/emit_sourcemap.js +66 -66
- package/dist/emit_tests.js +12 -47
- package/dist/emit_tests.js.map +1 -1
- package/dist/emit_websocket.js +3 -0
- package/dist/emit_websocket.js.map +1 -1
- package/dist/emitter.js +49 -94
- package/dist/emitter.js.map +1 -1
- package/dist/extension_manager.d.ts +2 -2
- package/dist/extension_manager.js +20 -9
- package/dist/extension_manager.js.map +1 -1
- package/dist/ir.d.ts +0 -4
- package/dist/lowering.d.ts +14 -5
- package/dist/lowering.js +417 -66
- package/dist/lowering.js.map +1 -1
- package/dist/module_loader.d.ts +2 -2
- package/dist/module_loader.js +23 -20
- package/dist/module_loader.js.map +1 -1
- package/dist/optimizer.js +3 -6
- package/dist/optimizer.js.map +1 -1
- package/dist/scaffold.d.ts +2 -2
- package/dist/scaffold.js +319 -315
- package/dist/scaffold.js.map +1 -1
- package/dist/solver.js +1 -1
- package/dist/solver.js.map +1 -1
- package/dist/source_map.js.map +1 -0
- package/dist/test.js.map +1 -0
- package/dist/test_typechecker.d.ts +5 -0
- package/dist/test_typechecker.js +126 -0
- package/dist/test_typechecker.js.map +1 -0
- package/dist/typechecker.d.ts +0 -7
- package/dist/typechecker.js +16 -103
- package/dist/typechecker.js.map +1 -1
- package/dist/verifier.d.ts +1 -5
- package/dist/verifier.js +38 -142
- package/dist/verifier.js.map +1 -1
- package/package.json +52 -62
- package/src/algorithm_catalog.ts +345 -345
- package/src/ast.d.ts +244 -0
- package/src/ast.ts +334 -334
- package/src/cli.ts +624 -98
- package/src/emit_batch.ts +140 -140
- package/src/emit_capability.ts +436 -613
- package/src/emit_composition.ts +196 -229
- package/src/emit_deploy.ts +190 -187
- package/src/emit_events.ts +307 -362
- package/src/emit_extras.ts +240 -237
- package/src/emit_full.ts +309 -472
- package/src/emit_maintenance.ts +459 -459
- package/src/emit_runtime.ts +730 -17
- package/src/emit_sourcemap.ts +140 -140
- package/src/emit_tests.ts +205 -243
- package/src/emit_websocket.ts +229 -226
- package/src/emitter.ts +578 -626
- package/src/extension_manager.ts +187 -177
- package/src/formatter.ts +297 -297
- package/src/index.ts +88 -88
- package/src/ir.ts +215 -216
- package/src/lexer.d.ts +195 -0
- package/src/lexer.ts +630 -630
- package/src/lowering.ts +556 -168
- package/src/module_loader.ts +114 -112
- package/src/optimizer.ts +196 -199
- package/src/parse_decls.d.ts +13 -0
- package/src/parse_decls.ts +409 -409
- package/src/parse_decls2.d.ts +13 -0
- package/src/parse_decls2.ts +244 -244
- package/src/parse_expr.d.ts +7 -0
- package/src/parse_expr.ts +197 -197
- package/src/parse_types.d.ts +6 -0
- package/src/parse_types.ts +54 -54
- package/src/parser.d.ts +10 -0
- package/src/parser.ts +1 -1
- package/src/parser_base.d.ts +19 -0
- package/src/parser_base.ts +57 -57
- package/src/parser_recovery.ts +153 -153
- package/src/scaffold.ts +375 -371
- package/src/solver.ts +330 -330
- package/src/typechecker.d.ts +52 -0
- package/src/typechecker.ts +591 -700
- package/src/types.d.ts +38 -0
- package/src/types.ts +122 -122
- package/src/verifier.ts +49 -154
- package/README.md +0 -382
- package/dist/commands/check.d.ts +0 -5
- package/dist/commands/check.js +0 -34
- package/dist/commands/check.js.map +0 -1
- package/dist/commands/compile.d.ts +0 -5
- package/dist/commands/compile.js +0 -215
- package/dist/commands/compile.js.map +0 -1
- package/dist/commands/debug.d.ts +0 -5
- package/dist/commands/debug.js +0 -59
- package/dist/commands/debug.js.map +0 -1
- package/dist/commands/diff.d.ts +0 -5
- package/dist/commands/diff.js +0 -123
- package/dist/commands/diff.js.map +0 -1
- package/dist/commands/fmt.d.ts +0 -5
- package/dist/commands/fmt.js +0 -49
- package/dist/commands/fmt.js.map +0 -1
- package/dist/commands/init.d.ts +0 -5
- package/dist/commands/init.js +0 -96
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/ir.d.ts +0 -5
- package/dist/commands/ir.js +0 -27
- package/dist/commands/ir.js.map +0 -1
- package/dist/commands/lex.d.ts +0 -5
- package/dist/commands/lex.js +0 -21
- package/dist/commands/lex.js.map +0 -1
- package/dist/commands/parse.d.ts +0 -5
- package/dist/commands/parse.js +0 -30
- package/dist/commands/parse.js.map +0 -1
- package/dist/commands/test.d.ts +0 -5
- package/dist/commands/test.js +0 -61
- package/dist/commands/test.js.map +0 -1
- package/dist/commands/verify_determinism.d.ts +0 -5
- package/dist/commands/verify_determinism.js +0 -64
- package/dist/commands/verify_determinism.js.map +0 -1
- package/dist/commands/watch.d.ts +0 -5
- package/dist/commands/watch.js +0 -50
- package/dist/commands/watch.js.map +0 -1
- package/dist/emit_auth.d.ts +0 -18
- package/dist/emit_auth.js +0 -507
- package/dist/emit_auth.js.map +0 -1
- package/dist/emit_database.d.ts +0 -7
- package/dist/emit_database.js +0 -72
- package/dist/emit_database.js.map +0 -1
- package/dist/emit_index.d.ts +0 -6
- package/dist/emit_index.js +0 -202
- package/dist/emit_index.js.map +0 -1
- package/dist/emit_models.d.ts +0 -12
- package/dist/emit_models.js +0 -171
- package/dist/emit_models.js.map +0 -1
- package/dist/emit_openapi.d.ts +0 -9
- package/dist/emit_openapi.js +0 -306
- package/dist/emit_openapi.js.map +0 -1
- package/dist/emit_package.d.ts +0 -7
- package/dist/emit_package.js +0 -68
- package/dist/emit_package.js.map +0 -1
- package/dist/emit_router.d.ts +0 -12
- package/dist/emit_router.js +0 -389
- package/dist/emit_router.js.map +0 -1
- package/dist/lowering_channels.d.ts +0 -11
- package/dist/lowering_channels.js +0 -103
- package/dist/lowering_channels.js.map +0 -1
- package/dist/lowering_entities.d.ts +0 -11
- package/dist/lowering_entities.js +0 -232
- package/dist/lowering_entities.js.map +0 -1
- package/dist/lowering_helpers.d.ts +0 -13
- package/dist/lowering_helpers.js +0 -76
- package/dist/lowering_helpers.js.map +0 -1
- package/src/commands/check.ts +0 -33
- package/src/commands/compile.ts +0 -191
- package/src/commands/debug.ts +0 -33
- package/src/commands/diff.ts +0 -105
- package/src/commands/fmt.ts +0 -22
- package/src/commands/init.ts +0 -72
- package/src/commands/ir.ts +0 -23
- package/src/commands/lex.ts +0 -17
- package/src/commands/parse.ts +0 -24
- package/src/commands/test.ts +0 -36
- package/src/commands/verify_determinism.ts +0 -66
- package/src/commands/watch.ts +0 -25
- package/src/emit_auth.ts +0 -513
- package/src/emit_database.ts +0 -72
- package/src/emit_index.ts +0 -210
- package/src/emit_models.ts +0 -176
- package/src/emit_openapi.ts +0 -315
- package/src/emit_package.ts +0 -66
- package/src/emit_router.ts +0 -408
- package/src/lowering_channels.ts +0 -108
- package/src/lowering_entities.ts +0 -258
- 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
|
-
}
|
package/src/emit_database.ts
DELETED
|
@@ -1,72 +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
|
-
import { toSnakeCase } from "./lowering_helpers";
|
|
8
|
-
|
|
9
|
-
export function emitDbClient(system: IR.IRSystem): string {
|
|
10
|
-
const name = toSnakeCase(system.name);
|
|
11
|
-
return [
|
|
12
|
-
"// Generated by BoneScript compiler. DO NOT EDIT.",
|
|
13
|
-
`import { Pool, PoolClient } from "pg";`,
|
|
14
|
-
"",
|
|
15
|
-
"// Lazy pool — created on first use so DATABASE_URL is read after dotenv loads",
|
|
16
|
-
"let _pool: Pool | null = null;",
|
|
17
|
-
"function getPool(): Pool {",
|
|
18
|
-
" if (!_pool) {",
|
|
19
|
-
` _pool = new Pool({ connectionString: process.env.DATABASE_URL || "postgresql://localhost:5432/${name}", max: 20 });`,
|
|
20
|
-
` _pool.on("error", (err: Error) => console.error("[DB] Pool error (non-fatal):", err.message));`,
|
|
21
|
-
" }",
|
|
22
|
-
" return _pool;",
|
|
23
|
-
"}",
|
|
24
|
-
"export const pool = new Proxy({} as Pool, { get(_t: any, p: any) { return (getPool() as any)[p]; } });",
|
|
25
|
-
"",
|
|
26
|
-
"export async function query<T = any>(text: string, params?: any[]): Promise<T[]> {",
|
|
27
|
-
" try { const result = await pool.query(text, params); return result.rows as T[]; }",
|
|
28
|
-
" catch (e: any) { throw new Error(`DB query failed: ${e.message}`); }",
|
|
29
|
-
"}",
|
|
30
|
-
"export async function queryOne<T = any>(text: string, params?: any[]): Promise<T | null> {",
|
|
31
|
-
" try { const rows = await query<T>(text, params); return rows[0] || null; }",
|
|
32
|
-
" catch (e: any) { throw new Error(`DB query failed: ${e.message}`); }",
|
|
33
|
-
"}",
|
|
34
|
-
"export async function execute(text: string, params?: any[]): Promise<number> {",
|
|
35
|
-
" try { const result = await pool.query(text, params); return result.rowCount || 0; }",
|
|
36
|
-
" catch (e: any) { throw new Error(`DB execute failed: ${e.message}`); }",
|
|
37
|
-
"}",
|
|
38
|
-
"export async function transaction<T>(fn: (client: PoolClient) => Promise<T>): Promise<T> {",
|
|
39
|
-
" const client = await pool.connect();",
|
|
40
|
-
` try { await client.query("BEGIN"); const result = await fn(client); await client.query("COMMIT"); return result; }`,
|
|
41
|
-
` catch (e) { await client.query("ROLLBACK"); throw e; }`,
|
|
42
|
-
" finally { client.release(); }",
|
|
43
|
-
"}",
|
|
44
|
-
].join("\n");
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function emitMigration(system: IR.IRSystem, schemas: string[]): string {
|
|
48
|
-
const lines: string[] = [];
|
|
49
|
-
lines.push(`// Generated by BoneScript compiler. DO NOT EDIT.`);
|
|
50
|
-
lines.push(`require("dotenv").config();`);
|
|
51
|
-
lines.push(`import { pool } from "./db";`);
|
|
52
|
-
lines.push(``);
|
|
53
|
-
lines.push(`async function migrate() {`);
|
|
54
|
-
lines.push(` console.log("Running migrations...");`);
|
|
55
|
-
lines.push(` const client = await pool.connect();`);
|
|
56
|
-
lines.push(` try {`);
|
|
57
|
-
|
|
58
|
-
for (const schema of schemas) {
|
|
59
|
-
const escaped = schema.replace(/`/g, "\\`").replace(/\$/g, "\\$");
|
|
60
|
-
lines.push(` await client.query(\`${escaped}\`);`);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
lines.push(` console.log("Migrations complete.");`);
|
|
64
|
-
lines.push(` } finally {`);
|
|
65
|
-
lines.push(` client.release();`);
|
|
66
|
-
lines.push(` await pool.end();`);
|
|
67
|
-
lines.push(` }`);
|
|
68
|
-
lines.push(`}`);
|
|
69
|
-
lines.push(``);
|
|
70
|
-
lines.push(`migrate().catch(e => { console.error(e); process.exit(1); });`);
|
|
71
|
-
return lines.join("\n");
|
|
72
|
-
}
|