Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <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).

## Prerequisites

- Node.js 20+
Expand Down
35 changes: 30 additions & 5 deletions docs/api-trpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand All @@ -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`

Expand All @@ -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.
17 changes: 11 additions & 6 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,43 +8,48 @@ 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 <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)).

## Trust boundaries

| Component | Trust |
| --- | --- |
| `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

Expand Down
9 changes: 6 additions & 3 deletions docs/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down
6 changes: 6 additions & 0 deletions docs/storage-and-encryption.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading