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
6 changes: 4 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ api/
│ │ ├── lightning-address.ts # GET /lightning-address (public LUD-16 resolve)
│ │ ├── debug.ts # GET/POST /debug/accounts; PATCH /debug/accounts/:id; POST /debug/accounts/:id/session (DEBUG_TOKEN)
│ │ ├── debug-contacts.ts # GET /debug/contacts (operator DEBUG_TOKEN)
│ │ ├── debug-messages.ts # PUT /debug/messages/:id/video (operator DEBUG_TOKEN)
│ │ ├── debug-payments.ts # GET /debug/invoices; GET /debug/zap-ingests (DEBUG_TOKEN)
│ │ ├── debug-push.ts # POST /debug/push-ping (operator DEBUG_TOKEN)
│ │ ├── push.ts # GET /push/vapid-public; POST/DELETE /me/push-subscriptions
Expand Down Expand Up @@ -176,6 +177,7 @@ api/
│ ├── contact.test.ts
│ ├── conversations.test.ts
│ ├── debug-contacts.test.ts
│ ├── debug-messages.test.ts
│ ├── debug-payments.test.ts
│ ├── push.test.ts
│ ├── debug-push.test.ts
Expand All @@ -195,7 +197,7 @@ api/
├── scripts/
│ ├── check-handbook.mjs # CI gate: missing heading → exit 1
│ ├── check-e2e.mjs # CI gate: missing endpoint request or Function: title → exit 1
│ └── gifts-debug.sh # Operator CLI: list, set role, unlink Lightning Address (DEBUG_TOKEN)
│ └── gifts-debug.sh # Operator CLI: list, set role, unlink Lightning Address, video-put (DEBUG_TOKEN)
├── e2e/
│ ├── http.spec.ts # Playwright endpoint smokes against bun src/index.ts
│ ├── forum-replies.spec.ts # Playwright: provision, session, note, public GET, reply, replyCount
Expand Down Expand Up @@ -391,7 +393,7 @@ Currently:
| `BIND_ADDR` | `0.0.0.0:3000` | Listen address |
| `SERVICE_VERSION` | `0.1.0` | Surfaced via `/info` |
| `DATABASE_URL` | _(unset → in-memory)_ | Postgres connection string. When set, auth, `btc_usd_daily`, `message` (plus `message_invoice` and `nostr_zap_ingest`), `contact`, `conversation` / `conversation_message`, `push_subscription`, `push_outbox`, and `db_change` are migrated, `GET /gifts` and `GET /gifts/stats` read `gift` plus persisted BTC-USD daily closes (best-effort boot fill; failures log and do not kill the process), `GET/POST /messages`, `GET /messages/:id`, `GET /messages/:id/replies`, `GET /messages/:id/photo`, and `GET /messages/:id/video.*` (MIME in Postgres, bytes under `MEDIA_DIR`) use `PostgresMessageStore`, `POST /contact` / `GET /debug/contacts` use `PostgresContactStore`, `GET/POST /conversations` and `GET/POST /conversations/:id` use `PostgresConversationStore`, `GET /debug/invoices` and `GET /debug/zap-ingests` list invoice attempts and zap ingest rows, and a matching `POST /invoices/proof` inserts into `gift`. Unset keeps `InMemoryAuthStore`, in-memory forum, contact, conversation, and push stores, empty gift stats, empty day lists, and a no-op gift recorder. |
| `DEBUG_TOKEN` | _(unset → debug off)_ | Operator bearer for `GET /debug/accounts`, `POST /debug/accounts`, `PATCH /debug/accounts/:id`, `POST /debug/accounts/:id/session`, `GET /debug/contacts`, `GET /debug/invoices`, `GET /debug/zap-ingests`, and `POST /debug/push-ping`. Unset or blank → `503`; the process still boots. |
| `DEBUG_TOKEN` | _(unset → debug off)_ | Operator bearer for `GET /debug/accounts`, `POST /debug/accounts`, `PATCH /debug/accounts/:id`, `POST /debug/accounts/:id/session`, `GET /debug/contacts`, `GET /debug/invoices`, `GET /debug/zap-ingests`, `PUT /debug/messages/:id/video`, and `POST /debug/push-ping`. Unset or blank → `503`; the process still boots. |
| `NIP57_PROBE` | _(unset → probe on)_ | Set to `0` to skip the NIP-57 mint probe on `POST /debug/accounts` new addresses (Playwright e2e only). Unset or any other value probes. Production must not set this. The process still boots. |
| `WEBAUTHN_RP_ID` | _(none — required for passkey)_ | WebAuthn RP ID (`21.gifts` / `dev.21.gifts` / `localhost`). Passkey routes return `500` until it is set; the process still boots. Not a secret. |
| `WEBAUTHN_RP_NAME` | `21.gifts` | Human-readable RP name. |
Expand Down
66 changes: 65 additions & 1 deletion SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
> Product decisions live in [`CONCEPT.md`](./CONCEPT.md); this file owns
> request/response contracts for routes that exist in code today.

**Status**: living document. Last revised 2026-08-31 (`POST /debug/accounts/:id/session`; platform-thread retarget; NIP-17 rumor `created_at`; GET replies `{ messages }`).
**Status**: living document. Last revised 2026-09-01 (`PUT /debug/messages/:id/video`).

---

Expand Down Expand Up @@ -95,6 +95,7 @@ Public base URLs used in examples:
| 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`) |
| PUT | `/debug/messages/:id/video` | `Authorization: Bearer` | Operator restore of missing forum-video bytes (`DEBUG_TOKEN`) |
| GET | `/push/vapid-public` | Bearer | VAPID public key for Web Push subscribe |
| POST | `/me/push-subscriptions` | Bearer | Upsert a browser PushSubscription |
| DELETE | `/me/push-subscriptions` | Bearer | Remove a browser PushSubscription |
Expand Down Expand Up @@ -958,6 +959,69 @@ Environment:
| `DATABASE_URL` | When set, ingest rows are stored in Postgres `nostr_zap_ingest`. |
| `DEBUG_TOKEN` | Operator bearer for this route. Unset → 503; process still boots. |

### `PUT /debug/messages/:id/video`

Operator restore of missing forum-video bytes for an **existing**
`hasVideo` row. Authenticated with `Authorization: Bearer` matching
`DEBUG_TOKEN`. This is not an end-user session. The raw body is
`decodeForumVideo`'d and written under `MEDIA_DIR`; the handler does
not insert a message row or change columns.

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

```json
{ "error": "Debug is not configured" }
```

Missing or non-matching bearer → **Response** `401` (checked before the
body is read):

```json
{ "error": "Unauthorized" }
```

Non-UUID or unknown id → **Response** `404`:

```json
{ "error": "Not found" }
```

Row has no video, or stored MIME missing → **Response** `409`:

```json
{ "error": "Message has no video" }
```

Decoded type extension does not match the stored MIME → **Response**
`409`:

```json
{ "error": "Video type does not match" }
```

Empty, oversize, or unrecognized bytes → **Response** `400`:

```json
{ "error": "Expected a video body" }
```

Store or disk write throws → **Response** `503`:

```json
{ "error": "Messages are unavailable" }
```

Success → **Response** `204` with an empty body. Public
`GET /messages/:id/video.mp4` (or `.mov` / `.webm` matching the stored
type) can then serve the file.

Environment:

| Variable | Meaning |
| ------------- | ----------------------------------------------------------------- |
| `DEBUG_TOKEN` | Operator bearer for this route. Unset → 503; process still boots. |
| `MEDIA_DIR` | Directory the bytes are written to. Required at boot. |

### `GET /push/vapid-public`

Bearer session. Returns the VAPID **public** key the browser needs for
Expand Down
7 changes: 7 additions & 0 deletions docs/handbook/endpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@
- **Used by:** Operators debugging zap receipt indexing.
- **Auth:** `Authorization: Bearer` with `DEBUG_TOKEN`. Not an end-user session.

## Endpoint: PUT /debug/messages/:id/video

- **Purpose:** Operator restore of missing forum-video bytes for an existing message with `hasVideo`. Raw body is validated (`decodeForumVideo`), must match the stored MIME extension, and is written under `MEDIA_DIR` so public `GET /messages/:id/video.*` can serve it. Does not create a new message id or change the DB row.
- **Errors:** 503 `{ error: 'Debug is not configured' }` when `DEBUG_TOKEN` is unset or blank; 401 `{ error: 'Unauthorized' }` when the Bearer token does not match (checked before the body is read); 404 `{ error: 'Not found' }` for a non-UUID or unknown id; 409 `{ error: 'Message has no video' }` when `hasVideo` is not true or `videoContentType` is missing; 409 `{ error: 'Video type does not match' }` when the decoded type's extension differs from the stored MIME; 400 `{ error: 'Expected a video body' }` for empty, oversize, or unrecognized bytes; 503 `{ error: 'Messages are unavailable' }` when the store or disk write throws (`debug.messages.video.put_failed`).
- **Used by:** Operators restoring a missing on-disk forum video without SSH (`gifts-debug video-put`).
- **Auth:** `Authorization: Bearer` with `DEBUG_TOKEN`. Not an end-user session.

## Endpoint: GET /push/vapid-public

- **Purpose:** Bearer session. Returns `{ publicKey }` (URL-safe base64 VAPID public) so the app can subscribe.
Expand Down
15 changes: 11 additions & 4 deletions docs/handbook/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@
- **Purpose:** Constant-time compare of `DEBUG_TOKEN` against `Authorization: Bearer`.
- **Inputs:** Configured token (non-empty) and raw header or `undefined`.
- **Returns / side effects:** `true` only on an exact Bearer match (trim on the presented token).
- **Used by:** `debugRoutes`, `debugContactsRoutes`, `debugPaymentsRoutes`, `debugPushRoutes`.
- **Used by:** `debugRoutes`, `debugContactsRoutes`, `debugMessagesRoutes`, `debugPaymentsRoutes`, `debugPushRoutes`.

## Function: compareAccountsForList

Expand All @@ -252,6 +252,13 @@
- **Returns / side effects:** Hono app. 503 if token unset; 401 if bearer mismatches; 200 `{ contacts }` newest-first (cap 200); 503 on store throw (`contact.list.failed`). Logs `debug.contacts.listed` with count, never the token.
- **Used by:** `createApp` at `/debug/contacts`.

## Function: debugMessagesRoutes

- **Purpose:** Operator restore of a missing forum-video file for an already-existing message with `hasVideo` (raw body under `MEDIA_DIR`; no new message id, no DB create).
- **Inputs:** `DebugMessagesRouteDeps`: message store, optional debugToken.
- **Returns / side effects:** Hono app exposing `PUT /:id/video`. 503 if token unset/blank; 401 if bearer mismatches (before the body is read); 404 for non-UUID or unknown id; 409 when the row has no video or the decoded MIME extension does not match the stored type; 400 for empty/oversize/unrecognized body; 204 after `writeForumVideo`; 503 `{ error: 'Messages are unavailable' }` when `getById` or `writeForumVideo` throws (`debug.messages.video.put_failed`). Logs `debug.messages.video.put` with `messageId` and `bytes`, never the token or raw bytes.
- **Used by:** `createApp` at `/debug/messages`.

## Function: debugPaymentsRoutes

- **Purpose:** Operator listing of forum invoice attempts (`message_invoice`) and kind:9735 ingest decisions (`nostr_zap_ingest`).
Expand Down Expand Up @@ -541,7 +548,7 @@

## Function: createApp

- **Purpose:** Wires CORS, requestLog, brand, health, info, auth, me, `/view`, lightning-address, `/debug/accounts`, `/debug/contacts`, `/debug/invoices`, `/debug/zap-ingests`, `/debug/push-ping`, Web Push subscription routes, `/gifts`, `/gifts/stats`, `/messages` (incl. invoice), `/.well-known` NIP-05 `nostr.json` (CORS `*`), `/contact`, `/conversations`, and invoices.
- **Purpose:** Wires CORS, requestLog, brand, health, info, auth, me, `/view`, lightning-address, `/debug/accounts`, `/debug/contacts`, `/debug/messages`, `/debug/invoices`, `/debug/zap-ingests`, `/debug/push-ping`, Web Push subscription routes, `/gifts`, `/gifts/stats`, `/messages` (incl. invoice), `/.well-known` NIP-05 `nostr.json` (CORS `*`), `/contact`, `/conversations`, and invoices.
- **Inputs:** Optional `AppDeps` (store, clock, payer, fetch, cache, readBrand, origins, `debugToken`, giftStore, `giftRecorder`, `btcUsdRates`, `messageStore`, `contactStore`, optional `conversationStore` (default `InMemoryConversationStore`), `pushStore`, `vapidPublicKey`, `nostrKek`, spendApiToken, invoiceStore, `webAuthnRpId`, `webAuthnRpName`, `passkeyCeremony`). Omitted `giftRecorder` → `invoiceRoutes` uses `NoopGiftRecorder`; omitted `messageStore` → `InMemoryMessageStore`; omitted `contactStore` → `InMemoryContactStore`; omitted `conversationStore` → `InMemoryConversationStore`; omitted `pushStore` → `InMemoryPushStore`; omitted/blank `vapidPublicKey` → push HTTP 503 after session; omitted `nostrKek` → unsigned forum + invoice 503; SQL boot injects `SqlGiftRecorder`, `PostgresMessageStore`, `PostgresContactStore`, `PostgresConversationStore`, `PostgresPushStore`, and parsed KEK. Does not take a push sender (worker owns delivery).
- **Returns / side effects:** Hono app. Default `btcUsdRates` is an empty `InMemoryBtcUsdStore`. Used by Bun.serve in `index.ts` and by tests via `app.request()`.
- **Used by:** Boot path and every HTTP test.
Expand Down Expand Up @@ -1357,5 +1364,5 @@

- **Purpose:** Persist video bytes under `MEDIA_DIR` (caller should already faststart MP4/MOV via `decodeForumVideo`).
- **Inputs:** message id, video, env.
- **Returns / side effects:** mkdir + writeFile.
- **Used by:** `MessageStore.create`.
- **Returns / side effects:** mkdir, write UUID sibling temp, `rename` onto the public path so readers never see a partial file.
- **Used by:** `MessageStore.create`; `debugMessagesRoutes`.
Loading
Loading