Skip to content

Bitkit: marketplace session bootstrap and dialogs - #103

Merged
BitcoinErrorLog merged 5 commits into
release/shop-v0.6.8from
cursor/bitkit-bootstrap-dialogs-a98e
Sep 24, 2026
Merged

BitcoinErrorLog merged 5 commits into
release/shop-v0.6.8from
cursor/bitkit-bootstrap-dialogs-a98e

Conversation

@BitcoinErrorLog

Copy link
Copy Markdown
Owner

Purchases for a Bitkit sign-in: the browser bootstrap (PR 3 of the Bitkit grant-auth wave, #48 / #49)

Stacked on #101 (cursor/bitkit-signin-grant-a98e @ fe3bc3d5, which includes release/shop-v0.6.8 @ 3c1ac747); open it against that branch. A Bitkit (grant) sign-in has no AuthToken to redeem, so until now it could not get a marketplace session. That kept #48 Step 2 ("Open Bitkit setup") disabled and blocked the #49 connect. This PR adds the browser purchase bootstrap:

  1. The grant session writes a single-use proof document to its own homeserver.
  2. The Shop BFF runs the CLI verifier's checks and opens a marketplace signin_grant.
  3. After a second Bitkit approval, the BFF claims the bearer.

Design: /Volumes/t7/vibes-dev/.evidence/bitkit-signin-grant-48/REPORT.md rev 2, rows R3.1–R3.10, and Addendum A in pr3/DESIGN-GAP-2026-09-23.md, rows A1–A11 (design review round 3: SHIP_DESIGN, cleanup list folded in).

Changes

  • BFF routes (src/app/api/marketplace/bootstrap-challenges, …/[challengeId]/verify, bootstrap-flows/[stateId]/status, …/cancel). They are same-origin only, gated by SHOP_BFF_GRANT_FLOW_ENABLED (not the CLI flag), and use the CLI verifier's parameters. A missing state key or database URL returns 404 grant_unavailable.
  • Derived result PoP seed, no migration.
    • browserSeedKey is an HKDF key from the state key, with its own salt and info.
    • deriveBrowserBootstrap derives the seed and the delivery id as two labelled HMACs over the challenge id.
    • Verify re-derives both under the current epoch and refuses with 409 fresh_approval_required before consume if the key rotated.
    • The seed is sealed in a {version: 2, kind: 'browser'} context and is never returned, logged, or written to the proof document.
  • Browser verify is not verifyCliChallenge. It runs the listed checks itself, then consumeChallengeAndInsertCliFlow with the flow-cookie hash and the version-2 context. The flow is bound to __Host-shop-marketplace-grant, the same cookie as the reconnect flow.
    • The CLI routes refuse that cookie (cli_token_denied), and the browser routes refuse a CLI token (flow_binding_denied).
    • verifyCliChallenge now refuses a browser challenge row before consume.
    • A version-1 CLI context at a browser claim is 403 result_denied.
  • Client (marketplace-bootstrap-client.ts).
    • Writes the proof document with the grant session and posts verify, then deletes the document whatever the outcome.
    • Polls the flow, retrying while another poll holds the claim.
    • Maps a refused homeserver write to shop_session_expired.
    • Refuses and cancels a marketplace QR that asks for anything beyond /pub/pubky.app/marketplace-service/v1/:rw.
  • Connect dialog. For a Bitkit sign-in with no marketplace session, the hook runs the bootstrap, and the dialog shows "Approve purchases in Bitkit" with Bitkit copy and no Ring logo. The refusal now shows only when the grant flow is off. The session-required card names Bitkit for such a session.
  • Reason codes. Every R3.8 code, plus shop_session_expired and approval_invalid (A10), has static copy. A failed approval shows the approval_invalid copy instead of an empty alert.
  • Other dialogs.
    • Inventory grant: refusal copy "Inventory edits need a Pubky Ring sign-in for now." The classic copy and button name Pubky Ring only.
    • Messaging: "Messages need a Pubky Ring sign-in for now."
    • Lock Server Step 1 and its dialog: "Approve the connection in Pubky Ring or Bitkit" / "Scan with Pubky Ring or Bitkit".

Contract rows → tests

Row Test
A1 Browser challenge browser-bff.test.tsbrowser challenge stores derived result_cpk and delivery hash
A2 Seed never leaves the BFF browser-bff.test.tsresult seed never leaves bff (the challenge and verify responses, the poll result, the proof body, a refusal body, the service bootstrap arguments and every console call are scanned for the seed as hex, base64 and base64url; the response keys are exactly the proof-document fields)
A3 Rotation before verify browser-bff.test.tsverify refuses a challenge after a state key rotation
A4 Rotation before claim browser-bff.test.tsclaim opens a context sealed under the previous epoch, claim fails closed when the sealing epoch is gone
A5 Domain separation browser-bff.test.tsbrowser seed key differs from every existing BFF key for the same epoch (the five existing keys recomputed with HKDF; both outputs compared with the raw state key and the assertion and request seeds; known-answer vector from an independent Python HKDF/HMAC)
A6 Credential cross-use browser-bff.test.tscli routes refuse a browser bootstrap flow, browser routes refuse a cli flow, cli verify refuses a browser bootstrap challenge before consume, browser verify refuses a cli challenge before consume
A7 Context kind browser-bff.test.tsbrowser claim refuses a cli context
A8 Config gating browser-bff.test.tsbrowser bootstrap does not need the cli flag, browser bootstrap 404s without grant config (flag off, state key missing, database URL missing)
A9 No migration browser-bff.test.tsno migration added by the browser bootstrap (db/bff holds exactly 0001 and 0002); git diff fe3bc3d5..HEAD -- db/ is empty
A10 Reason codes failure-messages.test.tsbootstrap reason code shop_session_expired maps to copy, bootstrap reason code approval_invalid maps to copy
A11 Bitkit approval caption marketplace-bootstrap-client.test.tsbootstrap url matches captured fixture (fixture src/test/fixtures/auth/marketplace-bootstrap-url.staging.json, captured from the staging bootstrap: signin_grant, caps,relay,secret,cid,cpk, only /pub/pubky.app/marketplace-service/v1/:rw, cid=marketplace.staging.shop.pubky.app), shows no caption for a URL that names no client; MarketplaceSessionConnectDialog.test.tsxbootstrap QR carries the caption naming the client Bitkit will show. Caption: "Bitkit shows this request from , for marketplace purchases only."
R3.1 Routes browser-bff.test.tsbootstrap routes reject cross-origin; A8 tests
R3.2 Challenge pubky useMarketplaceSessionConnect.test.tschallenge pubky comes from live grant session; marketplace-bootstrap-client.test.tsproof for a pubky the session cannot write fails
R3.3 Seed in the BFF A1, A2, A5
R3.4 Proof write marketplace-bootstrap-client.test.tsgrant session writes proof document, maps a refused homeserver write to shop_session_expired and never verifies; Chromium
R3.5 Verify browser-bff.test.tsreplayed challenge is consumed, expired challenge rejected, missing nonce rejected, proof from another pubky rejected, foreign homeserver proof rejected, verify binds the flow to the bootstrap cookie with a version 2 browser context
R3.6 Bootstrap URL marketplace-bootstrap-client.test.tsrefuses and cancels a bootstrap QR that asks Bitkit for more than purchases; A11; Chromium
R3.7 Cleanup and claim browser-bff.test.tsclaim pubky mismatch rejected; useMarketplaceSessionConnect.test.tsbrowser rejects bearer for another pubky; marketplace-bootstrap-client.test.tsproof deleted after verify, keeps polling while another poll holds the claim, then returns the claimed session, cancel posts to the bootstrap cancel route once; Chromium
R3.8 Reason codes failure-messages.test.tsbootstrap reason code <code> maps to copy (18 codes), keeps the reconnect copy for shop_session_expired outside the bootstrap, an unknown bootstrap code falls back to static copy, never the code; useMarketplaceSessionConnect.test.tsan ended grant sign-in shows the bootstrap copy, not the AuthToken fallback, a failed Bitkit approval shows the approval_invalid copy
R3.9a Step-up, grant session not changed here: the step-up keeps #101's refusal (see notes)
R3.9b Connect, grant session useMarketplaceSessionConnect.test.tsgrant session connect uses bootstrap, a Ring (cookie) sign-in never runs the grant bootstrap; MarketplaceSessionConnectDialog.test.tsxgrant session connect uses bootstrap (Bitkit copy, no refusal, flow starts), bootstrap creating state confirms with the homeserver, grant session sees refusal not classic qr (grant flow off); MarketplaceSessionRequiredCard.test.tsx (3 tests); VRT session-connect-bitkit-bootstrap-desktop; Chromium
R3.9c Inventory MarketplaceGrantSessionRefusal.test.tsxgrant session sees inventory refusal, inventory copy names Ring only
R3.9d Manual claim N/A: release removed the manual watch-only claim in 41287fba ("drop the dead claim control"); Step 2 "Open Bitkit setup" is the only claim path
R3.10 Messaging MarketplaceGrantSessionRefusal.test.tsxgrant session sees messaging refusal without qr, cookie session keeps messaging resume path (the paykit-wasm flow still starts)

Fail-on-revert: four checks were removed or widened in turn and the named tests failed (pr3/calibration.log):

  • the re-derive check in browser verify fails verify refuses a challenge after a state key rotation and browser verify refuses a cli challenge before consume;
  • the browser-row refusal in verifyCliChallenge fails cli verify refuses a browser bootstrap challenge before consume;
  • widening the client's pinned capability to /pub/pubky.app/:rw fails bootstrap url matches captured fixture and the other bootstrap client tests.

Proof

  • Real Chromium on the staging Shop: 14/14 on 3be1ffd8. The only change after it, 6c814e5d, adds the two VRT baselines.
    • Deployed to Vercel pubky-marketplace-staging (prj_V6j0IUoiX8U3imhavqenlvwdKRRc, team team_y2cqjCWZ9vTnCPWQkUAgfijD) as dpl_7eSs65hskmACPrDVuyjD8haTBNPp. The target was checked as not production and not shop.pubky.app before each deploy.
    • Afterwards staging was promoted back to the recorded dpl_GXBWJBkmvoZF4EREUu41mLKTbuhV, and vercel inspect confirms the alias.
    • Staging marketplace-service was not changed (no Railway deploy).
    • Seat: staging seller test seat 7oboeqnf….
    • What passed:
      • A Bitkit-style grant sign-in gives no cookie export and no marketplace session.
      • test(vrt): wait for card hover scale before capture #48 Step 2 is disabled before the bootstrap.
      • The connect dialog shows the Bitkit copy and caption, not a refusal.
      • The bootstrap QR is signin_grant with caps,relay,secret,cid,cpk, only /pub/pubky.app/marketplace-service/v1/:rw, and cid=marketplace.staging.shop.pubky.app.
      • The proof document was written and deleted (PUT 201, DELETE 204), and challenge and verify returned 201.
      • After a Bitkit-style approval of the marketplace grant, the claimed session belongs to the seat, and no AuthToken session POST was made.
      • test(vrt): wait for card hover scale before capture #48 Step 2 is enabled and opens the Paykit setup.
      • Sign-out clears the grant record and the marketplace session.
    • The same run passed 14/14 on the pre-rebase head d789080f (chromium-pr3-d789080f.log, chromium-d789080f/).
    • How the approvals were driven: Node @synonymdev/pubky 0.11 Signer.approveAuthRequest, run after the pre-checks Bitkit's Paykit rc55 applies. That is the JS binding of the pubky 0.11 PubkySigner::approve_auth.
    • Files: pr3/chromium-pr3-3be1ffd8.log, pr3/chromium/ (screenshots, results.json, network.json, bootstrap-url-shape.json), pr3/chromium-pr3.mjs, and pr3/staging/ (target confirmation, deploy logs, vercel inspect before and after, promote-back logs).
  • Gate (interim rule while gate PR Pre-push gate, Linux-only VRT, CI path filters #97 and its VRT crash fix are open; base Sign in with Bitkit (grant QR) #101 fe3bc3d5, the PR's 26 source files): prettier/eslint/typecheck/2,257 related unit tests pass. Log: pr3/gate-3be1ffd8.log.
  • Marketplace VRT for the spec this PR touches (MarketplaceSessionConnect.vrt.test.tsx, Linux container, Chromium and Firefox): the new scene session-connect-bitkit-bootstrap-desktop got its Linux baselines in one run. Both were reviewed. Then the spec passed 18/18 with CI=true, and no other screenshot changed. Logs: pr3/vrt-new-baselines-3be1ffd8.log, pr3/vrt-marketplace-session-connect-3be1ffd8.log. CI runs the full suite.

Notes for review

  • Auth, sessions and keys: Sol/Terra review and Kimi audit on this frozen head.
  • R3.9a (a grant step-up through a Bitkit QR) is not built here. A Bitkit sign-in already requests the full Shop capabilities, so the needs_reauth state that opens the step-up dialog should not arise for it, and the dialog keeps Sign in with Bitkit (grant QR) #101's refusal. Building the grant step-up means a second grant session swap under the finalization lock.
  • Residue by design (Addendum A): anyone holding an epoch's state key can recompute a challenge's seed from its id until that key is dropped. That is the accepted option-A trade. It does not cross challenges.
  • PR 4 (BitcoinErrorLog/locks) has no code dependency on this PR as long as it keeps the Lock Server contract the Shop reads.

A Bitkit (grant) sign-in has no AuthToken to redeem, so its purchase
session comes from a browser bootstrap: the grant session writes a
single-use proof document, the Shop BFF runs the CLI verifier's checks and
opens a marketplace signin_grant, and after the Bitkit approval the BFF
claims the bearer.

The result PoP seed is derived from the BFF state key and the challenge
id (HKDF key with its own salt and info, two labelled HMACs), so no
migration is added and a key rotation inside the challenge window fails
closed. The flow is bound to the __Host-shop-marketplace-grant cookie and
sealed as a version 2 browser context; CLI routes refuse it, and the CLI
verifier refuses a browser challenge row before consume.

The connect dialog shows Bitkit copy for the bootstrap, reason codes get
static copy, and the inventory, messaging and manual-claim dialogs name
their Pubky Ring requirement.
… for both signers

The session-required card names Bitkit for a Bitkit sign-in that can run
the purchase bootstrap. The Lock Server step and its dialog now say to
approve or scan with Pubky Ring or Bitkit, since the Lock Server offers a
Bitkit QR too. The bootstrap client refuses (and cancels) a marketplace
QR that asks Bitkit for anything beyond the marketplace-service
capability.
…kit shows

The bootstrap QR now says which client Bitkit will name and that the
request covers marketplace purchases only, read from the authorization
URL. A fixture pins the staging bootstrap URL shape (signin_grant with
caps, relay, secret, cid, cpk and only the marketplace-service
capability), and the client test checks the Shop's capability pin and
caption against it.
@BitcoinErrorLog
BitcoinErrorLog changed the base branch from cursor/bitkit-signin-grant-a98e to release/shop-v0.6.8 September 24, 2026 07:55
@BitcoinErrorLog
BitcoinErrorLog merged commit aba661c 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