**Note** from Bead: CORS Proxy · [canonical source](https://redfish.acequia.io/guerin/.agents/ff5ae03e-0087-4d71-b60a-bf1c7b5449c3/2026-06-17/notes/00-cors-proxy-spec.md) · session 2026-06-17 · discussion: Talk: CORS Proxy
*2026-06-17. Stephen: "start-bead CORS-proxy that can run in node.js, a chrome-extension, a php script on a hostgo-like site." Born from the web-edit bookmarklet: an in-page editor running on **arbitrary origins** needs to call an AI, but (a) cross-origin calls need **CORS**, and (b) the model **key must never reach the page**. A small relay solves both. It must be deployable wherever the host allows.*
## The shared contract (identical across runtimes) ``` POST { "section": "<text>", "instruction": "<what to do>", "model": "gemini-2.5-flash"? } -> { "suggestion": "<model output>" } (200) | { "error": "…" } / { "blocked": "…" } OPTIONS -> 204 (CORS preflight) CORS: Access-Control-Allow-Origin reflects the request Origin (the editor runs anywhere); Allow-Methods POST,OPTIONS; Allow-Headers content-type,authorization. ``` The relay forwards to the provider (Gemini `generateContent`) with the **server/extension-held key** and returns just the text. Same request shape the bead `/recommend` and `dev-serve` already speak, so the editor is unchanged — only its `cognition` endpoint differs.
## The three runtimes (pick by what the host allows) | Runtime | File | Use when | Key custody | |---|---|---|---| | **PHP** (`php/cognition.php`) | drop-in on any PHP/HostGo/cPanel host **next to the pages** | shared hosting, no Node; co-locate the relay with the site | `.ht-cognition-key` (Apache denies `.ht*` — never served) or `GEMINI_KEY` env | | **Node** (`node/cors-proxy.mjs`) | a box/VPS/home node you control (e.g. behind Caddy) | you can run a process; want it on the acequia node | `GEMINI_API_KEY` env or a key file | | **Chrome extension** (`chrome-extension/`) | **no server at all** — the user installs it | personal use on any page, key stays on the user's machine | `chrome.storage.local` (set in Options); page reaches it via `window.postMessage` ↔ content-script bridge | The editor selects transport by its `cognition` config: a URL (PHP/Node relay) or the literal `extension` (uses the postMessage protocol `rf-cognition-req` / `rf-cognition-res`).
## Key custody (the point of a relay) The key **never** goes in the page, the bookmarklet, or the commons. PHP reads it from an Apache-denied sibling file (or env); Node from env/keyfile; the extension from `chrome.storage`. The deployed key files are **not committed/synced** — only the code is. (Per the EULA-compliance + no-secrets-in-commons rules.)
## Security — open-relay risk (must harden before truly public) Because the editor runs on arbitrary origins, the relay reflects **any** Origin → it is an **open relay on your key**: anyone who finds the URL can spend your AI quota. v0 guards: POST-only, input size cap (24 KB), fixed provider/model sanitization. **Before exposing it widely, add:** a shared token the install page bakes into the bookmarklet (`?token=` checked by the relay), a **per-IP/per-day rate limit** (needs storage — a counter file/KV), and/or an **Origin allowlist**. The chrome-extension runtime sidesteps this entirely (per-user key, no public URL) — the safest option for individuals.
## Relation to the rest - **web-edit bookmarklet** (`e0cca7db`): this is its cognition transport. Default endpoint → `https://redfish.com/web-edit/cognition.php` once deployed. - **Save relay** (next): the no-CORS-save problem (HostGo webdisks) is the *same* shape — a relay that takes the serialized page + a path-scoped credential and does the server-side `PUT`. The PHP/Node variants here extend naturally to a `save.php` / save route. - **Liaison / `82bd6fa4`**: the Node variant is a sibling of the Liaison's inward-cognition pattern; both keep the key off the public surface (BYOK), differing only in transport (WS resolve-and-wait vs HTTP+CORS).