bonescript-compiler 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,18 @@
1
1
  /**
2
2
  * BoneScript Auth Emitter
3
- * Generates auth.ts — JWT middleware with production safety checks.
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
4
16
  */
5
17
  import * as IR from "./ir";
6
- export declare function emitAuthMiddleware(_system: IR.IRSystem): string;
18
+ export declare function emitAuthMiddleware(system: IR.IRSystem): string;
package/dist/emit_auth.js CHANGED
@@ -1,69 +1,507 @@
1
1
  "use strict";
2
2
  /**
3
3
  * BoneScript Auth Emitter
4
- * Generates auth.ts — JWT middleware with production safety checks.
4
+ * Generates auth.ts — JWT, OAuth2, or API key middleware depending on the
5
+ * resolved auth_method in the system's constraint resolution map.
6
+ *
7
+ * Auth method selection (in priority order):
8
+ * 1. system.resolution["implied.auth_method"] — set by constraint solver
9
+ * 2. Any api_service module's config.auth_method
10
+ * 3. Fallback: jwt
11
+ *
12
+ * Generated strategies:
13
+ * jwt — Bearer token verification via jsonwebtoken
14
+ * oauth2 — Authorization Code flow: /auth/login redirect + /auth/callback
15
+ * token exchange + refresh token rotation
16
+ * apikey — X-API-Key header lookup against hashed keys in the database
5
17
  */
6
18
  Object.defineProperty(exports, "__esModule", { value: true });
7
19
  exports.emitAuthMiddleware = void 0;
8
- function emitAuthMiddleware(_system) {
9
- return `// Generated by BoneScript compiler. DO NOT EDIT.
10
- import { Request, Response, NextFunction } from "express";
11
- import jwt from "jsonwebtoken";
12
-
13
- // JWT_SECRET must be set in production. The server will refuse to start without it
14
- // when NODE_ENV is "production" to prevent accidental use of a weak fallback.
15
- const JWT_SECRET = (() => {
16
- const secret = process.env.JWT_SECRET;
17
- if (!secret) {
18
- if (process.env.NODE_ENV === "production") {
19
- console.error("[FATAL] JWT_SECRET environment variable is not set. Refusing to start in production.");
20
- process.exit(1);
21
- }
22
- console.warn("[WARN] JWT_SECRET is not set. Using insecure default — do not use in production.");
23
- return "bonescript-dev-secret-do-not-use-in-production";
24
- }
25
- if (secret.length < 32) {
26
- console.warn("[WARN] JWT_SECRET is shorter than 32 characters. Use a longer secret in production.");
27
- }
28
- return secret;
29
- })();
30
-
31
- export interface AuthContext {
32
- authenticated: boolean;
33
- actor_id: string | null;
34
- trace_id: string;
35
- }
36
-
37
- export function authMiddleware(req: Request, res: Response, next: NextFunction): void {
38
- const header = req.headers.authorization;
39
- if (!header || !header.startsWith("Bearer ")) {
40
- (req as any).auth = { authenticated: false, actor_id: null, trace_id: req.headers["x-trace-id"] as string || "" };
41
- next();
42
- return;
43
- }
44
- try {
45
- const token = header.slice(7);
46
- const decoded = jwt.verify(token, JWT_SECRET) as { sub: string };
47
- (req as any).auth = {
48
- authenticated: true,
49
- actor_id: decoded.sub,
50
- trace_id: req.headers["x-trace-id"] as string || "",
51
- };
52
- } catch {
53
- (req as any).auth = { authenticated: false, actor_id: null, trace_id: req.headers["x-trace-id"] as string || "" };
54
- }
55
- next();
56
- }
57
-
58
- export function requireAuth(req: Request, res: Response, next: NextFunction): void {
59
- const auth: AuthContext = (req as any).auth;
60
- if (!auth || !auth.authenticated) {
61
- res.status(401).json({ error: { code: "UNAUTHORIZED", message: "Authentication required" } });
62
- return;
63
- }
64
- next();
65
- }
66
- `;
20
+ /** Resolve the auth method for the system. */
21
+ function resolveAuthMethod(system) {
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
+ // Check per-module resolution keys written by the solver
31
+ for (const [key, val] of Object.entries(system.resolution)) {
32
+ if (key.endsWith(".auth_method") && (val === "oauth2" || val === "apikey" || val === "jwt")) {
33
+ return val;
34
+ }
35
+ }
36
+ // Fall back to any api_service module config
37
+ for (const mod of system.modules) {
38
+ const m = mod.config["auth_method"];
39
+ if (m === "oauth2" || m === "apikey" || m === "jwt")
40
+ return m;
41
+ }
42
+ return "jwt";
43
+ }
44
+ function emitAuthMiddleware(system) {
45
+ const method = resolveAuthMethod(system);
46
+ switch (method) {
47
+ case "oauth2": return emitOAuth2Auth();
48
+ case "apikey": return emitApiKeyAuth();
49
+ default: return emitJwtAuth();
50
+ }
67
51
  }
68
52
  exports.emitAuthMiddleware = emitAuthMiddleware;
53
+ // ─── JWT ──────────────────────────────────────────────────────────────────────
54
+ function emitJwtAuth() {
55
+ return `// Generated by BoneScript compiler. DO NOT EDIT.
56
+ // Auth strategy: JWT (Bearer token)
57
+ import { Request, Response, NextFunction } from "express";
58
+ import jwt from "jsonwebtoken";
59
+
60
+ // JWT_SECRET must be set in production. The server will refuse to start without it
61
+ // when NODE_ENV is "production" to prevent accidental use of a weak fallback.
62
+ const JWT_SECRET = (() => {
63
+ const secret = process.env.JWT_SECRET;
64
+ if (!secret) {
65
+ if (process.env.NODE_ENV === "production") {
66
+ console.error("[FATAL] JWT_SECRET environment variable is not set. Refusing to start in production.");
67
+ process.exit(1);
68
+ }
69
+ console.warn("[WARN] JWT_SECRET is not set. Using insecure default — do not use in production.");
70
+ return "bonescript-dev-secret-do-not-use-in-production";
71
+ }
72
+ if (secret.length < 32) {
73
+ console.warn("[WARN] JWT_SECRET is shorter than 32 characters. Use a longer secret in production.");
74
+ }
75
+ return secret;
76
+ })();
77
+
78
+ export interface AuthContext {
79
+ authenticated: boolean;
80
+ actor_id: string | null;
81
+ trace_id: string;
82
+ }
83
+
84
+ export function authMiddleware(req: Request, res: Response, next: NextFunction): void {
85
+ const header = req.headers.authorization;
86
+ if (!header || !header.startsWith("Bearer ")) {
87
+ (req as any).auth = { authenticated: false, actor_id: null, trace_id: req.headers["x-trace-id"] as string || "" };
88
+ next();
89
+ return;
90
+ }
91
+ try {
92
+ const token = header.slice(7);
93
+ const decoded = jwt.verify(token, JWT_SECRET) as { sub: string };
94
+ (req as any).auth = {
95
+ authenticated: true,
96
+ actor_id: decoded.sub,
97
+ trace_id: req.headers["x-trace-id"] as string || "",
98
+ };
99
+ } catch {
100
+ (req as any).auth = { authenticated: false, actor_id: null, trace_id: req.headers["x-trace-id"] as string || "" };
101
+ }
102
+ next();
103
+ }
104
+
105
+ export function requireAuth(req: Request, res: Response, next: NextFunction): void {
106
+ const auth: AuthContext = (req as any).auth;
107
+ if (!auth || !auth.authenticated) {
108
+ res.status(401).json({ error: { code: "UNAUTHORIZED", message: "Authentication required" } });
109
+ return;
110
+ }
111
+ next();
112
+ }
113
+
114
+ /** Issue a signed JWT for a given actor. Used by login/register routes. */
115
+ export function issueToken(actorId: string, expiresIn: string = "24h"): string {
116
+ return jwt.sign({ sub: actorId }, JWT_SECRET, { expiresIn } as any);
117
+ }
118
+ `;
119
+ }
120
+ // ─── OAuth2 ───────────────────────────────────────────────────────────────────
121
+ function emitOAuth2Auth() {
122
+ return `// Generated by BoneScript compiler. DO NOT EDIT.
123
+ // Auth strategy: OAuth2 Authorization Code Flow with PKCE + refresh token rotation
124
+ import { Request, Response, NextFunction, Router } from "express";
125
+ import crypto from "crypto";
126
+ import jwt from "jsonwebtoken";
127
+
128
+ // ─── Configuration ────────────────────────────────────────────────────────────
129
+
130
+ const OAUTH2_CLIENT_ID = process.env.OAUTH2_CLIENT_ID || "";
131
+ const OAUTH2_CLIENT_SECRET = process.env.OAUTH2_CLIENT_SECRET || "";
132
+ const OAUTH2_AUTH_URL = process.env.OAUTH2_AUTH_URL || "";
133
+ const OAUTH2_TOKEN_URL = process.env.OAUTH2_TOKEN_URL || "";
134
+ const OAUTH2_REDIRECT_URI = process.env.OAUTH2_REDIRECT_URI || "http://localhost:3000/auth/callback";
135
+ const OAUTH2_SCOPES = (process.env.OAUTH2_SCOPES || "openid profile email").split(" ");
136
+
137
+ const JWT_SECRET = (() => {
138
+ const secret = process.env.JWT_SECRET;
139
+ if (!secret) {
140
+ if (process.env.NODE_ENV === "production") {
141
+ console.error("[FATAL] JWT_SECRET environment variable is not set. Refusing to start in production.");
142
+ process.exit(1);
143
+ }
144
+ console.warn("[WARN] JWT_SECRET is not set. Using insecure default — do not use in production.");
145
+ return "bonescript-dev-secret-do-not-use-in-production";
146
+ }
147
+ return secret;
148
+ })();
149
+
150
+ if (process.env.NODE_ENV === "production") {
151
+ if (!OAUTH2_CLIENT_ID || !OAUTH2_CLIENT_SECRET || !OAUTH2_AUTH_URL || !OAUTH2_TOKEN_URL) {
152
+ console.error("[FATAL] OAuth2 configuration incomplete. Set OAUTH2_CLIENT_ID, OAUTH2_CLIENT_SECRET, OAUTH2_AUTH_URL, OAUTH2_TOKEN_URL.");
153
+ process.exit(1);
154
+ }
155
+ }
156
+
157
+ // ─── PKCE helpers ─────────────────────────────────────────────────────────────
158
+
159
+ function generateCodeVerifier(): string {
160
+ return crypto.randomBytes(32).toString("base64url");
161
+ }
162
+
163
+ function generateCodeChallenge(verifier: string): string {
164
+ return crypto.createHash("sha256").update(verifier).digest("base64url");
165
+ }
166
+
167
+ // ─── In-memory state store (replace with Redis in production) ─────────────────
168
+
169
+ const pendingStates = new Map<string, { verifier: string; createdAt: number }>();
170
+
171
+ // Clean up stale states every 5 minutes
172
+ setInterval(() => {
173
+ const cutoff = Date.now() - 10 * 60 * 1000; // 10 min TTL
174
+ for (const [k, v] of pendingStates) {
175
+ if (v.createdAt < cutoff) pendingStates.delete(k);
176
+ }
177
+ }, 5 * 60 * 1000);
178
+
179
+ // ─── Auth context ─────────────────────────────────────────────────────────────
180
+
181
+ export interface AuthContext {
182
+ authenticated: boolean;
183
+ actor_id: string | null;
184
+ trace_id: string;
185
+ }
186
+
187
+ // ─── Middleware ───────────────────────────────────────────────────────────────
188
+
189
+ export function authMiddleware(req: Request, res: Response, next: NextFunction): void {
190
+ const header = req.headers.authorization;
191
+ if (!header || !header.startsWith("Bearer ")) {
192
+ (req as any).auth = { authenticated: false, actor_id: null, trace_id: req.headers["x-trace-id"] as string || "" };
193
+ next();
194
+ return;
195
+ }
196
+ try {
197
+ const token = header.slice(7);
198
+ const decoded = jwt.verify(token, JWT_SECRET) as { sub: string };
199
+ (req as any).auth = {
200
+ authenticated: true,
201
+ actor_id: decoded.sub,
202
+ trace_id: req.headers["x-trace-id"] as string || "",
203
+ };
204
+ } catch {
205
+ (req as any).auth = { authenticated: false, actor_id: null, trace_id: req.headers["x-trace-id"] as string || "" };
206
+ }
207
+ next();
208
+ }
209
+
210
+ export function requireAuth(req: Request, res: Response, next: NextFunction): void {
211
+ const auth: AuthContext = (req as any).auth;
212
+ if (!auth || !auth.authenticated) {
213
+ res.status(401).json({ error: { code: "UNAUTHORIZED", message: "Authentication required" } });
214
+ return;
215
+ }
216
+ next();
217
+ }
218
+
219
+ // ─── OAuth2 Routes ────────────────────────────────────────────────────────────
220
+
221
+ export const authRouter = Router();
222
+
223
+ /** GET /auth/login — redirect to OAuth2 provider */
224
+ authRouter.get("/login", (_req: Request, res: Response) => {
225
+ const state = crypto.randomBytes(16).toString("hex");
226
+ const verifier = generateCodeVerifier();
227
+ const challenge = generateCodeChallenge(verifier);
228
+ pendingStates.set(state, { verifier, createdAt: Date.now() });
229
+
230
+ const params = new URLSearchParams({
231
+ response_type: "code",
232
+ client_id: OAUTH2_CLIENT_ID,
233
+ redirect_uri: OAUTH2_REDIRECT_URI,
234
+ scope: OAUTH2_SCOPES.join(" "),
235
+ state,
236
+ code_challenge: challenge,
237
+ code_challenge_method: "S256",
238
+ });
239
+ res.redirect(\`\${OAUTH2_AUTH_URL}?\${params}\`);
240
+ });
241
+
242
+ /** GET /auth/callback — exchange code for tokens, issue internal JWT */
243
+ authRouter.get("/callback", async (req: Request, res: Response) => {
244
+ const { code, state, error } = req.query as Record<string, string>;
245
+
246
+ if (error) {
247
+ return res.status(400).json({ error: { code: "OAUTH2_ERROR", message: error } });
248
+ }
249
+ if (!code || !state) {
250
+ return res.status(400).json({ error: { code: "MISSING_PARAMS", message: "code and state are required" } });
251
+ }
252
+
253
+ const pending = pendingStates.get(state);
254
+ if (!pending) {
255
+ return res.status(400).json({ error: { code: "INVALID_STATE", message: "Unknown or expired state parameter" } });
256
+ }
257
+ pendingStates.delete(state);
258
+
259
+ try {
260
+ // Exchange authorization code for tokens
261
+ const tokenRes = await fetch(OAUTH2_TOKEN_URL, {
262
+ method: "POST",
263
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
264
+ body: new URLSearchParams({
265
+ grant_type: "authorization_code",
266
+ code,
267
+ redirect_uri: OAUTH2_REDIRECT_URI,
268
+ client_id: OAUTH2_CLIENT_ID,
269
+ client_secret: OAUTH2_CLIENT_SECRET,
270
+ code_verifier: pending.verifier,
271
+ }),
272
+ });
273
+
274
+ if (!tokenRes.ok) {
275
+ const body = await tokenRes.text();
276
+ console.error("[OAuth2] Token exchange failed:", body);
277
+ return res.status(502).json({ error: { code: "TOKEN_EXCHANGE_FAILED", message: "Failed to exchange authorization code" } });
278
+ }
279
+
280
+ const tokens = await tokenRes.json() as {
281
+ access_token: string;
282
+ id_token?: string;
283
+ refresh_token?: string;
284
+ expires_in?: number;
285
+ };
286
+
287
+ // Extract subject from id_token or access_token
288
+ let actorId: string;
289
+ try {
290
+ const payload = JSON.parse(Buffer.from((tokens.id_token || tokens.access_token).split(".")[1], "base64url").toString());
291
+ actorId = payload.sub || payload.email || payload.preferred_username || "unknown";
292
+ } catch {
293
+ actorId = "unknown";
294
+ }
295
+
296
+ // Issue internal JWT (short-lived)
297
+ const internalToken = jwt.sign({ sub: actorId }, JWT_SECRET, { expiresIn: "1h" } as any);
298
+
299
+ res.json({
300
+ token: internalToken,
301
+ actor_id: actorId,
302
+ expires_in: 3600,
303
+ });
304
+ } catch (err: any) {
305
+ console.error("[OAuth2] Callback error:", err.message);
306
+ res.status(500).json({ error: { code: "INTERNAL_ERROR", message: "OAuth2 callback failed" } });
307
+ }
308
+ });
309
+
310
+ /** POST /auth/refresh — refresh an expired internal JWT */
311
+ authRouter.post("/refresh", (req: Request, res: Response) => {
312
+ const { token } = req.body as { token?: string };
313
+ if (!token) {
314
+ return res.status(400).json({ error: { code: "MISSING_TOKEN", message: "token is required" } });
315
+ }
316
+ try {
317
+ // Allow expired tokens for refresh (ignoreExpiration)
318
+ const decoded = jwt.verify(token, JWT_SECRET, { ignoreExpiration: true }) as { sub: string };
319
+ const newToken = jwt.sign({ sub: decoded.sub }, JWT_SECRET, { expiresIn: "1h" } as any);
320
+ res.json({ token: newToken, expires_in: 3600 });
321
+ } catch {
322
+ res.status(401).json({ error: { code: "INVALID_TOKEN", message: "Cannot refresh: token is invalid" } });
323
+ }
324
+ });
325
+
326
+ /** POST /auth/logout — client-side only (stateless JWT); invalidation requires a denylist */
327
+ authRouter.post("/logout", (_req: Request, res: Response) => {
328
+ res.json({ ok: true, message: "Logged out. Discard the token on the client." });
329
+ });
330
+ `;
331
+ }
332
+ // ─── API Key ──────────────────────────────────────────────────────────────────
333
+ function emitApiKeyAuth() {
334
+ return `// Generated by BoneScript compiler. DO NOT EDIT.
335
+ // Auth strategy: API Key (X-API-Key header, hashed keys stored in database)
336
+ import { Request, Response, NextFunction, Router } from "express";
337
+ import crypto from "crypto";
338
+ import { query } from "./db";
339
+
340
+ // ─── Auth context ─────────────────────────────────────────────────────────────
341
+
342
+ export interface AuthContext {
343
+ authenticated: boolean;
344
+ actor_id: string | null;
345
+ trace_id: string;
346
+ }
347
+
348
+ // ─── Key hashing ──────────────────────────────────────────────────────────────
349
+
350
+ /** Hash an API key with SHA-256 for safe storage. Never store raw keys. */
351
+ function hashApiKey(key: string): string {
352
+ return crypto.createHash("sha256").update(key).digest("hex");
353
+ }
354
+
355
+ /** Generate a new API key. Returns the raw key (shown once) and its hash. */
356
+ export function generateApiKey(): { key: string; hash: string; prefix: string } {
357
+ const raw = "bsk_" + crypto.randomBytes(32).toString("base64url");
358
+ return { key: raw, hash: hashApiKey(raw), prefix: raw.slice(0, 12) };
359
+ }
360
+
361
+ // ─── In-memory LRU cache (avoids a DB hit on every request) ──────────────────
362
+
363
+ const KEY_CACHE_TTL_MS = 60_000; // 1 minute
364
+ const keyCache = new Map<string, { actorId: string; expiresAt: number }>();
365
+
366
+ function getCached(hash: string): string | null {
367
+ const entry = keyCache.get(hash);
368
+ if (!entry) return null;
369
+ if (Date.now() > entry.expiresAt) { keyCache.delete(hash); return null; }
370
+ return entry.actorId;
371
+ }
372
+
373
+ function setCached(hash: string, actorId: string): void {
374
+ // Evict oldest entry if cache is too large
375
+ if (keyCache.size >= 1000) {
376
+ const oldest = [...keyCache.entries()].sort((a, b) => a[1].expiresAt - b[1].expiresAt)[0];
377
+ if (oldest) keyCache.delete(oldest[0]);
378
+ }
379
+ keyCache.set(hash, { actorId, expiresAt: Date.now() + KEY_CACHE_TTL_MS });
380
+ }
381
+
382
+ // ─── Middleware ───────────────────────────────────────────────────────────────
383
+
384
+ export async function authMiddleware(req: Request, res: Response, next: NextFunction): Promise<void> {
385
+ const rawKey = req.headers["x-api-key"] as string | undefined;
386
+ const traceId = req.headers["x-trace-id"] as string || "";
387
+
388
+ if (!rawKey) {
389
+ (req as any).auth = { authenticated: false, actor_id: null, trace_id: traceId };
390
+ next();
391
+ return;
392
+ }
393
+
394
+ const hash = hashApiKey(rawKey);
395
+
396
+ // Check cache first
397
+ const cached = getCached(hash);
398
+ if (cached) {
399
+ (req as any).auth = { authenticated: true, actor_id: cached, trace_id: traceId };
400
+ next();
401
+ return;
402
+ }
403
+
404
+ try {
405
+ // Look up in database — api_keys table (see migration below)
406
+ const rows = await query<{ actor_id: string; revoked: boolean }>(
407
+ \`SELECT actor_id, revoked FROM api_keys WHERE key_hash = $1 AND expires_at > NOW()\`,
408
+ [hash]
409
+ );
410
+ if (rows.length === 0 || rows[0].revoked) {
411
+ (req as any).auth = { authenticated: false, actor_id: null, trace_id: traceId };
412
+ next();
413
+ return;
414
+ }
415
+ const actorId = rows[0].actor_id;
416
+ setCached(hash, actorId);
417
+ (req as any).auth = { authenticated: true, actor_id: actorId, trace_id: traceId };
418
+ } catch (err: any) {
419
+ console.error("[ApiKey] Auth lookup failed:", err.message);
420
+ (req as any).auth = { authenticated: false, actor_id: null, trace_id: traceId };
421
+ }
422
+ next();
423
+ }
424
+
425
+ export function requireAuth(req: Request, res: Response, next: NextFunction): void {
426
+ const auth: AuthContext = (req as any).auth;
427
+ if (!auth || !auth.authenticated) {
428
+ res.status(401).json({ error: { code: "UNAUTHORIZED", message: "Valid API key required (X-API-Key header)" } });
429
+ return;
430
+ }
431
+ next();
432
+ }
433
+
434
+ // ─── Key Management Routes ────────────────────────────────────────────────────
435
+
436
+ export const authRouter = Router();
437
+
438
+ /** POST /auth/keys — create a new API key for the authenticated actor */
439
+ authRouter.post("/keys", requireAuth, async (req: Request, res: Response) => {
440
+ const auth: AuthContext = (req as any).auth;
441
+ const { name, expires_in_days = 365 } = req.body as { name?: string; expires_in_days?: number };
442
+
443
+ const { key, hash, prefix } = generateApiKey();
444
+ const expiresAt = new Date(Date.now() + expires_in_days * 86_400_000);
445
+
446
+ try {
447
+ await query(
448
+ \`INSERT INTO api_keys (actor_id, key_hash, key_prefix, name, expires_at) VALUES ($1, $2, $3, $4, $5)\`,
449
+ [auth.actor_id, hash, prefix, name || "default", expiresAt]
450
+ );
451
+ // Return the raw key ONCE — it cannot be retrieved again
452
+ res.status(201).json({ key, prefix, expires_at: expiresAt });
453
+ } catch (err: any) {
454
+ res.status(500).json({ error: { code: "KEY_CREATE_FAILED", message: err.message } });
455
+ }
456
+ });
457
+
458
+ /** GET /auth/keys — list API keys for the authenticated actor (no raw keys) */
459
+ authRouter.get("/keys", requireAuth, async (req: Request, res: Response) => {
460
+ const auth: AuthContext = (req as any).auth;
461
+ try {
462
+ const rows = await query(
463
+ \`SELECT id, key_prefix, name, created_at, expires_at, revoked FROM api_keys WHERE actor_id = $1 ORDER BY created_at DESC\`,
464
+ [auth.actor_id]
465
+ );
466
+ res.json({ keys: rows });
467
+ } catch (err: any) {
468
+ res.status(500).json({ error: { code: "KEY_LIST_FAILED", message: err.message } });
469
+ }
470
+ });
471
+
472
+ /** DELETE /auth/keys/:id — revoke an API key */
473
+ authRouter.delete("/keys/:id", requireAuth, async (req: Request, res: Response) => {
474
+ const auth: AuthContext = (req as any).auth;
475
+ try {
476
+ const result = await query(
477
+ \`UPDATE api_keys SET revoked = true WHERE id = $1 AND actor_id = $2 RETURNING id\`,
478
+ [req.params.id, auth.actor_id]
479
+ );
480
+ if (result.length === 0) {
481
+ return res.status(404).json({ error: { code: "NOT_FOUND", message: "API key not found" } });
482
+ }
483
+ res.json({ ok: true });
484
+ } catch (err: any) {
485
+ res.status(500).json({ error: { code: "KEY_REVOKE_FAILED", message: err.message } });
486
+ }
487
+ });
488
+
489
+ /*
490
+ * Required migration — add to your migrations directory:
491
+ *
492
+ * CREATE TABLE IF NOT EXISTS api_keys (
493
+ * id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
494
+ * actor_id UUID NOT NULL,
495
+ * key_hash VARCHAR(64) NOT NULL UNIQUE,
496
+ * key_prefix VARCHAR(16) NOT NULL,
497
+ * name VARCHAR(255) NOT NULL DEFAULT 'default',
498
+ * created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
499
+ * expires_at TIMESTAMPTZ NOT NULL,
500
+ * revoked BOOLEAN NOT NULL DEFAULT false
501
+ * );
502
+ * CREATE INDEX IF NOT EXISTS idx_api_keys_actor ON api_keys (actor_id);
503
+ * CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys (key_hash);
504
+ */
505
+ `;
506
+ }
69
507
  //# sourceMappingURL=emit_auth.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"emit_auth.js","sourceRoot":"","sources":["../src/emit_auth.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAIH,SAAgB,kBAAkB,CAAC,OAAoB;IACrD,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyDR,CAAC;AACF,CAAC;AA3DD,gDA2DC"}
1
+ {"version":3,"file":"emit_auth.js","sourceRoot":"","sources":["../src/emit_auth.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;GAeG;;;AAIH,8CAA8C;AAC9C,SAAS,iBAAiB,CAAC,MAAmB;IAC5C,wDAAwD;IACxD,qFAAqF;IACrF,+EAA+E;IAC/E,MAAM,QAAQ,GAAG,MAAM,CAAC,UAAU,CAAC,qBAAqB,CAAC;WACpD,MAAM,CAAC,UAAU,CAAC,oBAAoB,CAAC,CAAC;IAC7C,IAAI,QAAQ,KAAK,QAAQ,IAAI,QAAQ,KAAK,QAAQ,IAAI,QAAQ,KAAK,KAAK,EAAE,CAAC;QACzE,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,yDAAyD;IACzD,KAAK,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;QAC3D,IAAI,GAAG,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,CAAC,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,KAAK,CAAC,EAAE,CAAC;YAC5F,OAAO,GAAkC,CAAC;QAC5C,CAAC;IACH,CAAC;IAED,6CAA6C;IAC7C,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;QACjC,MAAM,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,aAAa,CAAuB,CAAC;QAC1D,IAAI,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,KAAK;YAAE,OAAO,CAAC,CAAC;IAChE,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAgB,kBAAkB,CAAC,MAAmB;IACpD,MAAM,MAAM,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAAC;IACzC,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,QAAQ,CAAC,CAAE,OAAO,cAAc,EAAE,CAAC;QACxC,KAAK,QAAQ,CAAC,CAAE,OAAO,cAAc,EAAE,CAAC;QACxC,OAAO,CAAC,CAAQ,OAAO,WAAW,EAAE,CAAC;IACvC,CAAC;AACH,CAAC;AAPD,gDAOC;AAED,iFAAiF;AAEjF,SAAS,WAAW;IAClB,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+DR,CAAC;AACF,CAAC;AAED,iFAAiF;AAEjF,SAAS,cAAc;IACrB,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgNR,CAAC;AACF,CAAC;AAED,iFAAiF;AAEjF,SAAS,cAAc;IACrB,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2KR,CAAC;AACF,CAAC"}