docs: document the GIF proxy endpoints, gif.api_key, and degradation contract

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
J3vb
2026-07-20 13:29:57 +02:00
co-authored by Claude Fable 5
parent 89401c64a6
commit 825a1d0eb2
4 changed files with 95 additions and 2 deletions
+70 -1
View File
@@ -48,7 +48,8 @@ All error responses use this JSON envelope:
| `CONFLICT` | 409 | Duplicate username on register, or server already up-to-date on update |
| `TOO_LARGE` | 413 | File exceeds upload size limit |
| `SERVER_ERROR` / `INTERNAL` | 500 | Internal server error |
| `BAD_GATEWAY` | 502 | Upstream failure (GitHub API, LiveKit, asset download) |
| `BAD_GATEWAY` | 502 | Upstream failure (GitHub API, LiveKit, GIF provider, asset download) |
| `GIF_DISABLED` | 503 | GIF proxy is not configured on this server (no `gif.api_key`) |
---
@@ -621,6 +622,74 @@ Full-text search across messages in channels the user can read. Uses SQLite FTS5
---
## GIFs
The server proxies the Klipy GIF API so the provider API key stays server-side.
Clients never contact `api.klipy.com` — a key shipped in the desktop bundle
would be public by construction. The key is configured as `gif.api_key`
(see [Server Configuration](server-configuration.md#gif-picker-gif)).
**Default-off contract:** with no key configured, both endpoints return
`503` with error code `GIF_DISABLED`. Clients must treat that as "this server
does not have GIFs" and hide/disable the GIF affordance — not retry.
The media URLs in the response point at Klipy's CDN; the client still validates
them against its `klipy.com` CDN allowlist before rendering.
### GET /api/v1/gif/search
**Auth:** Required
**Rate limit:** 30 requests/minute (dedicated per-IP bucket)
#### Query Parameters
| Param | Type | Default | Range | Description |
| ----- | ---- | ------- | ----- | ----------- |
| `q` | string | (required) | 1-100 chars | Search term |
| `limit` | int | 20 | 1-50 | Maximum results to return |
#### Response 200 OK
```json
{
"results": [
{
"id": "abc123",
"title": "happy cat",
"media_formats": {
"tinygif": { "url": "https://media.klipy.com/abc123_tiny.gif" },
"gif": { "url": "https://media.klipy.com/abc123.gif" }
}
}
]
}
```
Only `id`, `title`, and the two `media_formats` URLs are forwarded. Every other
field the upstream returns is dropped, so an upstream that echoed the API key
could not leak it to clients. Results missing either format are omitted.
#### Errors
| Status | Code | When |
| ------ | ---- | ---- |
| 400 | `INVALID_INPUT` | Missing/blank `q`, `q` over 100 chars, or `limit` outside 1-50 |
| 401 | `UNAUTHORIZED` | No valid session (checked before the disabled check) |
| 429 | `RATE_LIMITED` | Over 30 requests/minute |
| 502 | `BAD_GATEWAY` | Upstream error, timeout, or unparseable response |
| 503 | `GIF_DISABLED` | `gif.api_key` is not configured |
### GET /api/v1/gif/trending
Same auth, rate limit, response shape, and error codes as
`/api/v1/gif/search`, minus the `q` parameter.
| Param | Type | Default | Range | Description |
| ----- | ---- | ------- | ----- | ----------- |
| `limit` | int | 20 | 1-50 | Maximum results to return |
---
## Direct Messages
DM channels use participant-based authorization rather than role-based permissions.
+1
View File
@@ -99,6 +99,7 @@ other (auth→voice→members), and the Solid beachhead is dead weight.
| Updates | `src/lib/updater.ts` + `update_commands.rs` | Endpoint derived from the connected server URL, https-only, TLS pinned to TOFU fingerprint, minisign-verified |
| Settings | `commands.rs` + `src/lib/preferences.ts` | Split persistence: Rust store (`settings.json`, key-allowlisted) *and* raw `localStorage` for UI prefs/themes |
| Theming | `src/lib/themes.ts` + `styles/tokens.css` | CSS custom properties; 4 built-in themes + custom overrides |
| GIF picker | `src/lib/gifProvider.ts` + `components/GifPicker.ts` | Calls the user's own server (`/api/v1/gif/*`) through `api.ts` — no provider API key in the bundle. Server answers `503 GIF_DISABLED` when unconfigured: the picker shows "GIFs are not enabled on this server" and `onUnavailable` disables the composer's GIF button (with a `title`/`aria-label` reason) instead of failing silently. Returned media URLs are still pinned to the `klipy.com` CDN. |
### Quality tooling
+1 -1
View File
@@ -75,7 +75,7 @@ The Tauri desktop client implements the following security measures:
- URLs are validated via `isSafeUrl` (rejects `javascript:`, `data:`, `vbscript:`)
- YouTube embeds use `sandbox` attribute on iframes
- `image/svg+xml` is excluded from safe MIME types for data URIs
- Tenor GIF URLs are validated against trusted CDN origins
- GIF media URLs are validated against the trusted Klipy CDN origins
- Linkified URLs strip trailing punctuation to prevent misleading destinations
### Search and Rate Limiting
+23
View File
@@ -114,6 +114,23 @@ Controls the Wazero WASM plugin runtime. Requires building with `-tags wazero`.
| `plugins.cpu_budget_ms` | int | `100` | Maximum CPU time per plugin invocation (milliseconds) |
| `plugins.http_allowlist` | string[] | `[]` | Host suffixes plugins may reach via the `host_http` capability (e.g. `["api.steampowered.com"]`). Empty = no outbound HTTP. |
### GIF Picker (`gif`)
Powers the client's GIF picker. The server proxies the Klipy API so the key
never ships in the desktop bundle — the client only ever calls
`/api/v1/gif/*` on its own server.
**Disabled by default.** With no `gif.api_key` set, `/api/v1/gif/*` returns
`503 GIF_DISABLED` and clients hide their GIF button. Nothing else changes.
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `gif.api_key` | string | `""` | Klipy API key. Get one at [partner.klipy.com](https://partner.klipy.com). Empty = feature off. |
> **Treat this as a credential.** Prefer `OWNCORD_GIF_API_KEY` (or a secrets
> manager) over writing it into `config.yaml`, and rotate it if it has ever
> been exposed to a client build.
## Environment Variable Overrides
Every config key can be overridden via environment variables using the prefix `OWNCORD_`.
@@ -146,6 +163,7 @@ Every config key can be overridden via environment variables using the prefix `O
| `OWNCORD_TELEMETRY_SERVICE_NAME` | `telemetry.service_name` |
| `OWNCORD_PLUGINS_ENABLED` | `plugins.enabled` |
| `OWNCORD_PLUGINS_DIRECTORY` | `plugins.directory` |
| `OWNCORD_GIF_API_KEY` | `gif.api_key` |
## Example config.yaml
@@ -214,6 +232,11 @@ plugins:
max_memory_mb: 64
cpu_budget_ms: 100
http_allowlist: [] # host suffixes plugins may reach, e.g. ["api.steampowered.com"]
# GIF picker (server-side Klipy proxy). Empty key = feature off.
# Prefer OWNCORD_GIF_API_KEY over storing the key in this file.
gif:
api_key: ""
```
## See Also