Skip to content

Sign in with Bitkit (grant QR) - #101

Merged
BitcoinErrorLog merged 8 commits into
release/shop-v0.6.8from
cursor/bitkit-signin-grant-a98e
Sep 24, 2026
Merged

BitcoinErrorLog merged 8 commits into
release/shop-v0.6.8from
cursor/bitkit-signin-grant-a98e

Conversation

@BitcoinErrorLog

@BitcoinErrorLog BitcoinErrorLog commented Sep 23, 2026 •

Copy link
Copy Markdown
Owner

Bitkit sign-in through a grant QR (PR 2 of the Bitkit grant-auth wave, #48 / #49)

Targets release/shop-v0.6.8. PR 1 (#100) merged there as 3c1ac747, and release is merged into this branch at fe3bc3d5. Bitkit 2.5+ accepts only pubkyauth://signin_grant. The sign-in page now offers a Bitkit grant QR next to the unchanged Pubky Ring cookie QR. A Bitkit identity can sign in, survive reload, and sign out. Commerce stays off for a grant sign-in until PR 3: there is no marketplace session, and the classic Ring approvals show refusal copy.

Design: /Volumes/t7/vibes-dev/.evidence/bitkit-signin-grant-48/REPORT.md rev 2, rows R2.1–R2.11 (design review round 2: SHIP_DESIGN).

Changes

  • Sign-in page. A "Signing in with Bitkit? Use Bitkit instead" switch is shown only when GrantAuthFlow.isDelegationAvailable. It opens a Bitkit QR from GrantAuthFlow.startDelegated(CAPABILITIES, signin, { clientId: 'shop.pubky.app', relay }) and offers "Use Pubky Ring instead", which mints a fresh Ring QR. The Bitkit QR has no Ring logo.
  • Grant session persistence. A grant session is never exported (sessionExport stays null). At sign-in completion it is saved to BrowserSessionStore inside the auth finalization lock, and only if the cross-tab authEpoch still equals the value read when the QR started. The auth store persists grantSessionRecordId.
  • Reload. Restores from the record id only: no cookie fallback, no bridge or fragment legs. A definitive failure removes the record and signs out; a transient one defers.
  • Sign-out, warm or cold. session.signout() runs first. Then, under the lock, the epoch is bumped and BrowserSessionStore.clearAll() runs. Only after the store reads back empty is local state (including the record pointer) cleared, and then a pubky-auth-v1 broadcast goes out. On that broadcast, a tab holding a live grant session signs it out, removes its own record, and drops local state. Cookie sessions ignore it. A failed cold restore still clears.
  • Key removal fails closed. removeGrantSession and clearGrantSessions read the store back with list() and reject while the record (or, for clearAll, any record) is still stored; the application retries twice. On a final failure, nothing clears the pointer:
    • warm or cold logout rejects without broadcasting, and /logout shows "We couldn't sign you out yet" with Retry;
    • an expired-grant restore keeps its record and reports deferred;
    • a second tab stays signed in.
      A browser without IndexedDB persistence (isAvailable() false) holds no records, so cookie sign-out there is not blocked.
  • Classic Pubky Ring approvals (purchase connect, step-up, inventory grant, messaging enable) show GrantSessionRefusal to a grant session and never start their flow. The manual Paykit claim this PR also guarded is gone: release removed it in 41287fba ("drop the dead claim control"), so the merge takes release's MarketplaceGetPaidSettings.tsx unchanged. Lock Server connect (Step 1) stays open; its page decides which QR it offers (PR 4 adds a grant QR there).

Contract rows → tests

Row Test
R2.1 Delegation unavailable SignIn.test.tsx › bitkit qr hidden without delegation
R2.2 Bitkit QR homeserver.test.ts › bitkit qr is signin_grant with shop caps and cid; SignIn.test.tsx › switches to a Bitkit grant QR and back to a fresh Ring QR; Chromium check 1–2
R2.3 Approval → grant session Chromium checks 3–4 (the session came from POST /auth/grant/session, not the cookie /session)
R2.4 Persist record id, never export; epoch-fenced save auth.test.ts (controller) › grant sign-in persists record id not export, save aborts after a sign-out since QR start; auth.store.test.ts › persists the grant record id and never exports a grant session
R2.5 Reload auth.test.ts (application) › reload restores grant session from store, restore never calls save, keeps the record on a transient restore failure; Chromium check 6
R2.6 Expiry auth.test.ts (application) › reload after grant expiry shows signed-out and drops the record, expiry cleanup rejects instead of signing out when the grant key cannot be removed, expiry cleanup signs out once a retried removal succeeds; auth.test.ts (controller) › expiry cleanup keeps the record pointer when its grant key cannot be removed
R2.7 Warm sign-out auth.test.ts (controller) › signout calls signout then clearAll under lock, warm logout removes the grant key before it clears the record pointer, warm logout fails and keeps the record pointer when the grant key cannot be removed, logout tells other tabs to let go; homeserver.test.ts › grant key removal (7 tests: read-back after remove/clearAll, unreadable store, no-IndexedDB no-op); Chromium check 8
R2.8 Cold sign-out auth.test.ts (controller) › cold logout calls signout before clearAll, cold logout still clears grant keys when the restore fails, cold logout fails and keeps the record pointer when the grant key cannot be removed
R2.9 Second tab auth.test.ts (controller) › second tab cannot save after sign-out, second tab drops live grant session on broadcast, cross-tab finalization removes this tab record before it clears the pointer, cross-tab finalization keeps the tab signed in when its grant key cannot be removed, a cookie session ignores the cross-tab broadcast; Chromium checks 9–11
R2.10 Bridge and consumer mode auth.store.test.ts › bridge never carries grant session: a grant session leaves no export to hand off; auth.test.ts (application) › consumer mode ignores grant record id: no bridge, no cookie restore
R2.11 Classic dialogs MarketplaceSessionConnectDialog.test.tsx › grant session sees refusal not classic qr, a cookie session still starts the classic approval when opened; MarketplaceReauthDialog.test.tsx › grant session sees refusal not classic qr (step-up); MarketplaceGrantSessionRefusal.test.tsx › inventory, messaging, cookie session keeps messaging resume path (the paykit-wasm flow still starts)

Fail-on-revert calibration

pr2/mutations.py applies each mutation and runs the named test file. It then restores the source byte-for-byte (sha256 checked) and ends with a clean run. Log: pr2/mutations.log. All 13 were caught; the clean run passes 271/271.

# Mutation Test that fails
K1 removeGrantSession logs instead of rejecting when the record is still stored remove rejects while the record is still stored, even when the SDK call resolved
K2 clearGrantSessions logs instead of rejecting when records remain clearAll rejects while any record is still stored
K3 warm logout clears the pointer before removing the key warm logout removes the grant key before it clears the record pointer (and the failure test)
K4 warm logout swallows a failed removal warm logout fails and keeps the record pointer when the grant key cannot be removed
K5 cold logout (deferred restore) swallows a failed removal cold logout fails and keeps the record pointer when the grant key cannot be removed
K6 restore finalizes on a failed removal expiry cleanup keeps the record pointer when its grant key cannot be removed
K7 expiry cleanup swallows a failed removal expiry cleanup rejects instead of signing out when the grant key cannot be removed
K8 second tab skips removing its record cross-tab finalization removes this tab record before it clears the pointer
R1 in-lock epoch equality check removed save aborts after a sign-out since QR start, second tab cannot save after sign-out
R2 grant safeSessionExport() calls session.export() persists the grant record id and never exports a grant session
R3 clearAll() before session.signout() signout calls signout then clearAll under lock, cold logout calls signout before clearAll
R4 grant restore falls through to cookie/bridge restore reload restores grant session from store, consumer mode ignores grant record id
R5 broadcast handler no longer drops the live session second tab drops live grant session on broadcast

Proof

  • Real Chromium on staging (local next dev against the staging homeserver and relay; staging seller test seat 7oboeqnf…): 11/11 on head c0107fa7. The same run passed 11/11 on c660118b, before the fix commit that treats a missing grant record like none.
    • The Bitkit QR is signin_grant with caps,relay,secret,cid,cpk, the Shop capabilities, and cid=shop.pubky.app.
    • A Bitkit-style approval signs in as the seat, with a record id and no export. The session came from POST /auth/grant/session (200) and no marketplace session was minted.
    • Reload restores it, and a second tab restores the same session.
    • /logout clears the record and bumps the epoch. The second tab drops its live session, stays signed out on reload, and a fresh tab is signed out.
    • How the approval was driven: Node @synonymdev/pubky 0.11 Signer.approveAuthRequest, run after the same pre-checks Bitkit's Paykit rc55 applies (grant URL, shown caps, shown client id). That is the JS binding of the pubky 0.11 PubkySigner::approve_auth that Paykit calls.
    • Files: log pr2/chromium-pr2.log, screenshots pr2/chromium/, script pr2/chromium-pr2.mjs.
  • Gate for the release merge fe3bc3d5 (the same interim rule; base release/shop-v0.6.8 @ 3c1ac747, the PR's 36 files): prettier/eslint/typecheck/7,078 related unit tests pass. No VRT spec is in the PR diff, so no VRT was run for the merge; CI runs the full suite. Log: pr2/gate-fe3bc3d5.log.
  • Gate for the key-removal fix b18fb084 (interim rule while gate PR Pre-push gate, Linux-only VRT, CI path filters #97 and its VRT crash fix are open): prettier/eslint/typecheck/6,410 related unit tests for the 7 changed files pass. One listing-form test, which does not touch sign-out, failed under load (load average ~108) and passes alone, 44/44. No VRT spec renders a changed line (auth services and controller only), so no VRT was run for this commit; CI runs the full suite. Logs: pr2/gate-b18fb084.log, pr2/gate-b18fb084-listingform-rerun.log; runner interim-gate.sh.
  • Gate for c0107fa7 (PREPUSH_BASE = PR 1 head 7724b848): prettier/eslint/typecheck/7,054 related unit tests/736 marketplace VRT pass. The marketplace VRT passed on a rerun (pr2/vrt-marketplace-rerun.log, 90/90 files) after the first run's container browser disconnected mid-suite. The non-marketplace VRT project crashes before any spec on the unchanged release head 55a084c too (pre-existing, handed to the gate owner); CI runs the full VRT. Logs: pr2/prepush.log, pr2/vrt-marketplace-rerun.log.

Evidence root: /Volumes/t7/vibes-dev/.evidence/bitkit-signin-grant-48/.

Notes for review

  • Auth, sessions and keys: Terra review and Kimi audit on this frozen head.
  • The Chromium journey ran on c0107fa7. The key-removal fix b18fb084 changes only the failure paths, which the unit tests and the mutation log above cover; its success path (store reads back empty) is the one the journey exercised.
  • HomeserverService.currentSessionIsGrant() is removed. It had no caller anywhere in the workspace.
  • Residue by design: signing out a scoped grant cannot revoke it (GrantManager.revoke needs a root session). The grant stays valid on the homeserver until grantExpiresAt. The local key is gone and cannot be saved again.
  • Not in this PR: the marketplace session for a Bitkit sign-in, test(vrt): wait for card hover scale before capture #48 Step 2 and the test(vrt): deterministic hover capture (zero-duration transitions in vrt-marketplace) #49 connect (PR 3), and the Lock Server grant QR (PR 4, BitcoinErrorLog/locks).

Bitkit accepts only pubkyauth://signin_grant. The sign-in page now offers
a Bitkit grant QR (GrantAuthFlow.startDelegated, client id shop.pubky.app,
the Shop capabilities) next to the unchanged Ring cookie QR.

A grant session is never exported: it is saved to BrowserSessionStore at
sign-in completion, inside the auth finalization lock and only if the
cross-tab auth epoch is unchanged since the QR started, and reload
restores it from its record id. Sign-out, warm or cold, signs the session
out, then bumps the epoch and clears BrowserSessionStore under the lock,
and tells other tabs to drop a live grant session.

Grant sessions carry no AuthToken, so the classic Pubky Ring approvals
(purchases, step-up, inventory, messaging, manual Paykit claim) show a
refusal instead of a QR. Lock Server connect stays open: its page decides
which QR it offers.
Grant URL arguments, delegation-gated Bitkit switch, BrowserSessionStore
restore without save, expiry and transient restore outcomes, consumer mode
skipping the bridge for a grant record, and the refusal on the classic
purchase, step-up, inventory and messaging approvals.
BrowserSessionStore remove() and clearAll() failures were logged and
swallowed after the record pointer had already been cleared, so logout
could succeed while a delegated key stayed in IndexedDB with no pointer
left to retry the delete.

Removal now reads the store back and rejects while the record (or any
record, for clearAll) is still listed, with two retries in the
application layer. Every path removes the key before it clears the
pointer: warm and cold logout run the removal after the homeserver
sign-out and before local cleanup, and fail without broadcasting when it
cannot finish; an expired-grant restore keeps its record and reports
deferred; a second tab removes its own record before it drops its
session. The unused HomeserverService.currentSessionIsGrant is removed.
@BitcoinErrorLog
BitcoinErrorLog changed the base branch from cursor/sdk-011-ring-a98e to release/shop-v0.6.8 September 23, 2026 17:39
…/bitkit-signin-grant-a98e

# Conflicts:
#	src/components/organisms/Marketplace/MarketplaceGetPaidSettings.tsx
@BitcoinErrorLog
BitcoinErrorLog merged commit 17faa2d into release/shop-v0.6.8 Sep 24, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant