You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Cold read of the live site, 2026-09-03. Measured with curl against desk.bounded.tools. Nothing was approved and no state was changed.
Edited shortly after filing. The first version of this issue quoted a live /pending response verbatim, including the operation prose, the Terraform resource and action, and the Actions run id. This repository is public, so that turned a record with a 900-second lifetime into a permanent, indexable one — committing the exact error the issue is about, at greater durability. The payload is described below rather than reproduced. The original text is still in this issue's edit history, which is itself worth knowing about; the named ceremony has long expired and the keeper is passkey-gated, so it is inert, but a maintainer may still want the history scrubbed.
What it returns
GET /pending needs no session and answers any caller with a JSON object shaped:
For the live response I captured, the body field contained, in plain readable English:
which privilege the approval mints, named exactly, down to the GitHub App token scope
which infrastructure directory it acts on
the precise Terraform operation, including the action and the specific resource being acted on
the flag values the run was invoked with
the Actions run id
Two polls eight minutes apart returned two different ceremony ids, so this is a live feed rather than one stuck record.
What it is not
I checked before writing this up rather than assuming. Fetching a published approval URL returns the keeper's own page, and that page asks for a passkey. The URL is not a bearer capability and this is not privilege escalation.src/pending.js also already knows the endpoint is anonymous — the comment on the oldestOpenQuestion import reasons explicitly about what an anonymous caller can make the Worker spend.
What it is
The endpoint's cost was considered. Its content does not appear to have been. Anonymous, unauthenticated and free to poll, it yields a continuous record of which privileged operation is pending, what it touches, and the wall-clock instant a 15-minute approval window opened.
APPROVAL_TTL_SECONDS = 900 bounds how long each record lives, not how long an observer keeps it. Polling every ten minutes reconstructs the org's privileged-change history indefinitely, with timestamps.
This sits oddly beside the line claims.bounded.tools holds deliberately and states in its own footer: the public feed drops assignees on purpose, because the board publishes a ranking and not a roster. /pending publishes considerably more than a roster, from the same origin, with no gate at all.
Why the shape makes it awkward
/pending is fetched by the service worker on push wake, and a push arrives with no payload by design (#51) precisely so the text comes over https from the origin instead. The endpoint therefore has to be reachable from a push handler. That is what makes this a design question rather than a one-line fix, and why I am filing rather than proposing a patch.
Bind it to the subscription. The caller already holds a push endpoint the origin stored. Requiring proof of one turns "anyone" into "a device we have seen".
Say less. The phone needs enough to decide whether to walk to a laptop. "An infra approval is waiting" plus the URL may be the whole requirement, with the operation prose behind the passkey on the keeper.
(3) is cheapest and probably the right first move. I have not measured which of these breaks the iOS wake path, and that is the thing to establish before picking one.
Caveat
This is a cold read from outside. I have not seen the threat model this endpoint was designed against, so it is possible the exposure is known and accepted. If it is, that belongs written down next to where the claims no-roster rule is written down — from outside, the two read as inconsistent.
Cold read of the live site, 2026-09-03. Measured with curl against
desk.bounded.tools. Nothing was approved and no state was changed.What it returns
GET /pendingneeds no session and answers any caller with a JSON object shaped:{ "kind": "approval", "title": "Approve <lane name>", "body": "<up to 400 characters of prose>", "url": "https://keeper.bounded.tools/a/<ceremony id>", "at": "<ISO 8601 timestamp>" }For the live response I captured, the
bodyfield contained, in plain readable English:Two polls eight minutes apart returned two different ceremony ids, so this is a live feed rather than one stuck record.
What it is not
I checked before writing this up rather than assuming. Fetching a published approval URL returns the keeper's own page, and that page asks for a passkey. The URL is not a bearer capability and this is not privilege escalation.
src/pending.jsalso already knows the endpoint is anonymous — the comment on theoldestOpenQuestionimport reasons explicitly about what an anonymous caller can make the Worker spend.What it is
The endpoint's cost was considered. Its content does not appear to have been. Anonymous, unauthenticated and free to poll, it yields a continuous record of which privileged operation is pending, what it touches, and the wall-clock instant a 15-minute approval window opened.
APPROVAL_TTL_SECONDS = 900bounds how long each record lives, not how long an observer keeps it. Polling every ten minutes reconstructs the org's privileged-change history indefinitely, with timestamps.This sits oddly beside the line
claims.bounded.toolsholds deliberately and states in its own footer: the public feed drops assignees on purpose, because the board publishes a ranking and not a roster./pendingpublishes considerably more than a roster, from the same origin, with no gate at all.Why the shape makes it awkward
/pendingis fetched by the service worker on push wake, and a push arrives with no payload by design (#51) precisely so the text comes over https from the origin instead. The endpoint therefore has to be reachable from apushhandler. That is what makes this a design question rather than a one-line fix, and why I am filing rather than proposing a patch.Directions, roughly by cost:
SESSION_SECRET, passkey login and gated routes landed in desk login: passkey auth, keeper-gated enrolment, and a session that ends #72. Apushhandler'sfetchcan carry same-origin cookies, so a subscribed-and-signed-in device would still read it. The cost is that a device with a subscription but no session falls back to the generic "The board changed." — which is what the code did before The installed app has no icon, and a notification cannot say what it is for #51, and what The installed app has no icon, and a notification cannot say what it is for #51 judged too thin.(3) is cheapest and probably the right first move. I have not measured which of these breaks the iOS wake path, and that is the thing to establish before picking one.
Caveat
This is a cold read from outside. I have not seen the threat model this endpoint was designed against, so it is possible the exposure is known and accepted. If it is, that belongs written down next to where the
claimsno-roster rule is written down — from outside, the two read as inconsistent.