Products BeaconMCPs
Company About Blog
Appearance

Where the tokens stop

Beacon's builder takes credentials for Hardcover and Steam. The tokens stay on the user's machine; they never reach the deployed page or a shared export bundle. The boundary is a list of field names that two functions consult.

Beacon's builder asks users for a Hardcover bearer token and a Steam Web API key. Both go into state.json on the user's machine, where the builder uses them server-side, and neither is supposed to reach the deployed page. The Hardcover token in particular has account-deletion scope; the Steam key is read-only but still a credential the user owns and shouldn't have to think about.

The mechanism is two tables: WIZARD_ONLY_WIDGET_FIELDS and WIZARD_ONLY_CONTENT_FIELDS. Both are plain objects keyed by widget kind (or content-item type), with a Set of field names per key: nowReading: ['token'], steam: ['apiKey'], bookshelf: ['token', 'shelfMode', 'shelfValue', 'hardcoverLists']. When renderSiteTs walks the widget tree to emit site.ts (the file deployed to the browser), it consults the table and skips any field listed there. The state on disk keeps the token; the site that ships to readers does not.

The same tables guard a second surface: the export bundle. redactCredentialsForExport deep-clones the state and strips the same fields before writing the bundle to disk. A user who exports their Beacon to back up or migrate to another machine ends up with a bundle that's safe to share, with no tokens inside. Importing a redacted bundle leaves the credential fields empty, and the builder's per-widget UI shows "no token" naturally; the user re-enters them.

Adding a new credential to Beacon is a one-line change: a new Set entry under the widget's name flows through both renderSiteTs and the export function automatically. The architectural decision is reviewable in twelve lines of code.

Earlier in Beacon, the Hardcover token was emitted to site.ts as a data-token attribute on the rendered page. It worked, in the sense that the widget rendered correctly and anyone with view-source could read the token. Now it doesn't reach the page because its name is on a list.