Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
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.

api-auth.tsBlame141 lines · 1 contributor
45e31d0Claude1/**
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
8import { createMiddleware } from "hono/factory";
9import { getCookie } from "hono/cookie";
10import { eq, gt } from "drizzle-orm";
11import { db } from "../db";
12import { apiTokens, sessions, users } from "../db/schema";
13import type { User } from "../db/schema";
14
15export 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 */
27export 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
3e8f8e8Claude79 try {
80 const sessionToken = getCookie(c, "session");
81 if (sessionToken) {
45e31d0Claude82 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 }
3e8f8e8Claude103 } catch {
104 // DB unavailable — fall through to unauthenticated
45e31d0Claude105 }
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 */
116export 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 */
130export 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}