Skip to content

Stop showing "Connected" for a CoinPay link that cannot send invoices - #553

Merged
ralyodio merged 1 commit into
masterfrom
fix/coinpay-wallet-scope-reconnect
Sep 9, 2026
Merged

Stop showing "Connected" for a CoinPay link that cannot send invoices#553
ralyodio merged 1 commit into
masterfrom
fix/coinpay-wallet-scope-reconnect

Conversation

@ralyodio

@ralyodio ralyodio commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

The report

A worker registered, connected CoinPay, and tried to invoice $0.25. ugig answered:

Connect your CoinPay account before sending an invoice

Their reply: "CoinPay is connected on my side, but invoicing still failed." Both statements were true, and reconnecting could never have fixed it.

Root cause

Two surfaces asked different questions about the same row:

  • settings/connections rendered Connected for any oauth_identities row.
  • getConnectedCoinpayAccessToken() additionally required wallet:read on the stored token, returning null without it — which the invoice route reports as "not connected".

The tokens genuinely lacked the scope. The ugig.net OAuth client on CoinPay (cp_02526f194fc21f6c659a71b6) was registered for openid profile email payments:read payments:write — no wallet:read, and payments:* are not scopes CoinPay defines. That was harmless until coinpayportal #257 (2026-08-15) began intersecting requested scopes with each client's registration; from then on CoinPay silently dropped wallet:read from every grant.

The data shows the cliff exactly — 61 identities carry wallet:read (last one 2026-08-16), and all 22 since carry only openid profile email, including the reporter's.

The client registration has been corrected on CoinPay (wallet:read added), so new and re-authorized links work. This PR fixes the contradiction users were shown in the meantime.

This change

One shared predicate, coinpayLinkCanReadWallets(), used by both surfaces. A link that cannot read wallets now shows Reconnect required with an explanation, instead of a green Connected badge that the invoice form contradicts.

The invoice gate's behaviour is unchanged — this only makes the connections page tell the same story.

Verification

  • tsc --noEmit clean (confirmed non-vacuous with a deliberate probe error).
  • 69 existing coinpay/invoice/auth tests pass; 7 new tests for the predicate.
  • pnpm lint: 0 errors (40 pre-existing warnings in untouched files).
  • Grant logic checked against the real registered arrays: before → openid profile email; after → openid profile email wallet:read.

Note for the 22 affected users

Their stored tokens stay scope-less until they re-authorize. They will now be told to, rather than seeing "Connected" and hitting the wall at invoice time.

🤖 Generated with Claude Code

https://claude.ai/code/session_01C5k3DbNSgAqZ1HGjicgLoB

A worker connected CoinPay, saw a green "Connected" badge on Settings >
Connections, and still got "Connect your CoinPay account before sending an
invoice" from the same account. Reconnecting did not help, because the two
surfaces were asking different questions: the page called any oauth_identities
row connected, while the invoice route additionally required wallet:read on the
stored token and treated a link without it as absent.

The tokens really did lack the scope. The ugig.net OAuth client on CoinPay was
registered for openid/profile/email (plus two scopes CoinPay does not define),
never wallet:read. That went unnoticed until coinpayportal #257 started
intersecting requested scopes with the client's registration, from which point
CoinPay filtered wallet:read out of every grant. Every link made since carries
openid profile email and can never read wallets, so no amount of reconnecting
could fix it from this side. The client registration has been corrected
separately; this change is about the contradiction the user was shown.

Both surfaces now share one predicate, coinpayLinkCanReadWallets(), so a link
that cannot do the job it exists for reports "Reconnect required" with an
explanation on the connections page instead of claiming to be connected. The
invoice gate is unchanged in behaviour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5k3DbNSgAqZ1HGjicgLoB
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

ThreatCrush Security Scan

45 finding(s)

HIGH/CRITICAL: 1 | MEDIUM: 8 | LOW: 36

Severity Rule Location
HIGH js-ssrf-outbound-request scripts/scan-all-skills.ts:38
MEDIUM js-open-redirect src/app/agent-login/AgentLoginForm.tsx:38
MEDIUM js-unescaped-html-sink src/app/blog/[slug]/page.tsx:79
MEDIUM js-open-redirect src/app/dashboard/subscription/page.tsx:90
MEDIUM js-open-redirect src/app/dashboard/subscription/page.tsx:113
MEDIUM js-open-redirect src/app/dashboard/subscription/page.tsx:144
MEDIUM js-open-redirect src/components/funding/FundingClient.tsx:137
MEDIUM js-dynamic-code-execution src/lib/skills/metadata-extract.ts:300
MEDIUM js-dynamic-code-execution src/lib/skills/security-scan.ts:48
LOW secret-generic-credential cli/src/commands/auth.test.ts:66
LOW secret-generic-credential cli/src/commands/auth.test.ts:85
LOW secret-generic-api-key docs/agents/integration-guide.md:893
LOW secret-generic-credential src/app/api/auth/login/route.test.ts:53
LOW secret-generic-credential src/app/api/auth/login/route.test.ts:68
LOW secret-generic-credential src/app/api/auth/login/route.test.ts:87
LOW secret-generic-credential src/app/api/auth/signup/route.test.ts:158
LOW secret-generic-credential src/app/api/auth/signup/route.test.ts:182
LOW secret-generic-credential src/app/api/auth/signup/route.test.ts:193
LOW secret-generic-credential src/app/api/auth/signup/route.test.ts:232
LOW js-dynamic-code-execution src/app/api/skills/[slug]/scan/route.test.ts:212
LOW js-dynamic-code-execution src/app/api/skills/[slug]/scan/route.test.ts:223
LOW js-dynamic-code-execution src/app/api/skills/[slug]/scan/route.test.ts:239
LOW secret-generic-credential src/lib/api.test.ts:126
LOW secret-generic-credential src/lib/api.test.ts:131
LOW js-dynamic-code-execution src/lib/skills/composite-scanner.test.ts:106
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:36
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:44
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:66
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:81
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:94
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:103
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:118
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:144
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:162
LOW js-dynamic-code-execution src/lib/skills/security-scan.test.ts:28
LOW js-dynamic-code-execution src/lib/skills/url-import.test.ts:178
LOW js-dynamic-code-execution src/lib/skills/url-import.test.ts:191
LOW secret-generic-credential src/lib/validations.test.ts:148
LOW secret-generic-credential src/lib/validations.test.ts:512
LOW secret-generic-credential src/lib/validations.test.ts:523
LOW secret-generic-credential src/lib/validations.test.ts:538
LOW secret-generic-credential src/lib/validations.test.ts:548
LOW secret-generic-credential src/lib/validations.test.ts:557
LOW secret-generic-credential src/lib/validations.test.ts:567
LOW secret-generic-credential src/lib/validations.test.ts:582

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit d922732 into master Sep 9, 2026
6 checks passed
ralyodio added a commit that referenced this pull request Sep 9, 2026
Reported from the agenticjobs apply flow: an agent tried to invoice $0.25 and
ugig answered "Connect your CoinPay account before sending an invoice". CoinPay
was connected. It tried again, and got the same sentence.

The gate is right and the sentence is wrong. A CoinPay link authorised before
ugig needed wallet:read cannot read global wallet addresses, so it cannot back
an invoice, and getConnectedCoinpayAccessToken correctly returns null for it.
But null carries no reason, so the route reported the one case it could name:
not connected. That is an instruction to do something the worker has already
done, which is why it loops. They check, see a connection, and try again.

#553 fixed this on the connections page, which now says "Reconnect required"
rather than "Connected". The API never learned the difference, and the API is
what an agent gets: it never sees that page, so the sentence in the 409 is its
only instruction.

So the reason travels with the token now. getCoinpayLink returns
none | needs_reconnect | connected, and the invoice route picks its message and
its setup_instructions from that. The steps for a stale link lead with
reconnecting and say what reconnecting does and does not change, because "your
gigs are untouched" is the question anyone hesitates on.

getConnectedCoinpayAccessToken stays, as a wrapper, so the wallets route and
the bounty payout route are untouched. oauth_required still reports true for a
worker in both cases: reconnecting is the same authorise round trip, so a
client that keys on the flag to offer the button keeps working. The new
coinpay_link_state field is what a caller should branch on.

Two tests: the stale link must not receive the exact sentence that sent the
reporter in circles, and a genuinely absent link must still be told to connect,
so this cannot be "fixed" by wording everything as reconnect.

2104 tests pass across 219 files. Typecheck clean.


Claude-Session: https://claude.ai/code/session_01WxWrsbLuaSaZqQ7FFTrdVW

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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