test(wallets): the column list and the GRANT that makes it readable can drift - #821
Merged
Conversation
…an drift Migration 20260802120000 revoked table-level SELECT on wallets and replaced it with an explicit COLUMN grant, so nwc_connection_uri — an encrypted wallet-spending credential — is write-only for client roles. Every non-service client therefore selects WALLET_CLIENT_COLUMNS, the code-side mirror of that grant. Two sources of truth for one fact, and nothing enforced the match. Postgres does not auto-grant a column added later. So: add a column to wallets, add it to WALLET_CLIENT_COLUMNS because your query needs it, and every wallet read on the site starts failing with 42501 permission-denied for anon/authenticated — not just the new feature. It presents as a broken page rather than as a missing grant, which is how #561 was spent finding it once already. #563 finding 5. Three assertions: the GRANT list and the TS constant name the same columns; neither ever names a secret column; and any column a LATER migration adds to wallets is granted somewhere or declared secret. Verified against production rather than assumed — anon currently holds SELECT on exactly the 25 columns in the constant, the table has 26, and the one withheld is nwc_connection_uri. Proven to fail in both directions before shipping. Adding a column to the TS constant alone reports it under codeOnly; a later migration adding a column with no grant names it. Restored, all 8 pass. The third assertion currently scans a set with nothing in it — no migration adds a wallets column yet — so it logs how many it scanned and how many it found. A silent pass over an empty set reads exactly like a pass over a checked one, and five fixture tests prove the parsers detect rather than quietly matching nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018waGt1ieA9TjpscqrbrnGb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes finding 5 of #563.
Migration
20260802120000revoked table-level SELECT onwalletsand replaced it with an explicit column grant, sonwc_connection_uri— an encrypted wallet-spending credential — is write-only for client roles. Every non-service client therefore selectsWALLET_CLIENT_COLUMNS, the code-side mirror of that grant.Two sources of truth for one fact, and nothing enforced the match.
Why it bites harder than it looks
Postgres does not auto-grant a column added later. So: add a column to
wallets, add it toWALLET_CLIENT_COLUMNSbecause your query needs it, and every wallet read on the site starts failing with42501permission-denied for anon/authenticated — not just the new feature.It presents as a broken page rather than as a missing grant, which is how #561 was spent finding it once already.
What it asserts
walletsis granted somewhere, or declared secret.Verified against production, not assumed
anoncurrently holds SELECT on exactly the 25 columns in the constant; the table has 26; the one withheld isnwc_connection_uri.Proven to fail in both directions
codeOnlysweep_keyAssertion 3 currently scans a set with nothing in it — no migration adds a wallets column yet — so it logs how many it scanned and how many it found. A silent pass over an empty set reads exactly like a pass over a checked one. Five fixture tests prove the parsers detect rather than quietly matching nothing, including a GRANT on a different table and an
ADD COLUMNon a different table.type-checkgreen; 8 tests pass.Also checked while here
Finding 6 is already fixed — and better than the audit proposed.
redactExtendedKeys(added 2026-08-24, after the audit) classifies by value rather than the user-setwallet_typelabel, so a key pasted into a row typedonchainis still redacted. Verified live: an anonymous request for a profile whose wallet genuinely holds an extended key returnsaddress_or_xpub: null.Finding 3 is substantially closed by #819.
notifyRecipientOfClaimfires only on the transition intobuyer_confirmed, never on idempotent re-claims — so the notification count is bounded by intent count, and #819 capped intent creation at 20 per 5 minutes per recipient. The unbounded primitive is gone.