Skip to content

Add CTX spend-api pubkey auth prototype - #6147

Open
j0ntz wants to merge 3 commits into
developfrom
jon/ctx-spend-prototype
Open

Add CTX spend-api pubkey auth prototype#6147
j0ntz wants to merge 3 commits into
developfrom
jon/ctx-spend-prototype

Conversation

@j0ntz

@j0ntz j0ntz commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Technical Design Document

ctx-spend-pubkey-auth.md

CHANGELOG

Does this branch warrant an entry to the CHANGELOG?

  • Yes
  • No

Dependencies

none

Description

Asana: https://app.asana.com/0/1215088146871429/1216403654258326

CTX published a reference script for an anonymous secp256k1 register/login protocol against their spend-api (CTX-com/spend-api-pubkey-auth-demo). This is a prototype client for that protocol, wired into the existing EdgeSpend gift card plugin.

GET /me against CTX returns gift-card-scoped permissions (giftcard:create, giftcard:list, giftcard:redeem, merchant:list) and GET /merchants returns a 118-entry brand catalog, so CTX is a competing provider for what the Phaze plugin already does. The client is added alongside Phaze; the shipping purchase flow is untouched.

The design doc committed in this PR covers the protocol, the decisions, and the two places the reference script's crypto could not be ported verbatim: src/docs/ctx-spend-pubkey-auth.md.

What is here:

  • ctxSpendCrypto.ts — the protocol wire format: sha256(uint64BE(nonce)) digest, the 65-byte recoverable signature ([27 + recoveryId + 4] || R || S), public key recovery, and JWT expiry parsing. No React Native imports, so it is unit tested in Node.
  • ctxSpendAuth.ts — the keypair is the CTX account. It is generated on first use and kept in the Edge account's encrypted dataStore, so the same anonymous CTX user is recovered across launches and devices. The reference script deliberately discards its keypair; a real client cannot. Tokens are derived state and are never persisted.
  • ctxSpendApi.ts — the confirmed read surface (/me, /merchants, /gift-cards), with one re-auth retry on a 401.
  • PLUGIN_API_KEYS.ctxSpend (clientId, baseUrl). There is no API key; the client id is registered server-side and the keypair identifies the user.
  • A CTX section on the existing developer-facing gift card account info scene, so the session can be driven in the app.

Two substitutions the runtime forced, both covered in the doc's Decisions:

  • No BigInt in the uint64 encoding. index.ts shims BigInt with big-integer when it is missing, and DataView.setBigUint64 rejects that shim.
  • Request URLs are built by string, not via URL. React Native's URL appends a trailing slash to any query-less path, and the spend-api routes /me/ and /merchants/ to 404.

No new npm dependency: @noble/curves and @noble/hashes were already present, and are the same libraries the reference script uses.

Not in scope: replacing Phaze, purchase and redemption, a shared provider interface over both APIs, and identity rotation.

Test plan

  • 13 new jest cases in src/__tests__/ctxSpendCrypto.test.ts. The load-bearing one recovers the signing public key from the signature, which is the same check the server performs, so the wire format is verified without a network round trip. Vectors were cross-checked against Buffer.writeBigUInt64BE and node:crypto.
  • verify-repo.sh passes: eslint, npm run prepare, and the full 536-case suite.
  • Driven in-app on an iOS 18.6 simulator against https://staging.spend.ctx.com. The app generated its own keypair, registered it, and read live data: 12 permissions, 118 merchants, company Edge. The server named the anonymous user Anon 032129df after the public key's own prefix.
  • Persistence checked by a full app stop and relaunch: the same user id and public key came back, so the keypair is reloaded from the encrypted store rather than regenerated.

Screenshots in the test-evidence comments below.

Bugbot found 6 issues across three rounds, all fixed and re-tested on the sim. Four were in the first round: a dataStore read failure reading as "no identity" and silently minting a replacement keypair (the worst of them, since CTX has no recovery), malformed stored hex throwing past the replace path, a light-account status conflated with a failed write, and Connect not being retryable. The retry fix then surfaced two follow-ons it had created or exposed: an ungated button that could stack sessions, and the app-wide retry: 2 turning one failed connect into three login handshakes against a rate-limited API. Decision 8.6 in the design doc records the error-handling contract that came out of the first round.

Requirements

If you have made any visual changes to the GUI. Make sure you have:

  • Tested on iOS device
  • Tested on Android device
  • Tested on small-screen device (iPod Touch)
  • Tested on large-screen device (tablet)

@j0ntz

j0ntz commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

📸 Test evidence

ctx before connect

ctx before connect

ctx authenticated

ctx authenticated

ctx persisted after relaunch

ctx persisted after relaunch

Captured by the agent's in-app test run (build-and-test).

@j0ntz
j0ntz force-pushed the jon/ctx-spend-prototype branch from 536021a to 09ab729 Compare August 10, 2026 21:02
@j0ntz
j0ntz marked this pull request as ready for review August 10, 2026 21:02
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Comment thread src/components/scenes/GiftCardAccountInfoScene.tsx
Comment thread src/components/scenes/GiftCardAccountInfoScene.tsx Outdated
Comment thread src/plugins/gift-cards/ctxSpendAuth.ts
Comment thread src/plugins/gift-cards/ctxSpendAuth.ts
Implements the anonymous secp256k1 register/login protocol documented by
CTX-com/spend-api-pubkey-auth-demo, plus the read endpoints it unlocks.

The keypair is the CTX account: it is generated on first use and kept in the
Edge account's encrypted dataStore, so the same anonymous CTX user is recovered
across launches and devices. Tokens are derived state and are never persisted.

The protocol crypto is kept free of React Native imports so its wire format is
unit testable in Node, and it avoids BigInt because Hermes falls back to a
big-integer shim that DataView.setBigUint64 rejects.
@j0ntz
j0ntz force-pushed the jon/ctx-spend-prototype branch from 09ab729 to 428ff98 Compare August 10, 2026 21:16
Comment thread src/components/scenes/GiftCardAccountInfoScene.tsx
@j0ntz
j0ntz force-pushed the jon/ctx-spend-prototype branch from 428ff98 to 7151140 Compare August 10, 2026 21:25

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 7151140. Configure here.

Comment thread src/components/scenes/GiftCardAccountInfoScene.tsx
j0ntz added 2 commits August 10, 2026 14:34
Gives the pubkey auth prototype a surface that can be driven in the running
app: connecting establishes the keypair identity, logs in, and reads the
authenticated user, company, permission count, and merchant catalog size.

The scene already exists as the developer-facing readout for this plugin, so
the prototype rides along without touching the shipping purchase flow.
Documents the protocol as implemented: the wire format, where the keypair
lives and why, the token lifecycle, and the decisions behind each, including
the two places the reference script's crypto could not be copied verbatim.
@j0ntz
j0ntz force-pushed the jon/ctx-spend-prototype branch from 7151140 to e222388 Compare August 10, 2026 21:35
@j0ntz

j0ntz commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

📸 Test evidence (after review fixes)

ctx persisted after relaunch

ctx persisted after relaunch

Captured by the agent's in-app test run (build-and-test).

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