From c83fe29ff37ba44598b3d7d1b347879695cc3cea Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 24 Jun 2026 17:54:45 +0000 Subject: [PATCH] test: cover subscription credential client contract Add a UI API client contract test for company-scoped subscription credentials and document BYO subscription caveats in the Docker guide. --- doc/DOCKER.md | 11 ++++ ui/src/api/subscriptionCredentials.test.ts | 58 ++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 ui/src/api/subscriptionCredentials.test.ts diff --git a/doc/DOCKER.md b/doc/DOCKER.md index bc1fae8cae9..745e7a426f7 100644 --- a/doc/DOCKER.md +++ b/doc/DOCKER.md @@ -156,6 +156,17 @@ Notes: - Without API keys, the app still runs normally. - Adapter environment checks in Paperclip will surface missing auth/CLI prerequisites. +## Bring Your Own Subscription + +Paperclip's subscription-credential flow is per user, not a company-shared secret store. Each employee links their own official Claude or Codex subscription material from the profile/settings UI, and Paperclip stores the material encrypted at rest while only showing redacted metadata back to the board. + +Keep these caveats in mind when you run a public deployment: + +- Do not ask one employee to paste another employee's subscription token or `auth.json` / `.credentials.json` file. +- Do not treat the linked credential as a shared service account. +- Runtime refreshes are written back to the same encrypted per-user record, so the stored copy stays authoritative after a CLI session rotates material. +- Live-provider checks still depend on the employee's local CLI login state or subscription account access; automated tests cover the storage and redaction paths, but they do not replace a real host-side login. + ## Podman Quadlet (systemd) The `docker/quadlet/` directory contains unit files to run Paperclip + PostgreSQL as systemd services via Podman Quadlet. diff --git a/ui/src/api/subscriptionCredentials.test.ts b/ui/src/api/subscriptionCredentials.test.ts new file mode 100644 index 00000000000..0e5f6ca682a --- /dev/null +++ b/ui/src/api/subscriptionCredentials.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it, vi } from "vitest"; + +const mockApi = vi.hoisted(() => ({ + get: vi.fn(), + post: vi.fn(), + put: vi.fn(), + delete: vi.fn(), +})); + +vi.mock("./client", () => ({ + api: mockApi, +})); + +import { subscriptionCredentialsApi } from "./subscriptionCredentials"; + +describe("subscriptionCredentialsApi", () => { + it("uses company-scoped endpoints and preserves the redacted client contract", async () => { + mockApi.get.mockResolvedValue([]); + mockApi.put.mockResolvedValue({ + id: "cred-1", + provider: "claude", + }); + mockApi.post.mockResolvedValue({ + id: "cred-1", + provider: "claude", + }); + mockApi.delete.mockResolvedValue(undefined); + + await subscriptionCredentialsApi.list("company-1"); + await subscriptionCredentialsApi.get("company-1", "cred-1"); + await subscriptionCredentialsApi.upsert("company-1", { + provider: "claude", + credentialKind: "claude_oauth_token", + material: "secret-token", + status: "active", + }); + await subscriptionCredentialsApi.recordTestResult("company-1", "cred-1", { + testStatus: "ready", + }); + await subscriptionCredentialsApi.remove("company-1", "cred-1"); + + expect(mockApi.get).toHaveBeenNthCalledWith(1, "/companies/company-1/subscription-credentials"); + expect(mockApi.get).toHaveBeenNthCalledWith(2, "/companies/company-1/subscription-credentials/cred-1"); + expect(mockApi.put).toHaveBeenCalledWith("/companies/company-1/subscription-credentials", { + provider: "claude", + credentialKind: "claude_oauth_token", + material: "secret-token", + status: "active", + }); + expect(mockApi.post).toHaveBeenCalledWith( + "/companies/company-1/subscription-credentials/cred-1/test-result", + { + testStatus: "ready", + }, + ); + expect(mockApi.delete).toHaveBeenCalledWith("/companies/company-1/subscription-credentials/cred-1"); + }); +});