From af7a74b4facdc72dc2d016041877f8e879da7321 Mon Sep 17 00:00:00 2001 From: Dave Date: Mon, 31 Aug 2026 01:30:52 +0200 Subject: [PATCH] feat(pos): add assertPosAuth for POS_API_KEY-gated routes Co-Authored-By: Claude --- src/lib/server/pos-auth.test.ts | 53 +++++++++++++++++++++++++++++++++ src/lib/server/pos-auth.ts | 22 ++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 src/lib/server/pos-auth.test.ts create mode 100644 src/lib/server/pos-auth.ts diff --git a/src/lib/server/pos-auth.test.ts b/src/lib/server/pos-auth.test.ts new file mode 100644 index 0000000..315b93b --- /dev/null +++ b/src/lib/server/pos-auth.test.ts @@ -0,0 +1,53 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { NextRequest } from "next/server"; +import { assertPosAuth } from "./pos-auth"; + +describe("assertPosAuth", () => { + const ORIGINAL_KEY = process.env.POS_API_KEY; + + beforeEach(() => { + process.env.POS_API_KEY = "test-pos-secret"; + }); + + afterEach(() => { + if (ORIGINAL_KEY === undefined) { + delete process.env.POS_API_KEY; + } else { + process.env.POS_API_KEY = ORIGINAL_KEY; + } + }); + + function reqWith(authHeader: string | null) { + const headers: Record = {}; + if (authHeader !== null) headers.authorization = authHeader; + return new NextRequest("http://localhost/api/v1/pos/catalog", { headers }); + } + + it("throws when POS_API_KEY env var is missing", () => { + delete process.env.POS_API_KEY; + expect(() => assertPosAuth(reqWith("Bearer test-pos-secret"))).toThrow( + /POS_API_KEY is not configured/ + ); + }); + + it("accepts a matching bearer token", () => { + expect(() => assertPosAuth(reqWith("Bearer test-pos-secret"))).not.toThrow(); + }); + + it("rejects a wrong bearer token", () => { + expect(() => assertPosAuth(reqWith("Bearer wrong"))).toThrow(/Unauthorized/); + }); + + it("rejects a request with no Authorization header", () => { + expect(() => assertPosAuth(reqWith(null))).toThrow(/Unauthorized/); + }); + + it("tags the thrown error with status 401", () => { + try { + assertPosAuth(reqWith("Bearer wrong")); + throw new Error("expected to throw"); + } catch (err) { + expect((err as Error & { status?: number }).status).toBe(401); + } + }); +}); diff --git a/src/lib/server/pos-auth.ts b/src/lib/server/pos-auth.ts new file mode 100644 index 0000000..5bc56c7 --- /dev/null +++ b/src/lib/server/pos-auth.ts @@ -0,0 +1,22 @@ +import { NextRequest } from "next/server"; + +/** + * Device-scoped secret, distinct from AGENT_API_KEY — a POS device + * only needs checkout/catalog/sales access, not the full agent API + * surface, so it gets its own key rather than reusing the agent's. + */ +export function assertPosAuth(req: NextRequest) { + const expected = process.env.POS_API_KEY; + if (!expected) { + throw new Error("POS_API_KEY is not configured."); + } + + const authHeader = req.headers.get("authorization") || ""; + const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : ""; + + if (token !== expected) { + const error = new Error("Unauthorized"); + (error as Error & { status?: number }).status = 401; + throw error; + } +}