feat(policy): counterparty allow/deny by payTo (refuse WHO, not just HOW MUCH) - #7
Open
twzrd-sol wants to merge 1 commit into
Open
feat(policy): counterparty allow/deny by payTo (refuse WHO, not just HOW MUCH)#7twzrd-sol wants to merge 1 commit into
twzrd-sol wants to merge 1 commit into
Conversation
…HOW MUCH) Spend limits answer "how much may I pay". There is currently no way to answer "whom may I pay at all" — so an operator who knows a specific payTo is unsafe has no way to express that, short of not using the proxy. This adds `allowPayTo` / `denyPayTo` in exactly the existing spend-limit shape: x402-proxy config set denyPayTo 34w53Ukhf...,0xabc... x402-proxy config set allowPayTo BJGdsDXJ... # empty string clears Implemented as a PaymentPolicy via the existing `client.registerPolicy`, so it is pre-sign like the spend-limit policies: when no acceptable option remains the policy throws, the client never builds a payment payload, and nothing is signed. Details worth reviewing: - `deny` wins over `allow` when an address is in both — a denial is the safer reading of contradictory config. - EVM `0x` addresses match case-insensitively (hex checksumming varies); Solana base58 matches case-sensitively, because lower-casing a base58 key yields a different key. Both are covered by tests. - An empty requirements list passes through rather than throwing, so this policy never converts "nothing offered" into "counterparty refused". - The throw names every refused address plus the `config set` command, so the failure is actionable rather than opaque. - No new dependencies. Zero behaviour change when neither list is configured. Wired through the three call sites that build a client from config (fetch, mcp, serve) so the setting applies wherever payments happen, not just one command. Tests: 10 new cases in src/lib/counterparty-policy.test.ts. Package suite goes 114 -> 124 passing (9 -> 10 files); `pnpm type-check` clean; `biome check` clean.
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.
Spend limits answer how much may I pay. There is currently no way to answer whom may I pay at all — so an operator who knows a specific
payTois unsafe has no way to express that, short of not using the proxy.How it works
Implemented as a
PaymentPolicythrough the existingclient.registerPolicy, so it behaves exactly like the spend-limit policies already inresolve-wallet.ts: pre-sign. When no acceptable option remains the policy throws, the client never builds a payment payload, and nothing is signed.No new dependencies. Zero behaviour change when neither list is configured.
Decisions worth a look
denywins overallowwhen an address appears in both — a denial seemed the safer reading of contradictory config.0xaddresses match case-insensitively (hex checksumming varies); Solana base58 matches case-sensitively, because lower-casing a base58 key yields a different key. Both directions are tested, including the trap where a lower-cased Solana address must not match.config setcommand to fix it, so a failure is actionable rather than opaque.Wiring
Applied at the three sites that build a client from config —
fetch,mcp,serve— so the setting holds wherever payments happen rather than in one command.Verification
pnpm test: 114 → 124 passing (9 → 10 files); 10 new cases insrc/lib/counterparty-policy.test.tspnpm type-check: cleanbiome check: cleanHappy to adjust
If you'd rather this were a single
payToPolicyfield, or scoped per-network, or named differently, say so and I'll rework it. I kept it in thespendLimit*shape on the assumption that matching the existing config idiom matters more than novelty.One follow-up I deliberately left out of this PR: a pluggable policy module hook, so an operator could point at their own risk logic rather than maintaining a static list. That's a bigger surface and a separate security conversation, so it seemed wrong to bundle. Glad to open it separately if useful.