# TextShare API (LiteTextShare Backend)

Text express-cabinet (TextShare) backend API — the single source of truth for
frontend (BFF) and AI agent integration. Any change to this document MUST land
in the same release as the corresponding API change.

- Base URL (REST, via BFF proxy): `https://textshare.litelake.com/api/**`
- Collaborative editing (WebSocket, direct connection, NOT via BFF):
  `wss://ws.textshare.{env}/collab/{noteId}?token={editToken}` — the `wsUrl`
  base is returned by the create/verify responses, clients never assemble it.

## Conventions

- All REST endpoints are `POST` + `application/json` (except `/docs/*` and the
  WebSocket endpoint, which are `GET`).
- Unified response envelope: `{ "code": number, "message": string, "data": T }`.
  Success: `code = 200`.
- Business errors use HTTP 200 and are distinguished by `body.code`
  (4101/4103/4104). Rate limiting uses real HTTP 429 + `Retry-After`;
  storage outage (fail-closed) uses real HTTP 503.
- All requests SHOULD carry the `X-Device-Id` header (UUID from
  `localStorage`). Invalid/missing values degrade brute-force protection to
  IP-only dimension (no error).

## POST /api/public/notes — create note and issue pickup code

Create a note with locally-written initial content (write first, generate code
afterwards). The note is immediately `active`.

Request body:

```json
{
  "mode": "md",
  "initState": "<base64(V1 update of the local Y.Doc)>"
}
```

- `mode`: `txt` | `md` (whitelist; anything else → 4103)
- `initState`: base64 of the local Y.Doc's `Y.encodeStateAsUpdate()` (V1).
  An empty string means an empty document. The server applies the update to an
  empty doc, folds it, and measures the plain-text byte length.

Response `data`:

```json
{
  "noteId": "cz9fkq2wm4xv...",
  "pickupCode": "K7M2XQ",
  "mode": "md",
  "expiresAt": "2026-09-16T08:00:00Z",
  "wsUrl": "wss://ws.textshare.{env}/collab",
  "editToken": "9f2c8e...64 hex chars"
}
```

Errors: `4103` invalid mode / `initState` not applicable (HTTP 200),
`4104` text exceeds 128 KB (HTTP 200), `429` rate limited (12/hour +
30/day per IP, real HTTP 429 + `Retry-After`), `503` storage unavailable
(fail-closed).

## POST /api/public/pickup/verify — exchange pickup code for collaboration entry

Request body:

```json
{ "pickupCode": "K7M2XQ" }
```

The code is normalized server-side: strip spaces/hyphens, uppercase,
`O→0`, `I/L→1`. Response `data` has the same shape as the create response
(`pickupCode` echoes the normalized value).

Errors: `4101` pickup code invalid or expired — unified message to prevent
enumeration (HTTP 200; each failure increments per-IP and per-device counters).
Brute-force protection: 5 failures / 10 minutes on EITHER dimension blocks the
caller with real HTTP 429 + `Retry-After`. Counters are not reset by success.
Redis outage → real HTTP 503 (fail-closed).

## GET /collab/{noteId}?token={editToken} — collaboration WebSocket

Yjs collaboration endpoint served by a Hocuspocus-protocol-compatible server
(`@hocuspocus/provider` interop; every frame carries the VarString(docName)
prefix). Direct browser connection (never via BFF; gateway forwards per-host).

Handshake authorization (performed before the WebSocket upgrade):

1. `noteId` exists, `status = active`, not expired (Redis-cached 60 s + MySQL
   fallback; expired/not-found → rejected with 401, business code 4102)
2. `editToken` is valid and bound to this `noteId` (Redis; miss or Redis
   failure → rejected, fail-closed, business code 4105)
3. Per-IP connection count < 10, peers per room < 16, single update <= 768 KB,
   single frame <= 1 MB

After the upgrade the server runs the y-protocols sync handshake
(SyncStep1/2), broadcasts awareness (cursors/presence), and persists every
commit as an incremental V1 update (append-then-compact into MySQL:
`notes.snapshot` + `note_updates` log). Documents expire 48 h after creation;
expiry closes rooms forcefully (clients receive a close event).

There are no read-only connections: possessing the token grants edit rights
(v1 product definition).

## GET /docs/api.md, /docs/api.en.md, /docs/api.zhs.md

This document (embedded in the binary; `en` is the default/fallback, `zhs` is
Simplified Chinese). Served as `text/markdown` with `Cache-Control: no-cache`.

## GET /api/health, /api/ready

Framework built-in liveness/readiness probes (gateway healthcheck target).

## Error code table

| body.code | Meaning | HTTP status |
|---|---|---|
| 200 | success | 200 |
| 400 | invalid arguments (binding) | 200 |
| 4101 | PICKUP_NOT_VALID (unified anti-enumeration message) | 200 |
| 4102 | NOTE_NOT_FOUND / expired (WS Authorize; REST does not reach it) | 200 |
| 4103 | INVALID_ARGUMENT (mode / initState) | 200 |
| 4104 | NOTE_TOO_LARGE (> 128 KB plain text after fold) | 200 |
| 4105 | EDIT_TOKEN_INVALID (WS handshake rejected) | 200 |
| 429 | RATE_LIMITED | **429 + `Retry-After`** |
| 503 | STORAGE_UNAVAILABLE (Redis outage, fail-closed) | **503** |
| 500 | internal error | 500 |

Note: the baseline rate-limit middleware on `/api/public` write methods
(30/min/IP) responds with its own body shape when triggered; clients must
treat 429 by HTTP status code, not by body shape.
