From c4a1b167a1d2a9734f2e9b091e77c26cf8e39d33 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Jul 2026 07:34:48 -0700 Subject: [PATCH] feat: resolve attachment user through cfw-auth binding Co-Authored-By: Claude --- src/auth-session.ts | 37 +++++++++++++++++++++++++++++++++++++ src/problem.ts | 33 +++++++++++++++++++++++++++++++++ tests/auth-session.test.ts | 27 +++++++++++++++++++++++++++ 3 files changed, 97 insertions(+) create mode 100644 src/auth-session.ts create mode 100644 src/problem.ts create mode 100644 tests/auth-session.test.ts diff --git a/src/auth-session.ts b/src/auth-session.ts new file mode 100644 index 0000000..b673981 --- /dev/null +++ b/src/auth-session.ts @@ -0,0 +1,37 @@ +import type { Env } from "./env"; +import { HttpProblem } from "./problem"; + +export interface CurrentUser { + id: string; + email?: string; +} + +interface BetterAuthSessionResponse { + user?: { + id?: unknown; + email?: unknown; + }; +} + +export async function resolveCurrentUser(request: Request, env: Env): Promise { + const sessionRequest = new Request("https://cfw-auth.internal/api/auth/get-session", { + method: "GET", + headers: request.headers, + }); + + const response = await env.AUTH.fetch(sessionRequest); + if (!response.ok) { + throw new HttpProblem(401, "authentication-required", "Authentication required"); + } + + const body = (await response.json()) as BetterAuthSessionResponse | null; + const id = body?.user?.id; + if (typeof id !== "string" || id.length === 0) { + throw new HttpProblem(401, "authentication-required", "Authentication required"); + } + + return { + id, + email: typeof body?.user?.email === "string" ? body.user.email : undefined, + }; +} diff --git a/src/problem.ts b/src/problem.ts new file mode 100644 index 0000000..76a7e13 --- /dev/null +++ b/src/problem.ts @@ -0,0 +1,33 @@ +export class HttpProblem extends Error { + constructor( + readonly status: number, + readonly code: string, + message: string, + readonly detail: Record = {}, + ) { + super(message); + } +} + +export function problemResponse(error: unknown): Response { + const problem = + error instanceof HttpProblem + ? error + : new HttpProblem(500, "internal-error", "Internal server error"); + + return Response.json( + { + type: `https://cfw-attachment.bowong.cc/problems/${problem.code}`, + title: problem.message, + status: problem.status, + detail: problem.detail, + code: problem.code, + }, + { + status: problem.status, + headers: { + "content-type": "application/problem+json", + }, + }, + ); +} diff --git a/tests/auth-session.test.ts b/tests/auth-session.test.ts new file mode 100644 index 0000000..a032420 --- /dev/null +++ b/tests/auth-session.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it, vi } from "vitest"; +import { resolveCurrentUser } from "../src/auth-session"; +import type { Env } from "../src/env"; + +function envWithAuth(response: Response): Env { + return { + AUTH: { fetch: vi.fn().mockResolvedValue(response) }, + DB: {} as D1Database, + ATTACHMENTS: {} as R2Bucket, + }; +} + +describe("resolveCurrentUser", () => { + it("returns the Better Auth user id from cfw-auth", async () => { + const env = envWithAuth(Response.json({ user: { id: "user_123", email: "a@example.com" } })); + + await expect(resolveCurrentUser(new Request("https://attachment.local/api/attachments"), env)) + .resolves.toEqual({ id: "user_123", email: "a@example.com" }); + }); + + it("throws authentication-required when cfw-auth has no session", async () => { + const env = envWithAuth(Response.json(null)); + + await expect(resolveCurrentUser(new Request("https://attachment.local/api/attachments"), env)) + .rejects.toMatchObject({ status: 401, code: "authentication-required" }); + }); +});