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.tsBlame153 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);
e75eddcClaude32
33 // Agent-multiplayer v1: `agentAuth` middleware (when mounted earlier
34 // in the chain) handles `agt_` tokens. Skip them here so we don't
35 // wrongly 401 a valid agent token. authMethod stays "none" because
36 // the agent context lives at c.get("agent"), not c.get("user").
37 if (token.startsWith("agt_")) {
38 c.set("user", null);
39 c.set("authMethod", "none");
40 c.set("tokenScopes", []);
41 return next();
42 }
43
45e31d0Claude44 try {
45 const hasher = new Bun.CryptoHasher("sha256");
46 hasher.update(token);
47 const tokenHash = hasher.digest("hex");
48
49 const [apiToken] = await db
50 .select()
51 .from(apiTokens)
52 .where(eq(apiTokens.tokenHash, tokenHash))
53 .limit(1);
54
55 if (!apiToken) {
56 return c.json({ error: "Invalid API token" }, 401);
57 }
58
59 // Check expiration
60 if (apiToken.expiresAt && new Date(apiToken.expiresAt) < new Date()) {
61 return c.json({ error: "API token expired" }, 401);
62 }
63
64 // Get user
65 const [user] = await db
66 .select()
67 .from(users)
68 .where(eq(users.id, apiToken.userId))
69 .limit(1);
70
71 if (!user) {
72 return c.json({ error: "Token owner not found" }, 401);
73 }
74
75 // Update last used
76 await db
77 .update(apiTokens)
78 .set({ lastUsedAt: new Date() })
79 .where(eq(apiTokens.id, apiToken.id));
80
81 c.set("user", user);
82 c.set("authMethod", "token");
83 c.set("tokenScopes", apiToken.scopes.split(",").map((s) => s.trim()));
84 return next();
85 } catch {
86 return c.json({ error: "Authentication failed" }, 401);
87 }
88 }
89
90 // Fall back to session cookie
3e8f8e8Claude91 try {
92 const sessionToken = getCookie(c, "session");
93 if (sessionToken) {
45e31d0Claude94 const [session] = await db
95 .select()
96 .from(sessions)
97 .where(eq(sessions.token, sessionToken))
98 .limit(1);
99
100 if (session && new Date(session.expiresAt) >= new Date()) {
101 const [user] = await db
102 .select()
103 .from(users)
104 .where(eq(users.id, session.userId))
105 .limit(1);
106
107 if (user) {
108 c.set("user", user);
109 c.set("authMethod", "session");
110 c.set("tokenScopes", ["repo", "user", "admin"]);
111 return next();
112 }
113 }
114 }
3e8f8e8Claude115 } catch {
116 // DB unavailable — fall through to unauthenticated
45e31d0Claude117 }
118
119 c.set("user", null);
120 c.set("authMethod", "none");
121 c.set("tokenScopes", []);
122 return next();
123});
124
125/**
126 * Require authentication — returns 401 for API routes instead of redirecting.
127 */
128export const requireApiAuth = createMiddleware<ApiAuthEnv>(async (c, next) => {
129 const user = c.get("user");
130 if (!user) {
131 return c.json(
132 { error: "Authentication required", hint: "Use Authorization: Bearer <token> header" },
133 401
134 );
135 }
136 return next();
137});
138
139/**
140 * Require specific token scope.
141 */
142export function requireScope(scope: string) {
143 return createMiddleware<ApiAuthEnv>(async (c, next) => {
144 const scopes = c.get("tokenScopes") || [];
145 if (!scopes.includes(scope) && !scopes.includes("admin")) {
146 return c.json(
147 { error: `Insufficient scope. Required: ${scope}` },
148 403
149 );
150 }
151 return next();
152 });
153}