From b286badf6ca4cb0afd5a08479defae2fb6dc6d97 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 5 Apr 2026 16:03:31 +0000 Subject: [PATCH] docs: align auth, CI API, collections, and dotenv Keys with code - Fix auth.md: JWT sessions and user upsert; remove obsolete Prisma adapter/session tables - Extend architecture.md: Postgres metadata, CI route, updated diagram - Document objects.delete, full collections and accessTokens routers in api-trpc.md - Describe Keys view add/remove/save in storage-and-encryption.md - Add GET /api/ci/file contract to README for custom CI integrations --- README.md | 4 ++++ docs/api-trpc.md | 35 +++++++++++++++++++++++++++++----- docs/architecture.md | 17 +++++++++++------ docs/auth.md | 9 ++++++--- docs/storage-and-encryption.md | 6 ++++++ 5 files changed, 57 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 4135104..2a9002e 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,10 @@ For a **non-`.env` file**, `pick` must be a **single** name; the file contents a If you develop the action from a clone of [barecheck/cerberus](https://github.com/barecheck/cerberus), rebuild the bundled entrypoint after changing action source: `npm run build:github-action`. +### CI HTTP endpoint (custom integrations) + +The composite action calls **`GET {hostname}/api/ci/file?secret={collection}/{relative/path}`** with header **`Authorization: Bearer `**. On success the body is JSON `{ "content": "" }`. The token must be allowed for the **collection** (first path segment); `secret` must include a non-empty path after that segment. Typical failures: `401` (missing/invalid bearer), `403` (token not scoped to that collection), `404` (unknown collection, missing object, or decrypt error — intentionally vague). Implementation: [`src/app/api/ci/file/route.ts`](src/app/api/ci/file/route.ts). + ## Prerequisites - Node.js 20+ diff --git a/docs/api-trpc.md b/docs/api-trpc.md index 6f4dad0..c0553d6 100644 --- a/docs/api-trpc.md +++ b/docs/api-trpc.md @@ -2,14 +2,24 @@ Base URL: `/api/trpc` (SuperJSON transformer enabled). -All procedures under `collections`, `objects`, and `secrets` use **`protectedProcedure`**: unauthenticated requests return `UNAUTHORIZED`. +Procedures under `collections`, `objects`, `secrets`, and `accessTokens` use **`protectedProcedure`** unless noted: unauthenticated requests return `UNAUTHORIZED`. ## `collections` -| Procedure | Input | Result | +Defined in [`src/server/trpc/routers/collections.ts`](../src/server/trpc/routers/collections.ts). **Owners** (see [`src/lib/owners.ts`](../src/lib/owners.ts)) see every S3 prefix as a collection; other users only see collections they **created** or have a **grant** for (and only if the S3 prefix still has objects). + +| Procedure | Input | Result / notes | | --- | --- | --- | -| `list` | — | `{ slug: string }[]` — top-level collection folders under `S3_ROOT_PREFIX`. | -| `exists` | `{ slug: string }` | `boolean` — whether `{root}{slug}/` appears as a common prefix (optional UX helper). | +| `accessMeta` | `{ slug: string }` | `{ canManageAccess, canRenameDelete }` for UI. | +| `list` | — | `{ slug: string }[]` under `S3_ROOT_PREFIX`. | +| `exists` | `{ slug: string }` | `boolean` — prefix exists in S3 and caller may access it. | +| `create` | `{ slug: string }` | Creates DB row + S3 placeholder under prefix; `CONFLICT` if prefix already used. | +| `delete` | `{ slug: string }` | Deletes all objects under the collection prefix and the DB row. Requires creator, grant, or owner per [`canRenameOrDeleteCollection`](../src/server/access/collections.ts). | +| `rename` | `{ fromSlug, toSlug }` | Copies all objects to the new prefix, deletes old keys, updates DB slug. Same permission rules as `delete`. | +| `listGrants` | `{ slug: string }` | **`ownerProcedure`** — emails granted access to the collection. | +| `listDomainUsers` | — | **`ownerProcedure`** — users in `ALLOWED_EMAIL_DOMAIN` (for grant picker). | +| `setGrant` | `{ slug, userEmail }` | **`ownerProcedure`** — upserts `collection_access` for that user. | +| `revokeGrant` | `{ slug, userEmail }` | **`ownerProcedure`** — removes grant. | ## `objects` @@ -20,6 +30,7 @@ All procedures under `collections`, `objects`, and `secrets` use **`protectedPro | `getByPath` | `{ collectionSlug, relativePath }` | Same shape as `get`, builds key via [`fullObjectKey`](../src/lib/paths.ts). | | `put` | `{ objectKey, content: string }` | Encrypts UTF-8 and overwrites S3 object. | | `putByPath` | `{ collectionSlug, relativePath, content }` | Same as `put`; returns `{ ok, objectKey }`. | +| `delete` | `{ objectKey: string }` | Deletes the object in S3. Requires collection access (`FORBIDDEN` if none). | ## `secrets` @@ -28,10 +39,24 @@ All procedures under `collections`, `objects`, and `secrets` use **`protectedPro | `parse` | `{ objectKey: string }` | `{ objectKey, entries: { key, value }[] }` after decrypt + dotenv parse. | | `getValue` | `{ objectKey, secretKey: string }` | `{ objectKey, secretKey, value }`. `NOT_FOUND` if key missing. | +## `accessTokens` + +Implements collection-scoped **CI bearer tokens** stored in Postgres ([`src/server/trpc/routers/accessTokens.ts`](../src/server/trpc/routers/accessTokens.ts)). The plaintext secret is shown **once** on create; [`src/app/api/ci/file/route.ts`](../src/app/api/ci/file/route.ts) accepts only a **hash** of the bearer value for lookup. + +| Procedure | Input | Result / notes | +| --- | --- | --- | +| `list` | `{ slug?: string }` optional | Tokens the caller may see: any token linked to a collection they can access; optional `slug` filters to tokens tied to that collection. Rows include `displayToken` (masked), `collectionSlugs`, `canManage` (creator or owner). | +| `create` | `{ name?: string, collectionIds: string[] }` | Creates token scoped to those collections. Caller must have access to every `collectionId`. Returns `{ id, token }` (**plaintext `token` — store immediately**). | +| `createForCollectionSlug` | `{ slug: string, name?: string }` | Same as `create` for one collection, keyed by slug. | +| `revoke` | `{ id: string }` | Deletes token. **Only** the creator or an [owner](../src/lib/owners.ts) email. | +| `reveal` | `{ id: string }` | Returns `{ token }` decrypted from DB. Same permission as `revoke`. | + ## Errors - `UNAUTHORIZED` — no session. +- `FORBIDDEN` — no collection access, or not allowed to manage tokens/grants. - `BAD_REQUEST` — decrypt failure or invalid payload. -- `NOT_FOUND` — dotenv key missing (`secrets.getValue`). +- `NOT_FOUND` — dotenv key missing (`secrets.getValue`), or missing collection/token where applicable. +- `CONFLICT` — duplicate collection slug on create, or rename target already in use. Standard Zod validation errors are attached to tRPC error `data.zodError` in development-oriented clients. diff --git a/docs/architecture.md b/docs/architecture.md index 6bf669d..9735ca6 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -8,29 +8,34 @@ flowchart LR UI[React_shadcn] end subgraph next [Nextjs_App_Router] - Auth[Authjs_Google_Prisma] + Auth[Authjs_Google_JWT] TRPC[tRPC_Router] + CI[GET_api_ci_file] Crypto[AES256GCM] end subgraph aws [AWS] S3[S3_bucket] end subgraph db [Postgres] - Prisma[Prisma_User_Session] + Prisma[Prisma_users_collections_tokens] end UI --> Auth UI --> TRPC TRPC --> Crypto TRPC --> S3 Auth --> Prisma + CI --> Prisma + CI --> Crypto + CI --> S3 ``` ## Request flow -1. User hits `/login`, completes Google OAuth. Auth.js persists `User`, `Account`, and `Session` rows via Prisma. +1. User hits `/login`, completes Google OAuth. Auth.js issues a **JWT** session; Prisma **upserts** a `users` row for ACLs. 2. The `signIn` callback rejects sign-ins whose email is not on `ALLOWED_EMAIL_DOMAIN`. 3. Authenticated users use tRPC (`/api/trpc`) from React Query. All vault procedures are **protected** and require a session. 4. For reads/writes, the server downloads or uploads S3 objects. Payloads are **decrypted only in memory** on the server using `ENCRYPTION_KEY`, then returned to the browser (plaintext in transit must be protected with TLS in production). +5. **CI / GitHub Action**: [`src/app/api/ci/file/route.ts`](../src/app/api/ci/file/route.ts) accepts `Authorization: Bearer ` and `?secret=collection/relative/path`, verifies the token against Postgres (`access_tokens` + collection scope), then returns decrypted file content as JSON. The bundled composite action calls this route (see [README](../README.md) and [`action.yml`](../action.yml)). ## Trust boundaries @@ -38,13 +43,13 @@ flowchart LR | --- | --- | | `ENCRYPTION_KEY` | Server-only. Anyone who holds it can decrypt all vault objects. | | AWS IAM credentials | Server (and operators running the CLI). Scope to `S3_ROOT_PREFIX` when possible. | -| PostgreSQL | Session and OAuth linkage; does not store secret file contents. | +| PostgreSQL | Users, collection metadata, grants, and **hashed** access tokens for CI; does not store secret **file** contents (those live in S3). | | Browser | Sees decrypted content after successful auth and TLS. | ## Source of truth -- **S3** is the source of truth for secret files. The app does not mirror the bucket tree in Postgres. -- **Postgres** stores Auth.js tables only. +- **S3** is the source of truth for secret **file blobs**. The app does not mirror object listings or contents in Postgres. +- **Postgres** stores users, per-collection access grants, collection rows (for rename/delete and grants), and CI access tokens (lookup hash + encrypted secret for optional reveal in the UI). ## Scaling notes diff --git a/docs/auth.md b/docs/auth.md index ba862df..d27df5e 100644 --- a/docs/auth.md +++ b/docs/auth.md @@ -2,7 +2,7 @@ ## Provider -Cerberus uses **Google** OAuth via [Auth.js v5](https://authjs.dev/) (`next-auth` beta) and the [Prisma adapter](https://authjs.dev/getting-started/adapters/prisma). +Cerberus uses **Google** OAuth via [Auth.js v5](https://authjs.dev/) (`next-auth` beta). The app uses a **JWT session strategy** (no Auth.js `Session` / `Account` tables in Prisma). ## Routes @@ -23,9 +23,12 @@ user.email.toLowerCase().endsWith(`@${ALLOWED_EMAIL_DOMAIN}`) Set `ALLOWED_EMAIL_DOMAIN` to your workspace domain (e.g. `acme.com`). Subdomains are **not** treated specially: `user@mail.acme.com` matches `acme.com`; adjust the callback if you need `endsWith` behavior for exact host parts only. -## Sessions +## Sessions and database users -Sessions are stored in PostgreSQL (`Session` model) because the Prisma adapter is enabled. Protected tRPC procedures require `ctx.session.user` from `auth()`. +- **Sessions**: JWT cookies. `auth()` resolves `session.user.id` and `session.user.isOwner` without reading a session table. +- **Users**: On first sign-in, [`src/auth.ts`](../src/auth.ts) **upserts** a row in PostgreSQL (`users`) so vault ACLs and access tokens can reference stable user IDs. + +Protected tRPC procedures require `ctx.session.user` from `auth()`. ## Vault route protection diff --git a/docs/storage-and-encryption.md b/docs/storage-and-encryption.md index 8cb59a1..4b0193a 100644 --- a/docs/storage-and-encryption.md +++ b/docs/storage-and-encryption.md @@ -39,6 +39,12 @@ The CLI ([`scripts/pull-secret.mjs`](../scripts/pull-secret.mjs)) implements the There is no separate per-key storage in S3: “line items” are a **view** over the decrypted file. +### Keys view: add, remove, save + +In [`src/app/vault/[slug]/file/file-workspace.tsx`](../src/app/vault/[slug]/file/file-workspace.tsx), **Add** appends a new `KEY=value` line using [`appendDotenvKey`](../src/lib/dotenv-parse.ts): duplicate keys (as the parser would see them), keys containing `=`, keys starting with `#`, and values containing line breaks are rejected. **Remove** strips every line the parser would attribute to that key via [`removeDotenvKey`](../src/lib/dotenv-parse.ts) (comments and blank lines stay). Changes live in a **draft** until **Save** runs `objects.put` and overwrites the whole object in S3. + +**Copy** uses `secrets.getValue` (server-side decrypt + parse) so the clipboard gets the value only, not surrounding file text. + ## Security notes - S3 at-rest encryption (SSE-S3 or SSE-KMS) is recommended in addition to application-layer encryption.