Skip to content
File

Blob: src/worker/auth/gitAuth.ts

typescript97 lines
1import { createLogger } from "@/worker/common";
2import { createDb, type Db } from "@/worker/db/d1/client";
3import { updatePatLastUsedAt } from "@/worker/db/d1/dal/tokens";
4import type { Logger } from "@/worker/common/logger";
5import type { RepositoryRoute } from "@/worker/repositories/route";
6 
7import {
8 PAT_LAST_USED_READ_THROTTLE_MS,
9 shouldTouchPatLastUsedAt,
10 verifyPat,
11 type PatLastUsedOp,
12 type PatVerifyError,
13 type PatVerifyOk,
14} from "./pat";
15 
16// Decode `Authorization: Basic <b64>` into `{ username, password }`. The
17// caller decides whether the credentials are valid; this helper does no
18// authorization. Used by Git endpoints that accept PAT credentials over
19// HTTP Basic.
20export function getBasicCredentials(req: Request): { username: string; password: string } | null {
21 const header = req.headers.get("Authorization") || "";
22 const match = /^Basic\s+(.+)$/i.exec(header);
23 if (!match) return null;
24 try {
25 const decoded = atob(match[1]!);
26 const idx = decoded.indexOf(":");
27 if (idx === -1) return { username: decoded, password: "" };
28 return {
29 username: decoded.slice(0, idx),
30 password: decoded.slice(idx + 1),
31 };
32 } catch {
33 return null;
34 }
35}
36 
37// UI handlers must not import this module; it is the only path that reaches
38// `verifyPat`. The kept invariant: PAT credentials authorize git endpoints
39// only, never browser surfaces.
40export type GitAuthResult =
41 | { kind: "anonymous" }
42 | { kind: "missing-credentials" }
43 | { kind: "pat"; verified: PatVerifyOk }
44 | { kind: "pat-rejected"; reason: PatVerifyError["reason"] };
45 
46export async function authenticateGitRequest(
47 env: Env,
48 request: Request,
49 route: RepositoryRoute,
50 options: { db?: Db } = {}
51): Promise<GitAuthResult> {
52 const basic = getBasicCredentials(request);
53 if (!basic) return { kind: "anonymous" };
54 if (!basic.password) return { kind: "missing-credentials" };
55 const verified = await verifyPat(env, {
56 username: basic.username,
57 plaintext: basic.password,
58 namespaceId: route.namespaceId,
59 repositoryId: route.repositoryId,
60 db: options.db,
61 });
62 if (verified.ok) return { kind: "pat", verified };
63 return { kind: "pat-rejected", reason: verified.reason };
64}
65 
66// Returns true when the update was scheduled so tests can assert the
67// throttle decision without leaning on D1.
68export function scheduleTouchPatLastUsedAt(
69 env: Env,
70 ctx: ExecutionContext,
71 verified: PatVerifyOk,
72 op: PatLastUsedOp,
73 now: number = Date.now(),
74 options: { db?: Db; log?: Logger } = {}
75): boolean {
76 if (!shouldTouchPatLastUsedAt(verified.lastUsedAt, op, now)) return false;
77 const log = options.log ?? createLogger(env.LOG_LEVEL, { service: "GitAuth" });
78 const db = options.db ?? createDb(env.DB);
79 ctx.waitUntil(
80 (async () => {
81 try {
82 await updatePatLastUsedAt(db, verified.patId, now);
83 log.debug("pat:last-used-update-ok", { patId: verified.patId, op });
84 } catch (error) {
85 log.warn("pat:last-used-update-failed", {
86 patId: verified.patId,
87 op,
88 error: String(error),
89 });
90 }
91 })()
92 );
93 return true;
94}
95 
96export { PAT_LAST_USED_READ_THROTTLE_MS };