CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history
api-auth.ts
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| 45e31d0 | 1 | /** |
| 2 | * API Token Authentication Middleware | |
| 3 | * | |
| 4 | * Validates Bearer tokens from the Authorization header against stored API tokens. | |
| 5 | * Supports both session cookies (for web UI) and API tokens (for programmatic access). | |
| 6 | */ | |
| 7 | ||
| 8 | import { createMiddleware } from "hono/factory"; | |
| 9 | import { getCookie } from "hono/cookie"; | |
| 10 | import { eq, gt } from "drizzle-orm"; | |
| 11 | import { db } from "../db"; | |
| 12 | import { apiTokens, sessions, users } from "../db/schema"; | |
| 13 | import type { User } from "../db/schema"; | |
| 14 | ||
| 15 | export type ApiAuthEnv = { | |
| 16 | Variables: { | |
| 17 | user: User | null; | |
| 18 | authMethod: "session" | "token" | "none"; | |
| 19 | tokenScopes: string[]; | |
| 20 | }; | |
| 21 | }; | |
| 22 | ||
| 23 | /** | |
| 24 | * Authenticate via Bearer token OR session cookie. | |
| 25 | * Sets c.get("user"), c.get("authMethod"), c.get("tokenScopes"). | |
| 26 | */ | |
| 27 | export const apiAuth = createMiddleware<ApiAuthEnv>(async (c, next) => { | |
| 28 | // Try Bearer token first | |
| 29 | const authHeader = c.req.header("Authorization"); | |
| 30 | if (authHeader?.startsWith("Bearer ")) { | |
| 31 | const token = authHeader.slice(7); | |
| 32 | try { | |
| 33 | const hasher = new Bun.CryptoHasher("sha256"); | |
| 34 | hasher.update(token); | |
| 35 | const tokenHash = hasher.digest("hex"); | |
| 36 | ||
| 37 | const [apiToken] = await db | |
| 38 | .select() | |
| 39 | .from(apiTokens) | |
| 40 | .where(eq(apiTokens.tokenHash, tokenHash)) | |
| 41 | .limit(1); | |
| 42 | ||
| 43 | if (!apiToken) { | |
| 44 | return c.json({ error: "Invalid API token" }, 401); | |
| 45 | } | |
| 46 | ||
| 47 | // Check expiration | |
| 48 | if (apiToken.expiresAt && new Date(apiToken.expiresAt) < new Date()) { | |
| 49 | return c.json({ error: "API token expired" }, 401); | |
| 50 | } | |
| 51 | ||
| 52 | // Get user | |
| 53 | const [user] = await db | |
| 54 | .select() | |
| 55 | .from(users) | |
| 56 | .where(eq(users.id, apiToken.userId)) | |
| 57 | .limit(1); | |
| 58 | ||
| 59 | if (!user) { | |
| 60 | return c.json({ error: "Token owner not found" }, 401); | |
| 61 | } | |
| 62 | ||
| 63 | // Update last used | |
| 64 | await db | |
| 65 | .update(apiTokens) | |
| 66 | .set({ lastUsedAt: new Date() }) | |
| 67 | .where(eq(apiTokens.id, apiToken.id)); | |
| 68 | ||
| 69 | c.set("user", user); | |
| 70 | c.set("authMethod", "token"); | |
| 71 | c.set("tokenScopes", apiToken.scopes.split(",").map((s) => s.trim())); | |
| 72 | return next(); | |
| 73 | } catch { | |
| 74 | return c.json({ error: "Authentication failed" }, 401); | |
| 75 | } | |
| 76 | } | |
| 77 | ||
| 78 | // Fall back to session cookie | |
| 3e8f8e8 | 79 | try { |
| 80 | const sessionToken = getCookie(c, "session"); | |
| 81 | if (sessionToken) { | |
| 45e31d0 | 82 | const [session] = await db |
| 83 | .select() | |
| 84 | .from(sessions) | |
| 85 | .where(eq(sessions.token, sessionToken)) | |
| 86 | .limit(1); | |
| 87 | ||
| 88 | if (session && new Date(session.expiresAt) >= new Date()) { | |
| 89 | const [user] = await db | |
| 90 | .select() | |
| 91 | .from(users) | |
| 92 | .where(eq(users.id, session.userId)) | |
| 93 | .limit(1); | |
| 94 | ||
| 95 | if (user) { | |
| 96 | c.set("user", user); | |
| 97 | c.set("authMethod", "session"); | |
| 98 | c.set("tokenScopes", ["repo", "user", "admin"]); | |
| 99 | return next(); | |
| 100 | } | |
| 101 | } | |
| 102 | } | |
| 3e8f8e8 | 103 | } catch { |
| 104 | // DB unavailable — fall through to unauthenticated | |
| 45e31d0 | 105 | } |
| 106 | ||
| 107 | c.set("user", null); | |
| 108 | c.set("authMethod", "none"); | |
| 109 | c.set("tokenScopes", []); | |
| 110 | return next(); | |
| 111 | }); | |
| 112 | ||
| 113 | /** | |
| 114 | * Require authentication — returns 401 for API routes instead of redirecting. | |
| 115 | */ | |
| 116 | export const requireApiAuth = createMiddleware<ApiAuthEnv>(async (c, next) => { | |
| 117 | const user = c.get("user"); | |
| 118 | if (!user) { | |
| 119 | return c.json( | |
| 120 | { error: "Authentication required", hint: "Use Authorization: Bearer <token> header" }, | |
| 121 | 401 | |
| 122 | ); | |
| 123 | } | |
| 124 | return next(); | |
| 125 | }); | |
| 126 | ||
| 127 | /** | |
| 128 | * Require specific token scope. | |
| 129 | */ | |
| 130 | export function requireScope(scope: string) { | |
| 131 | return createMiddleware<ApiAuthEnv>(async (c, next) => { | |
| 132 | const scopes = c.get("tokenScopes") || []; | |
| 133 | if (!scopes.includes(scope) && !scopes.includes("admin")) { | |
| 134 | return c.json( | |
| 135 | { error: `Insufficient scope. Required: ${scope}` }, | |
| 136 | 403 | |
| 137 | ); | |
| 138 | } | |
| 139 | return next(); | |
| 140 | }); | |
| 141 | } |