import { expect, test } from "bun:test"; import type { AuthUser, Feature, ServerContext } from "@stc/contracts"; import { createApp, resolveConfig } from "../src"; const owner: AuthUser = { id: "o", email: "o@x", name: "O", roles: ["owner"], groups: [] }; const ran: string[] = []; const feature: Feature = { name: "t", routes: { "/api/do": { POST: async () => { ran.push("do"); return Response.json({ ok: true }); }, DELETE: async () => { ran.push("del"); return Response.json({ ok: true }); }, GET: () => Response.json({ ok: true }) }, "/api/up": { POST: async () => { ran.push("up"); return Response.json({ ok: true }); } }, "/api/auth/*": { POST: async () => { ran.push("auth"); return Response.json({ ok: true }); } }, }, multipartRoutes: ["/api/up"], externalCallbacks: ["/api/auth/sso/saml2/sp/acs"], }; const config = resolveConfig({ dataDir: "/tmp/x", publicUrl: "https://courses.example.org", allowedOrigins: ["http://192.168.1.5:3000"] }); const app = createApp({ config, auth: { userFromRequest: async () => owner } } as unknown as ServerContext, [feature]); const send = (path: string, init: { method?: string; headers?: Record; body?: BodyInit } = {}) => app.fetch(new Request(`https://courses.example.org${path}`, { method: init.method ?? "POST", headers: init.headers, body: init.body })); const JSONH = { "content-type": "application/json" }; test("same-origin JSON writes run; GET needs no Origin", async () => { ran.length = 0; expect((await send("/api/do", { headers: { origin: "https://courses.example.org", ...JSONH }, body: "{}" })).status).toBe(200); expect((await send("/api/do", { headers: { origin: "http://192.168.1.5:3000", ...JSONH }, body: "{}" })).status).toBe(200); // STC_ALLOWED_ORIGINS expect((await send("/api/do", { headers: { referer: "https://courses.example.org/c/x", ...JSONH }, body: "{}" })).status).toBe(200); // Referer fallback expect((await send("/api/do", { method: "DELETE", headers: { origin: "https://courses.example.org" } })).status).toBe(200); // no body: no type needed expect((await send("/api/do", { method: "GET" })).status).toBe(200); expect(ran).toEqual(["do", "do", "do", "del"]); }); test("foreign, missing or opaque Origin is 403 and the handler never runs", async () => { ran.length = 0; for (const headers of []>[ { origin: "http://evil.example", "content-type": "text/plain" }, { origin: "http://evil.example", ...JSONH }, { origin: "https://courses.example.org.evil.example", ...JSONH }, { origin: "http://courses.example.org", ...JSONH }, // scheme differs { origin: "null", ...JSONH }, { referer: "http://evil.example/page", ...JSONH }, { ...JSONH }, // neither Origin nor Referer ]) { const r = await send("/api/do", { headers, body: "{}" }); expect([headers, r.status]).toEqual([headers, 403]); } expect((await send("/api/do", { method: "DELETE" })).status).toBe(403); expect(ran).toEqual([]); }); test("same-origin bodies must be JSON (multipart only on upload routes)", async () => { ran.length = 0; const o = { origin: "https://courses.example.org" }; for (const type of ["text/plain", "application/x-www-form-urlencoded", "multipart/form-data; boundary=x", ""]) { expect([type, (await send("/api/do", { headers: { ...o, ...(type ? { "content-type": type } : {}) }, body: "a=1" })).status]).toEqual([type, 403]); } const fd = new FormData(); fd.set("file", new File(["x"], "a.txt")); expect((await send("/api/up", { headers: o, body: fd })).status).toBe(200); expect((await send("/api/up", { headers: { origin: "http://evil.example" }, body: fd })).status).toBe(403); expect((await send("/api/up", { headers: { ...o, "content-type": "text/plain" }, body: "x" })).status).toBe(403); expect(ran).toEqual(["up"]); }); test("Better Auth routes are guarded too, except declared identity-provider callbacks", async () => { ran.length = 0; expect((await send("/api/auth/sign-in/email", { headers: { origin: "http://evil.example", ...JSONH }, body: "{}" })).status).toBe(403); expect((await send("/api/auth/sign-in/email", { headers: { origin: "https://courses.example.org", ...JSONH }, body: "{}" })).status).toBe(200); // a SAML IdP posts a form from its own origin; Better Auth verifies the signed assertion expect((await send("/api/auth/sso/saml2/sp/acs/idp1", { headers: { origin: "https://idp.example", "content-type": "application/x-www-form-urlencoded" }, body: "SAMLResponse=x" })).status).toBe(200); expect(ran).toEqual(["auth", "auth"]); }); test("no configuration means every write is refused", async () => { const bare = createApp({ auth: { userFromRequest: async () => owner } } as unknown as ServerContext, [feature]); const r = await bare.fetch(new Request("http://x/api/do", { method: "POST", headers: { origin: "http://x", ...JSONH }, body: "{}" })); expect(r.status).toBe(403); }); test("loadConfig reads STC_ALLOWED_ORIGINS (and STC_TRUSTED_ORIGINS) as origins", async () => { const { loadConfig } = await import("../src"); const c = loadConfig({ STC_SECRET: "s", STC_PUBLIC_URL: "https://Courses.Example.org/app/", STC_ALLOWED_ORIGINS: "http://lan:3000/, junk", STC_TRUSTED_ORIGINS: "https://b.example" }); expect(c.allowedOrigins).toEqual(["https://courses.example.org", "http://lan:3000", "https://b.example"]); });