You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Two independent fresh-eyes reviews (payments/wallets: #534#536#544#556; Cat forget: #542#545 + open #555) produced these verified findings. The criticals are already fixed (#561: L402 receipt binding / self-attestation / preimage settlement + entity-wallets embed; #562: tool-path profile clearing; #555 carries its own blockers as a PR comment). This issue tracks the rest, ranked.
Important — payments
Recipient-side invoice-spam limiter missing on GET /api/v1/pay/... and POST /api/v1/payments/public — both mint a real invoice via the recipient's LNURL/NWC relay per request. rateLimitTipRecipient (src/lib/rate-limit.ts:214) was built for exactly this and is only applied to tips/lnurlp. An attacker can get a seller rate-limited/banned by their own wallet provider and litter orphan intents.
Rate-limit key is the leftmost X-Forwarded-For hop (client-controlled; Caddy appends). Every keyed limiter using x-forwarded-for.split(',')[0] is bypassable with a random header. Fix once in a shared clientIpKey() (also closes the 3-way duplication of IP-derivation: v1/pay route, payments/public route, rate-limit.ts).
Acknowledge as notification cannon: POST /api/v1/payments/public/{id} acknowledge + feat(wallets): lock down wallet reads + notify recipients of payment claims #544's recipient notification → free intents (finding 1) fan plausible "someone paid you" cards into a seller's confirmation queue = ship-goods-for-no-money social-engineering primitive. Needs per-recipient limiting + queue hygiene.
Settlement side-effects are at-most-once with no compensating job: claimPaidTransition flips to paid FIRST; a crash before order/inventory/notify/webhook loses them forever (refresh short-circuits on paid; cron won't re-pick). Add a side_effects_at marker + sweep for paid AND side_effects_at IS NULL.
Migration 20260802120000's stated goal not achieved: GET /api/wallets?profile_id=<any> (withOptionalAuth) serves address_or_xpub to anonymous callers via the admin client — an xpub reveals the owner's entire address history. Drop it from PUBLIC_WALLET_FIELDS (keep lightning_address) or amend the migration's claim.
L402 verify branch deliberately unlimited yet drives an outbound NWC/LNURL/mempool call per request on non-terminal intents — one valid token buys unbounded outbound traffic. Cheap per-token limiter.
Store failures indistinguishable from "no match": both stores swallow DB errors into {deleted:[], notFound: wanted} → half-done deletions reported as success, and outright query failures reported as "nothing matched". Add an explicit failed channel to ForgetResult/ProfileRemovalResult; handler returns success:false with an honest "couldn't reach your memories".
Silent cap: facts 11+ are sliced off and never reported in notFound (profile path has NO cap — another drift). Report over-cap facts.
stillUnknown compares raw vs trimmed facts — facts with padding or <4 chars silently vanish from the "no match" list.
forget_memories is the only irreversible destructive verb with riskLevel low + no confirmation while ~18 lesser actions require confirmation; tool path has no deterministic deletion listing (depends on LLM compliance). Consider confirmation or a guaranteed receipt event.
pruneIfNeeded delete not user-scoped (memory.ts ~:406) — safe today under RLS-scoped client, becomes cross-user the day it runs under service role. Add .eq('user_id', userId) for pattern consistency.
apiPaymentRequired embeds the bearer token with no Cache-Control: no-store (only payment response without it).
fix(payments): publication gate on authenticated payment initiation #536's visibility gate needs a per-entity-type test matrix (a USING (true) policy makes it a no-op; a missing anon policy 400s every payment) and is duplicated across the anon and key/OIDC paths with only one load-bearing per path.
Test gaps flagged: no handler-level test for forget_memories exec_action; embeddings floor (0.45/0.75) never executed by any test; INCIDENT_CORPUS lacks innocent memories sharing generic stems (adding "Has strong cooking skills" breaks the over-match test — intentionally).
What the reviews confirmed is SOLID (for the record)
Two independent fresh-eyes reviews (payments/wallets: #534 #536 #544 #556; Cat forget: #542 #545 + open #555) produced these verified findings. The criticals are already fixed (#561: L402 receipt binding / self-attestation / preimage settlement + entity-wallets embed; #562: tool-path profile clearing; #555 carries its own blockers as a PR comment). This issue tracks the rest, ranked.
Important — payments
GET /api/v1/pay/...andPOST /api/v1/payments/public— both mint a real invoice via the recipient's LNURL/NWC relay per request.rateLimitTipRecipient(src/lib/rate-limit.ts:214) was built for exactly this and is only applied to tips/lnurlp. An attacker can get a seller rate-limited/banned by their own wallet provider and litter orphan intents.x-forwarded-for.split(',')[0]is bypassable with a random header. Fix once in a sharedclientIpKey()(also closes the 3-way duplication of IP-derivation: v1/pay route, payments/public route, rate-limit.ts).POST /api/v1/payments/public/{id}acknowledge + feat(wallets): lock down wallet reads + notify recipients of payment claims #544's recipient notification → free intents (finding 1) fan plausible "someone paid you" cards into a seller's confirmation queue = ship-goods-for-no-money social-engineering primitive. Needs per-recipient limiting + queue hygiene.side_effects_atmarker + sweep forpaid AND side_effects_at IS NULL.wallets(*)/select('*')on WALLETS.GET /api/wallets?profile_id=<any>(withOptionalAuth) servesaddress_or_xpubto anonymous callers via the admin client — an xpub reveals the owner's entire address history. Drop it from PUBLIC_WALLET_FIELDS (keep lightning_address) or amend the migration's claim.Important — Cat forget (beyond #555's blockers)
{deleted:[], notFound: wanted}→ half-done deletions reported as success, and outright query failures reported as "nothing matched". Add an explicitfailedchannel to ForgetResult/ProfileRemovalResult; handler returns success:false with an honest "couldn't reach your memories"..eq('user_id', userId)for pattern consistency.Suggestions
refreshPaymentStatus/reconcilePaymentIntent/resolveSellerWallettaking a supabase client they no longer use — signatures advertise caller-scoped behavior that no longer exists.apiPaymentRequiredembeds the bearer token with noCache-Control: no-store(only payment response without it).USING (true)policy makes it a no-op; a missing anon policy 400s every payment) and is duplicated across the anon and key/OIDC paths with only one load-bearing per path.INCIDENT_CORPUSlacks innocent memories sharing generic stems (adding "Has strong cooking skills" breaks the over-match test — intentionally).What the reviews confirmed is SOLID (for the record)
claimPaidTransitionconditional-update is genuinely atomic — no double-spend path found.nwc_connection_urinever reaches Cat context (mapped tohas_nwcboolean under service role).