Skip to content
Merged
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
59 changes: 34 additions & 25 deletions CONTRIBUTING.md

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ RUN bun build src/index.ts --target=bun --outdir=dist

FROM oven/bun:1.3-alpine
WORKDIR /app
RUN addgroup -S app && adduser -S app -G app
RUN addgroup -S app && adduser -S app -G app \
&& mkdir -p /data/media && chown app:app /data/media
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
Expand Down
73 changes: 42 additions & 31 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ Public base URLs used in examples:
| POST | `/auth/passkey/register/finish` | none | Verify attestation, issue session |
| POST | `/auth/passkey/authenticate/begin` | none | Issue WebAuthn request options |
| POST | `/auth/passkey/authenticate/finish` | none | Verify assertion, issue session |
| GET | `/me` | `Authorization: Bearer` | Account |
| GET | `/me` | `Authorization: Bearer` | Account (`setup` next onboarding step) |
| GET | `/view/:viewKey` | none | Public profile card by view key |
| POST | `/me/name` | Bearer | Set/replace display name |
| POST | `/me/forum-laws-dismissed` | Bearer | Dismiss welcome-forum living-room laws |
Expand All @@ -83,7 +83,7 @@ Public base URLs used in examples:
| GET | `/lightning-address` | none | Resolve LUD-16 metadata (cached) |
| GET | `/debug/accounts` | `Authorization: Bearer` | Operator account listing (`DEBUG_TOKEN`) |
| POST | `/debug/accounts` | `Authorization: Bearer` | Operator provision name + Lightning Address (`DEBUG_TOKEN`) |
| PATCH | `/debug/accounts/:id` | `Authorization: Bearer` | Operator set `account.role` (`DEBUG_TOKEN`) |
| PATCH | `/debug/accounts/:id` | `Authorization: Bearer` | Operator set `role` and/or unlink Lightning Address |
| GET | `/debug/contacts` | `Authorization: Bearer` | Operator contact listing (`DEBUG_TOKEN`) |
| GET | `/debug/invoices` | `Authorization: Bearer` | Operator forum invoice attempts (`DEBUG_TOKEN`) |
| GET | `/debug/zap-ingests` | `Authorization: Bearer` | Operator kind:9735 ingest log (`DEBUG_TOKEN`) |
Expand Down Expand Up @@ -229,12 +229,13 @@ ID).
"forumLawsDismissed": false,
"viewKey": "<64-hex>",
"createdAt": 0,
"rulesAgreedAt": null
"rulesAgreedAt": null,
"setup": "name"
}
}
```

The `account` object is the same owner JSON as `GET /me` (includes `viewKey`).
The `account` object is the same owner JSON as `GET /me` (includes `viewKey` and `setup`).

### `POST /auth/passkey/authenticate/begin`

Expand Down Expand Up @@ -277,22 +278,24 @@ Missing or invalid bearer → **Response** `401`:
"forumLawsDismissed": false,
"viewKey": "<64-hex>",
"createdAt": 0,
"rulesAgreedAt": null
"rulesAgreedAt": null,
"setup": "name"
}
```

| Field | Type | Meaning |
| -------------------------- | -------------- | --------------------------------------------------------------------------------------------- |
| `id` | string | Opaque account id |
| `linkingKey` | string \| null | Historical LNURL-auth linking key (hex), or `null` for passkey accounts |
| `role` | string | `basis`, `verified`, `moderator`, or `founder` |
| `name` | string \| null | Display name, or `null` until set |
| `lightningAddress` | string \| null | Linked LUD-16 address, or `null` |
| `lightningAddressVerified` | boolean | Proof-of-control flag (`true` only after confirm) |
| `forumLawsDismissed` | boolean | `true` after the welcome-forum living-room laws hint was dismissed |
| `viewKey` | string | Durable 64 lowercase hex capability secret for GET /view/:viewKey. Owner-only. Not a session. |
| `createdAt` | number | Creation time (epoch ms) |
| `rulesAgreedAt` | number \| null | Epoch ms of first living-room rules agreement, or `null` |
| Field | Type | Meaning |
| -------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `id` | string | Opaque account id |
| `linkingKey` | string \| null | Historical LNURL-auth linking key (hex), or `null` for passkey accounts |
| `role` | string | `basis`, `verified`, `moderator`, or `founder` |
| `name` | string \| null | Display name, or `null` until set |
| `lightningAddress` | string \| null | Linked LUD-16 address, or `null` |
| `lightningAddressVerified` | boolean | Proof-of-control flag (`true` only after confirm) |
| `forumLawsDismissed` | boolean | `true` after the welcome-forum living-room laws hint was dismissed |
| `viewKey` | string | Durable 64 lowercase hex capability secret for GET /view/:viewKey. Owner-only. Not a session. |
| `createdAt` | number | Creation time (epoch ms) |
| `rulesAgreedAt` | number \| null | Epoch ms of first living-room rules agreement, or `null` |
| `setup` | string \| null | Next owner step: `name`, `lightning-address`, `rules`, or `null` when complete. Computed here; clients must not invent a parallel sequence. |

### `GET /view/:viewKey`

Expand Down Expand Up @@ -665,19 +668,24 @@ still omits `viewKey`.

### `PATCH /debug/accounts/:id`

Operator assignment of the account's forum display role. Authenticated with
`Authorization: Bearer` matching `DEBUG_TOKEN` (same gate as
`GET /debug/accounts`). Body:
Operator assignment of the account's forum display role and/or unlinking the
Lightning Address. Authenticated with `Authorization: Bearer` matching
`DEBUG_TOKEN` (same gate as `GET /debug/accounts`). Body is one or both of:

```json
{ "role": "basis" }
{ "role": "basis", "lightningAddress": null }
```

`role` must be one of `basis`, `verified`, `moderator`, or `founder`. This
path does not patch name or Lightning Address. `verified` is a human-identity
badge (a moderator physically met the person); it is not
`lightningAddressVerified`. New passkey accounts stay `basis` until an
operator changes them here.
`role` must be one of `basis`, `verified`, `moderator`, or `founder`.
`lightningAddress` may only be JSON `null` (unlink). Setting a new address
is not supported here (`POST /me/lightning-address` remains the live
resolve path). Unlink resets `lightningAddressVerified` to `false` and
drops any in-flight verification. `GET /me` then returns
`setup: "lightning-address"` when a name is already stored, so any client
that follows `setup` (or a missing `lightningAddress`) shows the address
form. `verified` as a **role** is a human-identity badge (a moderator
physically met the person); it is not `lightningAddressVerified`. New
passkey accounts stay `basis` until an operator changes them here.

`DEBUG_TOKEN` unset or blank → **Response** `503`:

Expand All @@ -691,10 +699,11 @@ Missing or non-matching bearer → **Response** `401`:
{ "error": "Unauthorized" }
```

Body is not JSON with a known `role` string → **Response** `400`:
Body is not JSON with a known `role` and/or `lightningAddress: null` →
**Response** `400`:

```json
{ "error": "Expected a JSON body with a \"role\" string" }
{ "error": "Expected a JSON body with a \"role\" string and/or lightningAddress null" }
```

Unknown account id → **Response** `404`:
Expand All @@ -703,9 +712,11 @@ Unknown account id → **Response** `404`:
{ "error": "Not found" }
```

Success → **Response** `200` with the updated account JSON (same eight-field dump as
`GET /debug/accounts`; no `viewKey`). The process logs
`debug.accounts.role_set` with the account id and new role (never the token).
Success → **Response** `200` with the updated account JSON (same nine-field dump as
`GET /debug/accounts`; no `viewKey`). Role changes log
`debug.accounts.role_set` with the account id and new role. Unlink logs
`debug.accounts.lightning_address.cleared` with the account id (never the
token or the previous address).

### `GET /debug/contacts`

Expand Down
42 changes: 35 additions & 7 deletions docs/handbook/endpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,34 @@
- **Used by:** `unlinkLightningAddress` in the app.
- **Auth:** See Purpose — Bearer where stated, else public.

## Endpoint: GET /messages/:id/video.mp4

- **Purpose:** Public MP4 bytes streamed from disk with `Accept-Ranges` / HTTP 206 so Damus can seek. `Access-Control-Allow-Origin: *`.
- **Errors:** 404 `{ error: 'Video not found' }`; 416 unsatisfiable `Range` (`Content-Range: bytes */SIZE`); 503 `{ error: 'Messages are unavailable' }`.
- **Used by:** Damus/Primal kind:1 video URLs.
- **Auth:** none.

## Endpoint: GET /messages/:id/video.webm

- **Purpose:** Same as `video.mp4` for WebM posts.
- **Errors:** Same 404 / 416 / 503.
- **Used by:** Damus/Primal.
- **Auth:** none.

## Endpoint: GET /messages/:id/video.mov

- **Purpose:** Same as `video.mp4` for QuickTime posts.
- **Errors:** Same 404 / 416 / 503.
- **Used by:** Damus/Primal.
- **Auth:** none.

## Endpoint: GET /.well-known/nostr.json

- **Purpose:** NIP-05 directory `{ names, relays }`. CORS `*`. Optional `?name=`.
- **Errors:** 503 `{ error: 'Directory is unavailable' }`.
- **Used by:** Damus verification; app proxies this from the site apex.
- **Auth:** none.

## Endpoint: GET /apple-touch-icon.png

- **Purpose:** PNG brand mark (apple-touch). `Cache-Control: public, max-age=86400`.
Expand All @@ -30,9 +58,9 @@

## Endpoint: PATCH /debug/accounts/:id

- **Purpose:** Operator assignment of `account.role` (`basis` \| `verified` \| `moderator` \| `founder`). Body `{ "role": "<AccountRole>" }`. Returns the updated account JSON (same shape as `GET /debug/accounts`: eight fields via `serializeAccount`; no `viewKey`). Does not patch name or Lightning Address.
- **Errors:** 503 `{ error: 'Debug is not configured' }` when `DEBUG_TOKEN` is unset or blank; 401 `{ error: 'Unauthorized' }` when the Bearer token does not match; 400 `{ error: 'Expected a JSON body with a "role" string' }` for unknown/missing/non-JSON body; 404 `{ error: 'Not found' }` when the account id is unknown.
- **Used by:** Operator `gifts-debug role` CLI.
- **Purpose:** Operator assignment of `account.role` (`basis` \| `verified` \| `moderator` \| `founder`) and/or hard-unlink of the Lightning Address. Body is one or both of `{ "role": "<AccountRole>" }` and `{ "lightningAddress": null }`. Unlink sets `lightningAddress` to null, `lightningAddressVerified` to false, and drops in-flight address verification. Returns the updated account JSON (same nine-field dump as `GET /debug/accounts` via `serializeAccount`; no `viewKey`). Does not set a new address here (`POST /me/lightning-address` remains the live resolve path).
- **Errors:** 503 `{ error: 'Debug is not configured' }` when `DEBUG_TOKEN` is unset or blank; 401 `{ error: 'Unauthorized' }` when the Bearer token does not match; 400 `{ error: 'Expected a JSON body with a "role" string and/or lightningAddress null' }` for unknown/missing/non-JSON body or a non-null `lightningAddress`; 404 `{ error: 'Not found' }` when the account id is unknown.
- **Used by:** Operator `gifts-debug role` and `gifts-debug unlink` CLI.
- **Auth:** `Authorization: Bearer` with `DEBUG_TOKEN`. Not an end-user session.

## Endpoint: GET /debug/contacts
Expand Down Expand Up @@ -177,7 +205,7 @@

## Endpoint: GET /me

- **Purpose:** Bearer session. Current account JSON (id, linkingKey, role, name, lightning address, verified flag, forumLawsDismissed, `createdAt`, `rulesAgreedAt`, owner `viewKey`).
- **Purpose:** Bearer session. Current account JSON (id, linkingKey, role, name, lightning address, verified flag, forumLawsDismissed, `createdAt`, `rulesAgreedAt`, owner `viewKey`, `setup`). `setup` is the next owner step (`name` \| `lightning-address` \| `rules`) or `null` when complete; computed here so clients do not invent a parallel sequence.
- **Errors:** 401 if missing/expired.
- **Used by:** App `fetchMe`.
- **Auth:** See Purpose — Bearer where stated, else public.
Expand All @@ -191,7 +219,7 @@

## Endpoint: GET /messages

- **Purpose:** Bearer required. Lists the public member forum newest-first (author name snapshotted at post, `text`, ISO `createdAt`, `sats`, `payable`, `hasPhoto`, and live author `role`), capped at 200 (latest-200 window). Clients render chronological messenger-group order (oldest top, newest bottom above the composer). Empty list is 200 `{ messages: [] }`. No `accountId` and no photo bytes in JSON; `payable` is true when the note has an `eventId` and the author has a Lightning Address; missing author → `role` `"basis"` and `payable` false.
- **Purpose:** Bearer required. Lists the public member forum newest-first (author name snapshotted at post, `text`, ISO `createdAt`, `sats`, `payable`, `hasPhoto`, `hasVideo`, `videoContentType`, and live author `role`), capped at 200 (latest-200 window). Clients render chronological messenger-group order (oldest top, newest bottom above the composer). Empty list is 200 `{ messages: [] }`. No `accountId` and no photo/video bytes in JSON; `payable` is true when the note has an `eventId` and the author has a Lightning Address; missing author → `role` `"basis"` and `payable` false. `videoContentType` is `null` when `hasVideo` is false.
- **Errors:** 401 `{ error: 'Unauthorized' }` missing/invalid/expired bearer; 503 `{ error: 'Messages are unavailable' }` if the store throws (`messages.list.failed`).
- **Used by:** App public comment thread.
- **Auth:** `Authorization: Bearer` session.
Expand Down Expand Up @@ -233,8 +261,8 @@

## Endpoint: POST /messages

- **Purpose:** Bearer required. JSON body `{ text?, photo?: { contentType, data } }` (base64; not multipart). Text-only `{ text }` stays valid; photo-only allowed; at least one of non-empty trimmed text or photo required. Name snapshot + optional JPEG/PNG/WebP ≤ 1 MiB. 200 is the public message including `sats`, `payable`, `hasPhoto`, and the session account's live `role` (not wrapped). New notes have `sats` 0 and `payable` false until signed.
- **Errors:** 401 Unauthorized; 400 Expected a JSON body with text and/or photo; 400 Set a name before posting; 400 Text must be 1–500 characters; 400 Text must be 1–500 characters or include a photo; 400 Photo must be a JPEG, PNG, or WebP under 1 MiB; 429 Too many messages (`Retry-After: 10`); 503 Messages are unavailable (`messages.create.failed`).
- **Purpose:** Bearer required. JSON `{ text?, photo?: { contentType, data } }` (base64 JPEG/PNG/WebP ≤ 1 MiB) or `multipart/form-data` with `text`, `video` (MP4/WebM/MOV ≤ 32 MiB), and optional JPEG/PNG/WebP `poster`. Text-only stays valid; photo-only or video-only allowed; at least one of non-empty trimmed text, photo, or video required. Name snapshot. 200 is the public message including `sats`, `payable`, `hasPhoto`, `hasVideo`, `videoContentType`, and the session account's live `role` (not wrapped). New notes have `sats` 0 and `payable` false until signed.
- **Errors:** 401 Unauthorized; 400 Expected a JSON body with text and/or photo; 400 Set a name before posting; 400 Text must be 1–500 characters; 400 Text must be 1–500 characters or include a photo or video; 400 Photo must be a JPEG, PNG, or WebP under 1 MiB; 400 Poster must be a JPEG, PNG, or WebP under 1 MiB; 400 Video must be an MP4, WebM, or MOV under 32 MiB; 429 Too many messages (`Retry-After: 10`); 503 Messages are unavailable (`messages.create.failed`).
- **Used by:** App forum composer.
- **Auth:** `Authorization: Bearer` session.

Expand Down
Loading
Loading