A dev sign-in can never act on an Atmosphere account #211

Merged
Grandiras merged 1 commit from fix-dev-login-never-writes-network into main 2026-09-22 18:50:23 +00:00
Owner

Urgent — this is what wrote to a real Bluesky profile today.

/dev/login stored no token, and that was the whole guarantee. But the token store is keyed by DID, not by session, and the DID the e2e suite signs in as is a real account (did:plc:vmdq…, grandiras.net). Once that account had signed in for real on the local test stack, its OAuth token was in the store, every dev session as that DID read it as its own, and profile.spec.js's "Ponente only" save wrote a test display name and a one-pixel avatar onto the account's real Bluesky profile.

What changes:

  • /dev/login stamps its session with a claim (Auth/DevSignIn.cs).
  • ProfileService treats a stamped session as holding no token and no client, whatever the store says. Both the token read and the client factory call are guarded. It is the only place in the app that builds an Atmosphere client from the signed-in member.
  • DevSignInTests pins it: a write-scoped token sits in the store, a dev session saves to the network, and the result is NotPermitted with the client factory never asked.
  • profile.spec.js asserts the network option is disabled and picks the Ponente target explicitly before it types anything. Neither half alone stands between the suite and somebody's profile.
  • docs/auth.md and docs/testing.md say why.

No member-visible change; /dev/login only exists in Development.

🤖 Generated with Claude Code

**Urgent — this is what wrote to a real Bluesky profile today.** `/dev/login` stored no token, and that was the whole guarantee. But the token store is keyed by DID, not by session, and the DID the e2e suite signs in as is a real account (`did:plc:vmdq…`, grandiras.net). Once that account had signed in for real on the local test stack, its OAuth token was in the store, every dev session as that DID read it as its own, and `profile.spec.js`'s "Ponente only" save wrote a test display name and a one-pixel avatar onto the account's real Bluesky profile. What changes: - `/dev/login` stamps its session with a claim (`Auth/DevSignIn.cs`). - `ProfileService` treats a stamped session as holding **no token and no client**, whatever the store says. Both the token read and the client factory call are guarded. It is the only place in the app that builds an Atmosphere client from the signed-in member. - `DevSignInTests` pins it: a write-scoped token sits in the store, a dev session saves to the network, and the result is `NotPermitted` with the client factory never asked. - `profile.spec.js` asserts the network option is disabled and picks the Ponente target explicitly **before** it types anything. Neither half alone stands between the suite and somebody's profile. - `docs/auth.md` and `docs/testing.md` say why. No member-visible change; `/dev/login` only exists in Development. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
A dev sign-in can never act on an Atmosphere account
All checks were successful
CI / build (pull_request) Successful in 43s
5851ba2938
/dev/login stored no token, and that was the whole guarantee — but the
token store is keyed by DID, not by session, and the DID the e2e suite
signs in as is a real account. Once that account had signed in for real
on the test stack, its token was in the store, the dev session read it
as its own, and profile.spec.js's "Ponente only" save wrote a test name
and a one-pixel picture onto the account's Bluesky profile.

/dev/login now stamps its session with a claim (Auth/DevSignIn), and
ProfileService treats a stamped session as holding no token and no
client whatever the store says. DevSignInTests pins it with a
write-scoped token in the store. profile.spec.js also refuses to press
Save unless the network option is off, so neither half alone stands
between the suite and somebody's profile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collaborator

🤖 Reviewing this PR — the review will link the full session transcript.

🤖 Reviewing this PR — the review will link the full session transcript. <!-- opencode-run:review -->
opencode-reviewer left a comment

✅ Verdict: approve — no blocking issues found.

Good fix for a real incident. The claim-based approach is the right one — the question "how did this session sign in" is only answerable from the session, and the cookie is server-signed so the claim can't be forged. Both /dev/login paths (member and ?handle= stranger) stamp DevSignIn.Claim, and ProfileService — verified as the only place in the app that builds an Atmosphere client from the signed-in member — guards both the token read (HeldTokenAsync) and the client factory call. DevSignInTests pins the exact incident state: a write-scoped token in the store beside a dev session yields NotPermitted with the factory never asked, and the network path refuses before touching members/avatars/resolver, so the null! placeholders are safe. The e2e spec now asserts the Network radio is disabled and picks Ponente explicitly before typing, both of which fail loudly rather than silently if the guarantee regresses. Docs updated consistently. No issues found.



⏱ 1m 15s · 4 steps · $0.0065 · tokens: 33.0k in / 906 out (+36.5k cache-read) · Go: 5h 3% · wk 1% · mo 33%
📜 Full session transcript

✅ **Verdict: approve** — no blocking issues found. Good fix for a real incident. The claim-based approach is the right one — the question "how did this session sign in" is only answerable from the session, and the cookie is server-signed so the claim can't be forged. Both `/dev/login` paths (member and `?handle=` stranger) stamp `DevSignIn.Claim`, and `ProfileService` — verified as the only place in the app that builds an Atmosphere client from the signed-in member — guards both the token read (`HeldTokenAsync`) and the client factory call. `DevSignInTests` pins the exact incident state: a write-scoped token in the store beside a dev session yields `NotPermitted` with the factory never asked, and the network path refuses before touching `members`/`avatars`/`resolver`, so the null! placeholders are safe. The e2e spec now asserts the Network radio is disabled and picks Ponente explicitly before typing, both of which fail loudly rather than silently if the guarantee regresses. Docs updated consistently. No issues found. --- --- ⏱ 1m 15s · 4 steps · $0.0065 · tokens: 33.0k in / 906 out (+36.5k cache-read) · Go: 5h 3% · wk 1% · mo 33% 📜 [Full session transcript](https://git.grandiras.net/Grandiras/opencode-bot/src/branch/opencode-logs/transcripts/Grandiras/Ponente/pr-211/run-2026-09-22T18-45-34-887Z.md) <!-- opencode-reviewed-head:5851ba293896d1506e7a8dbe624b0173eb2a76af --> <!-- opencode-verdict:approve -->
Grandiras deleted branch fix-dev-login-never-writes-network 2026-09-22 18:50:41 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Grandiras/Ponente!211
No description provided.