Platform connections move to an admin page

Three of the four credentials the content pipeline needs expire on their own, and one of them every four hours. They are now connected once on an admin page instead of pasted into repository secrets.

The previous entry found three platform collectors that had quietly stopped, and recorded each as waiting on a credential change. Working out what those changes were made the underlying problem obvious: the credentials cannot be maintained by pasting them into repository secrets, because most of them expire on their own.

A Twitch user token lasts about four hours. The endpoint that returns a follower total accepts only a user token carrying moderator:read:followers, and the application token the collector falls back to can read viewer counts but not that — which is exactly the shape of the failure: viewers kept refreshing while follower totals stopped on 8 September. No secret pasted by hand can survive a four-hour expiry. A long-lived Instagram token lasts sixty days and can only be extended while it is still valid. A TikTok grant has to be re-consented whenever its scope set changes, and the live grant is missing the statistics scope.

What is at /admin/

A page on the site Worker, signed in with GitHub and limited to one account, listing the four platforms with their connection state, the scopes each one is asked for, and why it is there. Connecting runs that platform’s consent flow and stores the result.

The design point is where the long-lived credential sits. It stays in the Worker. The pollers ask for an access token over an authenticated route; the Worker refreshes first if that token is close to expiry, and the reply carries the access token, its expiry, the granted scopes and the verified account — never the refresh token. A token that leaks from a workflow log is then a token that expires on its own rather than a grant somebody else can keep renewing. The poller refuses a reply containing a refresh token outright, so neither side can quietly start sending one.

A grant is written only after two checks pass: the scopes it was actually granted cover what was asked for, and the account it belongs to is the expected handle. A consent screen completed on the wrong account, or granted without the scope the metric needs, is reported on the page and changes nothing — the working grant stays. Grants are sealed with AES-GCM before they reach storage, so a leaked namespace listing is not a leaked credential.

The pipeline does not depend on it

This is deliberate, and it is the point of the previous entry: a pipeline that had just been fixed for stalling silently must not gain a new single point of failure. With no endpoint configured, an unreachable Worker, or no usable grant, every poller falls back to the Actions secret it has always used and says which platform fell back and why. Until the storage binding and the secrets exist, the admin page answers with the list of what is missing and nothing about the pipeline changes.

Closed by default

The surface that holds the credentials is the one worth being careful about, so the ways in that should be shut are asserted shut rather than reasoned about: no session, a different GitHub account, a rewritten session payload or signature, an unsigned payload, an expired session, a mismatched OAuth state, a replayed state, a GET where a POST is required, and an absent binding. Sign-in requests no GitHub scopes at all, because the unauthenticated user endpoint already returns the account name the identity check needs. The pages carry a policy with no script source, no framing and no base-tag rewriting, are never cached, and are never indexed.

What still needs confirming

Each platform’s endpoints and scopes live in one table, and every entry records whether those URLs are proven against Sammy’s own applications. TikTok and Twitch are — the poller already calls those token endpoints every hour. Instagram and Kick are not: Meta’s documentation requires a login and Kick’s is behind a robots rule, so neither could be read from the build environment. Those two entries carry the correction and the note that a wrong value fails on the consent screen, with the platform’s own error text shown on the page, rather than silently. Whether any Kick endpoint exposes a follower total at all is still an open question; if it does not, the honest fix is to drop that metric from the panel rather than publish an empty one.

Validation evidence

154 delivery tests, 257 built-output tests, 77 Worker and feature tests — 21 of them the admin surface and 7 the poller’s side of it — 7 feed-delivery tests, the release audit across 34 canonical pages, the browser suite across 34 routes with zero failures, and 78 responsive, keyboard and print checks with zero failures. The 503 path was also confirmed against a real Worker runtime rather than only the test harness: an unconfigured deployment answers with the list of what is missing, carrying no-store, noindex and the restricted policy. /admin/* is served by the Worker rather than built, so it adds no route to the sitemap, no page to the release audit and nothing to the crawler-facing surface beyond this entry and its sitemap record.