From 825a1d0eb234df02680e733f0b868ec84cf08c4a Mon Sep 17 00:00:00 2001 From: J3vb <192430104+J3vb@users.noreply.github.com> Date: Mon, 20 Jul 2026 13:29:57 +0200 Subject: [PATCH] docs: document the GIF proxy endpoints, gif.api_key, and degradation contract Co-Authored-By: Claude Fable 5 --- docs/api.md | 71 +++++++++++++++++++++++++++++++++++- docs/architecture/client.md | 1 + docs/security.md | 2 +- docs/server-configuration.md | 23 ++++++++++++ 4 files changed, 95 insertions(+), 2 deletions(-) diff --git a/docs/api.md b/docs/api.md index c2f1fffa..76c811d0 100644 --- a/docs/api.md +++ b/docs/api.md @@ -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. diff --git a/docs/architecture/client.md b/docs/architecture/client.md index 5fe67a6e..768aaf61 100644 --- a/docs/architecture/client.md +++ b/docs/architecture/client.md @@ -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 diff --git a/docs/security.md b/docs/security.md index b54cfe7d..5349f620 100644 --- a/docs/security.md +++ b/docs/security.md @@ -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 diff --git a/docs/server-configuration.md b/docs/server-configuration.md index 73cd38ad..b8369bbc 100644 --- a/docs/server-configuration.md +++ b/docs/server-configuration.md @@ -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