Skip to content

Commit af0ef44

Browse files
docs: align auth, CI API, collections, and dotenv Keys with code (#9)
- 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 Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent 037f03e commit af0ef44

File tree

5 files changed

+57
-14
lines changed

5 files changed

+57
-14
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@ For a **non-`.env` file**, `pick` must be a **single** name; the file contents a
9191

9292
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`.
9393

94+
### CI HTTP endpoint (custom integrations)
95+
96+
The composite action calls **`GET {hostname}/api/ci/file?secret={collection}/{relative/path}`** with header **`Authorization: Bearer <token>`**. On success the body is JSON `{ "content": "<decrypted file string>" }`. 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).
97+
9498
## Prerequisites
9599

96100
- Node.js 20+

docs/api-trpc.md

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,24 @@
22

33
Base URL: `/api/trpc` (SuperJSON transformer enabled).
44

5-
All procedures under `collections`, `objects`, and `secrets` use **`protectedProcedure`**: unauthenticated requests return `UNAUTHORIZED`.
5+
Procedures under `collections`, `objects`, `secrets`, and `accessTokens` use **`protectedProcedure`** unless noted: unauthenticated requests return `UNAUTHORIZED`.
66

77
## `collections`
88

9-
| Procedure | Input | Result |
9+
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).
10+
11+
| Procedure | Input | Result / notes |
1012
| --- | --- | --- |
11-
| `list` || `{ slug: string }[]` — top-level collection folders under `S3_ROOT_PREFIX`. |
12-
| `exists` | `{ slug: string }` | `boolean` — whether `{root}{slug}/` appears as a common prefix (optional UX helper). |
13+
| `accessMeta` | `{ slug: string }` | `{ canManageAccess, canRenameDelete }` for UI. |
14+
| `list` || `{ slug: string }[]` under `S3_ROOT_PREFIX`. |
15+
| `exists` | `{ slug: string }` | `boolean` — prefix exists in S3 and caller may access it. |
16+
| `create` | `{ slug: string }` | Creates DB row + S3 placeholder under prefix; `CONFLICT` if prefix already used. |
17+
| `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). |
18+
| `rename` | `{ fromSlug, toSlug }` | Copies all objects to the new prefix, deletes old keys, updates DB slug. Same permission rules as `delete`. |
19+
| `listGrants` | `{ slug: string }` | **`ownerProcedure`** — emails granted access to the collection. |
20+
| `listDomainUsers` || **`ownerProcedure`** — users in `ALLOWED_EMAIL_DOMAIN` (for grant picker). |
21+
| `setGrant` | `{ slug, userEmail }` | **`ownerProcedure`** — upserts `collection_access` for that user. |
22+
| `revokeGrant` | `{ slug, userEmail }` | **`ownerProcedure`** — removes grant. |
1323

1424
## `objects`
1525

@@ -20,6 +30,7 @@ All procedures under `collections`, `objects`, and `secrets` use **`protectedPro
2030
| `getByPath` | `{ collectionSlug, relativePath }` | Same shape as `get`, builds key via [`fullObjectKey`](../src/lib/paths.ts). |
2131
| `put` | `{ objectKey, content: string }` | Encrypts UTF-8 and overwrites S3 object. |
2232
| `putByPath` | `{ collectionSlug, relativePath, content }` | Same as `put`; returns `{ ok, objectKey }`. |
33+
| `delete` | `{ objectKey: string }` | Deletes the object in S3. Requires collection access (`FORBIDDEN` if none). |
2334

2435
## `secrets`
2536

@@ -28,10 +39,24 @@ All procedures under `collections`, `objects`, and `secrets` use **`protectedPro
2839
| `parse` | `{ objectKey: string }` | `{ objectKey, entries: { key, value }[] }` after decrypt + dotenv parse. |
2940
| `getValue` | `{ objectKey, secretKey: string }` | `{ objectKey, secretKey, value }`. `NOT_FOUND` if key missing. |
3041

42+
## `accessTokens`
43+
44+
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.
45+
46+
| Procedure | Input | Result / notes |
47+
| --- | --- | --- |
48+
| `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). |
49+
| `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**). |
50+
| `createForCollectionSlug` | `{ slug: string, name?: string }` | Same as `create` for one collection, keyed by slug. |
51+
| `revoke` | `{ id: string }` | Deletes token. **Only** the creator or an [owner](../src/lib/owners.ts) email. |
52+
| `reveal` | `{ id: string }` | Returns `{ token }` decrypted from DB. Same permission as `revoke`. |
53+
3154
## Errors
3255

3356
- `UNAUTHORIZED` — no session.
57+
- `FORBIDDEN` — no collection access, or not allowed to manage tokens/grants.
3458
- `BAD_REQUEST` — decrypt failure or invalid payload.
35-
- `NOT_FOUND` — dotenv key missing (`secrets.getValue`).
59+
- `NOT_FOUND` — dotenv key missing (`secrets.getValue`), or missing collection/token where applicable.
60+
- `CONFLICT` — duplicate collection slug on create, or rename target already in use.
3661

3762
Standard Zod validation errors are attached to tRPC error `data.zodError` in development-oriented clients.

docs/architecture.md

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,43 +8,48 @@ flowchart LR
88
UI[React_shadcn]
99
end
1010
subgraph next [Nextjs_App_Router]
11-
Auth[Authjs_Google_Prisma]
11+
Auth[Authjs_Google_JWT]
1212
TRPC[tRPC_Router]
13+
CI[GET_api_ci_file]
1314
Crypto[AES256GCM]
1415
end
1516
subgraph aws [AWS]
1617
S3[S3_bucket]
1718
end
1819
subgraph db [Postgres]
19-
Prisma[Prisma_User_Session]
20+
Prisma[Prisma_users_collections_tokens]
2021
end
2122
UI --> Auth
2223
UI --> TRPC
2324
TRPC --> Crypto
2425
TRPC --> S3
2526
Auth --> Prisma
27+
CI --> Prisma
28+
CI --> Crypto
29+
CI --> S3
2630
```
2731

2832
## Request flow
2933

30-
1. User hits `/login`, completes Google OAuth. Auth.js persists `User`, `Account`, and `Session` rows via Prisma.
34+
1. User hits `/login`, completes Google OAuth. Auth.js issues a **JWT** session; Prisma **upserts** a `users` row for ACLs.
3135
2. The `signIn` callback rejects sign-ins whose email is not on `ALLOWED_EMAIL_DOMAIN`.
3236
3. Authenticated users use tRPC (`/api/trpc`) from React Query. All vault procedures are **protected** and require a session.
3337
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).
38+
5. **CI / GitHub Action**: [`src/app/api/ci/file/route.ts`](../src/app/api/ci/file/route.ts) accepts `Authorization: Bearer <access_token>` 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)).
3439

3540
## Trust boundaries
3641

3742
| Component | Trust |
3843
| --- | --- |
3944
| `ENCRYPTION_KEY` | Server-only. Anyone who holds it can decrypt all vault objects. |
4045
| AWS IAM credentials | Server (and operators running the CLI). Scope to `S3_ROOT_PREFIX` when possible. |
41-
| PostgreSQL | Session and OAuth linkage; does not store secret file contents. |
46+
| PostgreSQL | Users, collection metadata, grants, and **hashed** access tokens for CI; does not store secret **file** contents (those live in S3). |
4247
| Browser | Sees decrypted content after successful auth and TLS. |
4348

4449
## Source of truth
4550

46-
- **S3** is the source of truth for secret files. The app does not mirror the bucket tree in Postgres.
47-
- **Postgres** stores Auth.js tables only.
51+
- **S3** is the source of truth for secret **file blobs**. The app does not mirror object listings or contents in Postgres.
52+
- **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).
4853

4954
## Scaling notes
5055

docs/auth.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Provider
44

5-
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).
5+
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).
66

77
## Routes
88

@@ -23,9 +23,12 @@ user.email.toLowerCase().endsWith(`@${ALLOWED_EMAIL_DOMAIN}`)
2323

2424
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.
2525

26-
## Sessions
26+
## Sessions and database users
2727

28-
Sessions are stored in PostgreSQL (`Session` model) because the Prisma adapter is enabled. Protected tRPC procedures require `ctx.session.user` from `auth()`.
28+
- **Sessions**: JWT cookies. `auth()` resolves `session.user.id` and `session.user.isOwner` without reading a session table.
29+
- **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.
30+
31+
Protected tRPC procedures require `ctx.session.user` from `auth()`.
2932

3033
## Vault route protection
3134

docs/storage-and-encryption.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ The CLI ([`scripts/pull-secret.mjs`](../scripts/pull-secret.mjs)) implements the
3939

4040
There is no separate per-key storage in S3: “line items” are a **view** over the decrypted file.
4141

42+
### Keys view: add, remove, save
43+
44+
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.
45+
46+
**Copy** uses `secrets.getValue` (server-side decrypt + parse) so the clipboard gets the value only, not surrounding file text.
47+
4248
## Security notes
4349

4450
- S3 at-rest encryption (SSE-S3 or SSE-KMS) is recommended in addition to application-layer encryption.

0 commit comments

Comments
 (0)