You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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>
Copy file name to clipboardExpand all lines: README.md
+4Lines changed: 4 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -91,6 +91,10 @@ For a **non-`.env` file**, `pick` must be a **single** name; the file contents a
91
91
92
92
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`.
93
93
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).
Copy file name to clipboardExpand all lines: docs/api-trpc.md
+30-5Lines changed: 30 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,14 +2,24 @@
2
2
3
3
Base URL: `/api/trpc` (SuperJSON transformer enabled).
4
4
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`.
6
6
7
7
## `collections`
8
8
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).
|`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. |
|`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). |
23
34
24
35
## `secrets`
25
36
@@ -28,10 +39,24 @@ All procedures under `collections`, `objects`, and `secrets` use **`protectedPro
28
39
|`parse`|`{ objectKey: string }`|`{ objectKey, entries: { key, value }[] }` after decrypt + dotenv parse. |
29
40
|`getValue`|`{ objectKey, secretKey: string }`|`{ objectKey, secretKey, value }`. `NOT_FOUND` if key missing. |
30
41
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
+
31
54
## Errors
32
55
33
56
-`UNAUTHORIZED` — no session.
57
+
-`FORBIDDEN` — no collection access, or not allowed to manage tokens/grants.
34
58
-`BAD_REQUEST` — decrypt failure or invalid payload.
Copy file name to clipboardExpand all lines: docs/architecture.md
+11-6Lines changed: 11 additions & 6 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -8,43 +8,48 @@ flowchart LR
8
8
UI[React_shadcn]
9
9
end
10
10
subgraph next [Nextjs_App_Router]
11
-
Auth[Authjs_Google_Prisma]
11
+
Auth[Authjs_Google_JWT]
12
12
TRPC[tRPC_Router]
13
+
CI[GET_api_ci_file]
13
14
Crypto[AES256GCM]
14
15
end
15
16
subgraph aws [AWS]
16
17
S3[S3_bucket]
17
18
end
18
19
subgraph db [Postgres]
19
-
Prisma[Prisma_User_Session]
20
+
Prisma[Prisma_users_collections_tokens]
20
21
end
21
22
UI --> Auth
22
23
UI --> TRPC
23
24
TRPC --> Crypto
24
25
TRPC --> S3
25
26
Auth --> Prisma
27
+
CI --> Prisma
28
+
CI --> Crypto
29
+
CI --> S3
26
30
```
27
31
28
32
## Request flow
29
33
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.
31
35
2. The `signIn` callback rejects sign-ins whose email is not on `ALLOWED_EMAIL_DOMAIN`.
32
36
3. Authenticated users use tRPC (`/api/trpc`) from React Query. All vault procedures are **protected** and require a session.
33
37
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)).
34
39
35
40
## Trust boundaries
36
41
37
42
| Component | Trust |
38
43
| --- | --- |
39
44
|`ENCRYPTION_KEY`| Server-only. Anyone who holds it can decrypt all vault objects. |
40
45
| 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). |
42
47
| Browser | Sees decrypted content after successful auth and TLS. |
43
48
44
49
## Source of truth
45
50
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).
Copy file name to clipboardExpand all lines: docs/auth.md
+6-3Lines changed: 6 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,7 +2,7 @@
2
2
3
3
## Provider
4
4
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).
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.
25
25
26
-
## Sessions
26
+
## Sessions and database users
27
27
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()`.
Copy file name to clipboardExpand all lines: docs/storage-and-encryption.md
+6Lines changed: 6 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -39,6 +39,12 @@ The CLI ([`scripts/pull-secret.mjs`](../scripts/pull-secret.mjs)) implements the
39
39
40
40
There is no separate per-key storage in S3: “line items” are a **view** over the decrypted file.
41
41
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
+
42
48
## Security notes
43
49
44
50
- S3 at-rest encryption (SSE-S3 or SSE-KMS) is recommended in addition to application-layer encryption.
0 commit comments