diff --git a/.taskmaster/tasks/tasks.json b/.taskmaster/tasks/tasks.json index 549cbc0..9fabf96 100644 --- a/.taskmaster/tasks/tasks.json +++ b/.taskmaster/tasks/tasks.json @@ -631,7 +631,7 @@ { "id": 10, "title": "Implement Telegram as first-class authentication provider", - "description": "Add a POST /auth/telegram endpoint and frontend login flow so users can authenticate with Amanat using only their Telegram identity \u2014 no email or password required.", + "description": "Add a POST /auth/telegram endpoint and frontend login flow so users can authenticate with Amanat using only their Telegram identity — no email or password required.", "details": "Source PRD: .taskmaster/docs/prd-telegram-phone-auth.md. Backend: create POST /auth/telegram that accepts Mini App initData or Telegram Login Widget payload, verifies the signature (reuse verifyMiniAppInitData; add verifyTelegramLoginWidget for the widget path), looks up TelegramLink by telegramUserId, and either authenticates the linked user or auto-provisions a new Amanat account (authProvider: telegram, telegramVerified: true, nullable email via sparse unique index). Returns JWT + refreshToken + isNewUser flag. Apply existing replay protection and rate limits. User model: make email nullable (sparse index), add authProvider and telegramVerified fields. Frontend: auto-detect Telegram Mini App context and skip login page; POST initData to /auth/telegram; show lightweight onboarding overlay for new users (optional email, language, currency). Add 'Continue with Telegram' button on web login page alongside Google OAuth. Security: blocked Telegram accounts return 403 regardless of re-linking attempts; high-risk action step-up policy is unchanged; never expose raw phone number.", "status": "done", "dependencies": [ @@ -650,7 +650,7 @@ "id": "6", "title": "Request Network in-house checkout (Rabby-supporting)", "description": "Replace the redirect to pay.request.network with an Amanat-rendered checkout page that submits the same on-chain calls as RN's hosted UI, so RN's webhook fires unchanged but buyers stay on amn.gg and Rabby works.", - "details": "See PRD: nick-doc/.taskmaster/docs/prd-request-network-in-house-checkout.md (summary at nick-doc/PRD - Request Network In-House Checkout.md). Status: draft, pending review with second developer. Approach: replicate the two on-chain calls (approve + RN_FEE_PROXY.transferFromWithReferenceAndFee) using wagmi v2 with existing injected()/metaMask() connectors (Rabby works via EIP-6963). Hard-known: proxy 0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9, selector 0xc219a14d, paymentRef = last8Bytes(keccak256(requestId+salt+dest)), feeAmount=0, feeAddress=0x...dEaD. Backend: extend POST /payment/request-network/intents response with inHouseCheckout object (destination, tokenAddress, decimals, chainId, proxyAddress, paymentReference, feeAmount, feeAddress, amountWei). Frontend: new page /checkout/request-network/:paymentId with state machine reusing manual-payment.tsx layout chrome, hosted-page link kept as escape hatch. Implementation gated on a $0.50 cold probe on dev BSC to confirm RN's webhook fires for an externally-built tx. Out of scope: per-seller multi-chain config (\u00a72), ephemeral wallets (\u00a73), full RN removal (\u00a74), gasless. Open questions in PRD \u00a710.", + "details": "See PRD: nick-doc/.taskmaster/docs/prd-request-network-in-house-checkout.md (summary at nick-doc/PRD - Request Network In-House Checkout.md). Status: draft, pending review with second developer. Approach: replicate the two on-chain calls (approve + RN_FEE_PROXY.transferFromWithReferenceAndFee) using wagmi v2 with existing injected()/metaMask() connectors (Rabby works via EIP-6963). Hard-known: proxy 0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9, selector 0xc219a14d, paymentRef = last8Bytes(keccak256(requestId+salt+dest)), feeAmount=0, feeAddress=0x...dEaD. Backend: extend POST /payment/request-network/intents response with inHouseCheckout object (destination, tokenAddress, decimals, chainId, proxyAddress, paymentReference, feeAmount, feeAddress, amountWei). Frontend: new page /checkout/request-network/:paymentId with state machine reusing manual-payment.tsx layout chrome, hosted-page link kept as escape hatch. Implementation gated on a $0.50 cold probe on dev BSC to confirm RN's webhook fires for an externally-built tx. Out of scope: per-seller multi-chain config (§2), ephemeral wallets (§3), full RN removal (§4), gasless. Open questions in PRD §10.", "testStrategy": "", "status": "done", "dependencies": [], @@ -674,7 +674,7 @@ "id": "7", "title": "Per-(buyer, sellerOffer) ephemeral RN destination wallets", "description": "Replace the single shared Amanat destination wallet with a per-(buyerId, sellerOfferId) HD-derived address sent to Request Network on intent creation, plus sweep-on-approval and an admin UI.", - "details": "See PRD - Wallet, Multichain, Confirmations, AML, Trezor.md \u00a71. Files: new backend/src/services/payment/wallets/derivedDestinations.ts (getDestinationFor(buyerId, sellerOfferId) \u2192 {address, derivationPath, chainId}); Payment schema add metadata.derivedDestination; requestNetworkPayInService.ts override destinationId before POST /v2/secure-payments (we confirmed RN accepts different destinations per intent); new sweep cron + admin manual-trigger endpoint gated on Transaction Safety Provider; admin UI at /dashboard/admin/derived-destinations with address, balance, last sweep tx (BscScan link), ownership status. Open questions to settle first: HD vs disposable EOAs vs smart-forwarder (recommended HD); sweep cadence (recommended immediate); granularity (recommended per-(buyer, seller), not per-payment); re-use vs rotate after sweep. KMS-rooted seed; backend never holds raw private keys; signing via KMS API (Task #11 Trezor flow is the longer-term replacement). Acceptance: two payments from one buyer to two sellers land on two different addresses; RN webhook fires for both; sweep is idempotent; master seed never leaves KMS.", + "details": "See PRD - Wallet, Multichain, Confirmations, AML, Trezor.md §1. Files: new backend/src/services/payment/wallets/derivedDestinations.ts (getDestinationFor(buyerId, sellerOfferId) → {address, derivationPath, chainId}); Payment schema add metadata.derivedDestination; requestNetworkPayInService.ts override destinationId before POST /v2/secure-payments (we confirmed RN accepts different destinations per intent); new sweep cron + admin manual-trigger endpoint gated on Transaction Safety Provider; admin UI at /dashboard/admin/derived-destinations with address, balance, last sweep tx (BscScan link), ownership status. Open questions to settle first: HD vs disposable EOAs vs smart-forwarder (recommended HD); sweep cadence (recommended immediate); granularity (recommended per-(buyer, seller), not per-payment); re-use vs rotate after sweep. KMS-rooted seed; backend never holds raw private keys; signing via KMS API (Task #11 Trezor flow is the longer-term replacement). Acceptance: two payments from one buyer to two sellers land on two different addresses; RN webhook fires for both; sweep is idempotent; master seed never leaves KMS.", "testStrategy": "", "status": "in-progress", "dependencies": [], @@ -686,7 +686,7 @@ "id": "8", "title": "Multichain RN proxy registry + USDC/USDT support", "description": "Probe and persist RN ERC20FeeProxy addresses on BSC/Arb/ETH/Polygon/Base, add USDC + USDT token entries with correct decimals per chain, and surface an admin networks page. Include the USDT-mainnet approve(0) reset quirk in the frontend approve step.", - "details": "See PRD - Wallet, Multichain, Confirmations, AML, Trezor.md \u00a72. Tasks: new backend/scripts/probe-rn-chains.ts that walks each chain in supported-chains.json and verifies the canonical 0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9 proxy is the real RN proxy via a known view fn (CREATE2 is deterministic, but verify); promote backend/src/services/payment/requestNetwork/tokens.ts to load from JSON + admin override; add USDT entries on all 5 chains (BSC USDT 18-dec quirk, mainnet/Arb/Polygon/Base USDT 6-dec); buildInHouseCheckoutBlock returns reason='unsupported_chain:' for unknowns; new admin route GET /api/admin/rn/networks + frontend page /dashboard/admin/networks rendering the registry with per-row 'probe again'. Frontend approve flow: if buyer is on Ethereum mainnet AND token is USDT AND current allowance > 0, do approve(spender, 0) first then approve(spender, amount). Acceptance: probe succeeds on at least BSC/Arb/Polygon/ETH/Base; one paid probe on BSC USDT end-to-end; mainnet USDT approve(0) reset works; admin page reflects registry. Dependencies: none \u2014 runs in parallel with #9. This is task #8 in the PRD.", + "details": "See PRD - Wallet, Multichain, Confirmations, AML, Trezor.md §2. Tasks: new backend/scripts/probe-rn-chains.ts that walks each chain in supported-chains.json and verifies the canonical 0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9 proxy is the real RN proxy via a known view fn (CREATE2 is deterministic, but verify); promote backend/src/services/payment/requestNetwork/tokens.ts to load from JSON + admin override; add USDT entries on all 5 chains (BSC USDT 18-dec quirk, mainnet/Arb/Polygon/Base USDT 6-dec); buildInHouseCheckoutBlock returns reason='unsupported_chain:' for unknowns; new admin route GET /api/admin/rn/networks + frontend page /dashboard/admin/networks rendering the registry with per-row 'probe again'. Frontend approve flow: if buyer is on Ethereum mainnet AND token is USDT AND current allowance > 0, do approve(spender, 0) first then approve(spender, amount). Acceptance: probe succeeds on at least BSC/Arb/Polygon/ETH/Base; one paid probe on BSC USDT end-to-end; mainnet USDT approve(0) reset works; admin page reflects registry. Dependencies: none — runs in parallel with #9. This is task #8 in the PRD.", "testStrategy": "", "status": "done", "dependencies": [], @@ -698,51 +698,55 @@ "id": "9", "title": "Per-chain confirmation thresholds + admin UI", "description": "Make TransactionSafetyProvider's confirmation threshold tunable at runtime per chain via admin UI, with an awaiting-confirmation payments view that shows live confirmations vs threshold.", - "details": "See PRD - Wallet, Multichain, Confirmations, AML, Trezor.md \u00a73. Today TRANSACTION_SAFETY_MIN_CONFIRMATIONS is a global env var, default 12, baked in until redeploy. Move to runtime config: new Setting docs keyed 'confirmation_threshold:' or extend existing model; cache reads in transactionSafetyProvider.ts for 30s; GET/PATCH /api/admin/settings/confirmation-thresholds (auth: admin); new admin page /dashboard/admin/confirmation-thresholds (table: chain, current, recommended default, edit-in-place with confirm dialog, audit log of changes); new admin page /dashboard/admin/payments/awaiting-confirmation (payments where escrowState !== 'funded' AND metadata.transactionSafety.lastCheck.status === 'pending'; for each show tx hash linked to explorer, current confirmations via 12s poll on BSC, threshold, ETA). Acceptance: admin lowers BSC threshold from 12 to 3 on dev, next webhook honors new value within 30s; awaiting-confirmation table updates live; audit log records every change. Non-goals: per-asset, per-seller thresholds. Dependencies: none. This is task #9 in the PRD.", + "details": "See PRD - Wallet, Multichain, Confirmations, AML, Trezor.md §3. Today TRANSACTION_SAFETY_MIN_CONFIRMATIONS is a global env var, default 12, baked in until redeploy. Move to runtime config: new Setting docs keyed 'confirmation_threshold:' or extend existing model; cache reads in transactionSafetyProvider.ts for 30s; GET/PATCH /api/admin/settings/confirmation-thresholds (auth: admin); new admin page /dashboard/admin/confirmation-thresholds (table: chain, current, recommended default, edit-in-place with confirm dialog, audit log of changes); new admin page /dashboard/admin/payments/awaiting-confirmation (payments where escrowState !== 'funded' AND metadata.transactionSafety.lastCheck.status === 'pending'; for each show tx hash linked to explorer, current confirmations via 12s poll on BSC, threshold, ETA). Acceptance: admin lowers BSC threshold from 12 to 3 on dev, next webhook honors new value within 30s; awaiting-confirmation table updates live; audit log records every change. Non-goals: per-asset, per-seller thresholds. Dependencies: none. This is task #9 in the PRD.", "testStrategy": "", - "status": "pending", + "status": "done", "dependencies": [], "priority": "medium", - "subtasks": [] + "subtasks": [], + "updatedAt": "2026-05-29T09:51:57.565Z" }, { "id": "10", "title": "Optional AML screening on incoming payments (seller-paid)", "description": "Turn the existing aml_screening placeholder in TransactionSafetyProvider into a real Chainalysis (or equivalent) Address Screening call that the seller opts into per-offer and pays the per-check cost for.", - "details": "See PRD - Wallet, Multichain, Confirmations, AML, Trezor.md \u00a74. Default provider recommendation: Chainalysis Address Screening (cheapest, simplest). Files: new backend/src/services/payment/safety/amlProvider.ts interface + chainalysisProvider.ts impl behind env TRANSACTION_SAFETY_AML_PROVIDER=chainalysis with API_KEY in KMS; transactionSafetyProvider's evaluateAmlPlaceholder() becomes real, persists raw provider response on Payment.metadata.amlResult; Offer schema add requireAmlCheck + amlBlockOnFailure booleans; offer-edit UI toggle 'Require AML on incoming payments ($X per payment, paid by you)'; admin global config UI for provider selection + API key rotation + per-chain enabled flag; cost accounting: deduct per-check cost from seller's escrow on completion as a separate ledger line item, surfaced on payment-details. Open questions before code: pick provider (Chainalysis vs TRM vs Elliptic \u2014 need 1-page comparison of cost/latency/coverage); failure mode (fail-closed only when seller opted in AND amlBlockOnFailure=true, else warn/log); cost batching cadence. Acceptance: seller toggles AML on an offer; incoming payment triggers a real Chainalysis call; sanctions verdict blocks the safety gate; clean verdict passes; seller's settled amount reduced by check cost; admin can rotate API key without redeploy; provider-down + amlBlockOnFailure=true keeps payment pending with provider_unavailable reason. Dependencies: none. This is task #10 in the PRD.", + "details": "See PRD - Wallet, Multichain, Confirmations, AML, Trezor.md §4. Default provider recommendation: Chainalysis Address Screening (cheapest, simplest). Files: new backend/src/services/payment/safety/amlProvider.ts interface + chainalysisProvider.ts impl behind env TRANSACTION_SAFETY_AML_PROVIDER=chainalysis with API_KEY in KMS; transactionSafetyProvider's evaluateAmlPlaceholder() becomes real, persists raw provider response on Payment.metadata.amlResult; Offer schema add requireAmlCheck + amlBlockOnFailure booleans; offer-edit UI toggle 'Require AML on incoming payments ($X per payment, paid by you)'; admin global config UI for provider selection + API key rotation + per-chain enabled flag; cost accounting: deduct per-check cost from seller's escrow on completion as a separate ledger line item, surfaced on payment-details. Open questions before code: pick provider (Chainalysis vs TRM vs Elliptic — need 1-page comparison of cost/latency/coverage); failure mode (fail-closed only when seller opted in AND amlBlockOnFailure=true, else warn/log); cost batching cadence. Acceptance: seller toggles AML on an offer; incoming payment triggers a real Chainalysis call; sanctions verdict blocks the safety gate; clean verdict passes; seller's settled amount reduced by check cost; admin can rotate API key without redeploy; provider-down + amlBlockOnFailure=true keeps payment pending with provider_unavailable reason. Dependencies: none. This is task #10 in the PRD.", "testStrategy": "", - "status": "pending", + "status": "done", "dependencies": [], "priority": "medium", - "subtasks": [] + "subtasks": [], + "updatedAt": "2026-05-29T10:00:28.716Z" }, { "id": "11", "title": "Trezor signing for admin actions (release/refund/sweep)", "description": "Replace the hot-key admin signing flow with a WebUSB-based Trezor flow so the backend never holds a private key. All admin-side txes are built backend, signed via Trezor in the browser, broadcast from the browser.", - "details": "See PRD - Wallet, Multichain, Confirmations, AML, Trezor.md \u00a75. Lib: @trezor/connect-web (WebUSB; Chromium-only \u2014 Firefox users need Trezor Bridge native helper). Files: new frontend/src/web3/trezor/trezorConnector.ts wrapping @trezor/connect-web; existing admin actions (release/refund/sweep when #7 lands) get a 'Sign with Trezor' button that flows: POST /api/admin/actions/build-tx \u2192 returns unsigned tx bytes \u2192 send to Trezor \u2192 sign \u2192 wagmi sendTransaction broadcasts \u2192 POST /api/admin/actions/confirm-tx with hash; admin settings page to register Trezor address(es) (backend rejects signatures from unauthorized devices); audit log on every Trezor-signed action; break-glass hot-key path requires explicit admin toggle, expires after 1h, fires Telegram alarm. Open questions: m-of-n multi-admin signing \u2014 default single-signer for v1; Trezor One vs Model T \u2014 lib abstracts; fallback when Trezor unavailable \u2014 break-glass with alarm. Acceptance: admin registers Trezor address; release flow uses Trezor end-to-end; backend rejects signatures from unregistered devices; audit log captures admin user + Trezor addr + tx hash + before/after escrow state; break-glass works and alarms. Non-goals: mobile Trezor flow, buyer-side Trezor (buyer uses wagmi injected). Dependencies: task #7 (ephemeral wallets) for the sweep step \u2014 but task #11 can ship the release/refund flows first. This is task #11 in the PRD.", + "details": "See PRD - Wallet, Multichain, Confirmations, AML, Trezor.md §5. Lib: @trezor/connect-web (WebUSB; Chromium-only — Firefox users need Trezor Bridge native helper). Files: new frontend/src/web3/trezor/trezorConnector.ts wrapping @trezor/connect-web; existing admin actions (release/refund/sweep when #7 lands) get a 'Sign with Trezor' button that flows: POST /api/admin/actions/build-tx → returns unsigned tx bytes → send to Trezor → sign → wagmi sendTransaction broadcasts → POST /api/admin/actions/confirm-tx with hash; admin settings page to register Trezor address(es) (backend rejects signatures from unauthorized devices); audit log on every Trezor-signed action; break-glass hot-key path requires explicit admin toggle, expires after 1h, fires Telegram alarm. Open questions: m-of-n multi-admin signing — default single-signer for v1; Trezor One vs Model T — lib abstracts; fallback when Trezor unavailable — break-glass with alarm. Acceptance: admin registers Trezor address; release flow uses Trezor end-to-end; backend rejects signatures from unregistered devices; audit log captures admin user + Trezor addr + tx hash + before/after escrow state; break-glass works and alarms. Non-goals: mobile Trezor flow, buyer-side Trezor (buyer uses wagmi injected). Dependencies: task #7 (ephemeral wallets) for the sweep step — but task #11 can ship the release/refund flows first. This is task #11 in the PRD.", "testStrategy": "", - "status": "pending", + "status": "done", "dependencies": [], "priority": "high", - "subtasks": [] + "subtasks": [], + "updatedAt": "2026-05-29T10:50:02.957Z" }, { "id": "12", "title": "Replace auth rate limiter with CAPTCHA (Cloudflare Turnstile or reCAPTCHA v3)", - "description": "The current authLimiter blocks all login attempts from an IP for 15 minutes after N failures. This creates terrible UX (legitimate users get locked out, especially during testing) and is bypassable via rotating IPs anyway. Replace with a progressive challenge: allow 3 attempts freely, then require CAPTCHA (Cloudflare Turnstile preferred \u2014 no user friction; reCAPTCHA v3 as fallback). Backend verifies the token server-side before proceeding with auth. Rate limiter can stay as a last-resort backstop but with a much higher threshold (e.g. 100 req/15 min).", + "description": "The current authLimiter blocks all login attempts from an IP for 15 minutes after N failures. This creates terrible UX (legitimate users get locked out, especially during testing) and is bypassable via rotating IPs anyway. Replace with a progressive challenge: allow 3 attempts freely, then require CAPTCHA (Cloudflare Turnstile preferred — no user friction; reCAPTCHA v3 as fallback). Backend verifies the token server-side before proceeding with auth. Rate limiter can stay as a last-resort backstop but with a much higher threshold (e.g. 100 req/15 min).", "details": "", "testStrategy": "", - "status": "pending", + "status": "in-progress", "dependencies": [], "priority": "medium", - "subtasks": [] + "subtasks": [], + "updatedAt": "2026-05-29T11:23:30.368Z" }, { "id": "13", - "title": "AMN Pay Scanner \u2014 retire Request Network API (Go microservice)", + "title": "AMN Pay Scanner — retire Request Network API (Go microservice)", "description": "Build a standalone Go microservice (AMN Pay Scanner) that replaces the RN API: generates paymentReferences locally, scans ERC20FeeProxy eth_getLogs per chain, and delivers HMAC-signed webhooks to the backend on confirmation. Backend swaps provider from 'request.network' to 'amn.scanner' via a new adapter. Supports any destination address, enabling HD-derived addresses as real payment destinations.", - "details": "See PRD - Retire Request Network \u2014 In-House Payment Scanner.md. Service exposes: POST /intents, GET /intents/:id, GET /scanner/status, GET /health. Node.js backend adds amnPayAdapter.ts and POST /api/payment/amn-scanner/webhook receiver. Parallel-run with RN during drain period. Language: Go v1 (Rust rewrite if volume justifies).\n\nImplemented by Kimi 2026-05-29. Scanner repo: scanner@8fee27e. Backend: backend@cdc8df1. Frontend: frontend@a5dd48e. Still open: live e2e probe (manual ops step \u2014 deploy scanner + send real BSC TransferWithReferenceAndFee tx to verify event topic match + webhook delivery).", + "details": "See PRD - Retire Request Network — In-House Payment Scanner.md. Service exposes: POST /intents, GET /intents/:id, GET /scanner/status, GET /health. Node.js backend adds amnPayAdapter.ts and POST /api/payment/amn-scanner/webhook receiver. Parallel-run with RN during drain period. Language: Go v1 (Rust rewrite if volume justifies).\n\nImplemented by Kimi 2026-05-29. Scanner repo: scanner@8fee27e. Backend: backend@cdc8df1. Frontend: frontend@a5dd48e. Still open: live e2e probe (manual ops step — deploy scanner + send real BSC TransferWithReferenceAndFee tx to verify event topic match + webhook delivery).", "testStrategy": "1. POST /intents returns checkoutBlock within 300ms with no RN API call. 2. Scanner detects TransferWithReferenceAndFee on BSC within 2 poll cycles. 3. Payment marked confirmed after threshold blocks. 4. Scanner resumes from checkpoint after restart. 5. Webhook rejected on bad HMAC.", "priority": "high", "status": "done", @@ -753,21 +757,22 @@ }, { "id": "14", - "title": "Sweep service \u2014 PermitPull + GasTopUp (Kimi, backend@7688f57)", - "description": "Standalone sweep service with three signer modes: PermitPullSweepSigner (EIP-712 gasless permit for ETH/Arb/Polygon/Base), GasTopUpSweepSigner (BNB top-up for BSC), BuildOnlySweepSigner (fallback). Auto-selects by chainId and token. Currently uses SWEEP_MASTER_PRIVKEY hot key \u2014 Task #11 (Trezor) replaces this.", + "title": "Sweep service — PermitPull + GasTopUp (Kimi, backend@7688f57)", + "description": "Standalone sweep service with three signer modes: PermitPullSweepSigner (EIP-712 gasless permit for ETH/Arb/Polygon/Base), GasTopUpSweepSigner (BNB top-up for BSC), BuildOnlySweepSigner (fallback). Auto-selects by chainId and token. Currently uses SWEEP_MASTER_PRIVKEY hot key — Task #11 (Trezor) replaces this.", "details": "Implemented by Kimi in backend@7688f57 (integrate-main-into-development). Files: src/services/payment/wallets/sweepService.ts, __tests__/sweep-service.test.ts. PERMIT_CAPABLE_TOKENS seeded from 2026-05-29 on-chain audit. 31/31 unit tests pass. Still open: on-chain integration tests (one per signer mode against testnet or Anvil fork). Env vars added: SWEEP_MASTER_PRIVKEY, SWEEP_GAS_MIN_BNB, SWEEP_GAS_TOP_UP_BNB.", "testStrategy": "Unit: 31/31 pass (auto-selection, permit capability matrix, gas top-up logic). Integration (open): one live broadcast per signer mode on BSC testnet or local Anvil fork.", "priority": "high", - "status": "in-progress", + "status": "done", "dependencies": [], - "subtasks": [] + "subtasks": [], + "updatedAt": "2026-05-29T11:56:24.674Z" } ], "metadata": { "version": "1.0.0", - "lastModified": "2026-05-29T08:21:05.470Z", - "taskCount": 12, - "completedCount": 6, + "lastModified": "2026-05-29T11:56:24.675Z", + "taskCount": 14, + "completedCount": 11, "tags": [ "master" ] diff --git a/00 - Overview/Roles & Personas.md b/00 - Overview/Roles & Personas.md index 54c60b5..9e907a1 100644 --- a/00 - Overview/Roles & Personas.md +++ b/00 - Overview/Roles & Personas.md @@ -7,9 +7,9 @@ created: 2026-05-23 # Roles & Personas > [!info] Where roles live in code -> The hard role enum is defined in `backend/src/models/User.ts:94` as `"admin" | "buyer" | "seller"`. Support is implemented as an admin variant (a dedicated `support@amn.gg` user is created at bootstrap — see `backend/TODO.md`) rather than as its own enum value. Permission checks live in route middleware and in service guards. +> The hard role enum is defined in `backend/src/models/User.ts:94` as `"admin" | "buyer" | "seller" | "resolver"`. The `resolver` role was added to the backend in commit `fce8a19` and is now a first-class enum value in `User.ts`, `UserRole` enum in `shared/types/index.ts`, and the dispute routes. Support is implemented as an admin variant (a dedicated `support@amn.gg` user is created at bootstrap — see `backend/TODO.md`) rather than as its own enum value. Permission checks live in route middleware and in service guards. -Amn has four user personas. Three are first-class roles in the data model; the fourth (Support) is a special-cased admin with reduced privileges. +Amn has five user personas. Four are first-class roles in the data model; the fifth (Support) is a special-cased admin with reduced privileges. ```mermaid flowchart LR @@ -18,11 +18,13 @@ flowchart LR Seller["Seller
(Owner)"] Support["Support
(admin variant)"] Admin["Admin"] + Resolver["Resolver
(dispute specialist)"] Visitor -->|signs up| Buyer Buyer -->|requests seller mode
+ admin approval| Seller Buyer & Seller -->|opens ticket| Support Support -->|escalates| Admin + Admin -->|assigns role| Resolver ``` --- @@ -82,7 +84,7 @@ The buyer dashboard lives under `/dashboard` (`frontend/src/app/dashboard/`). No - **Configure shop**: shop name, banner, description, response time SLA, accepted payment methods, payout wallet address. See `backend/src/models/ShopSettings.ts` and `frontend/src/sections/shop-settings/`. - **Discover requests** through the seller feed (filtered by category and preferred-seller status). Receive live notifications when a relevant request is posted via the `sellers` / `seller-` Socket.IO rooms (`backend/src/app.ts:101-112`). -- **Submit offers** with price, currency (USDT default, USDC, USD, EUR, IRR supported), delivery time, optional attachments and notes. +- **Submit offers** with price in **USDT** (the only supported currency for the escrow MVP — USD/EUR/IRR removed in commit 3aaa2fe), delivery time, optional attachments and notes. - **Negotiate** in the per-request chat — bilateral with the buyer until an offer is accepted. - **Fulfil** the order: ship physical goods (with optional tracking number), or upload/email digital deliverables. - **Use the [[delivery code]]** for physical handoffs: a six-digit one-time code the buyer reads to the courier to confirm receipt. @@ -110,6 +112,7 @@ Seller dashboard reuses the same `/dashboard` shell with extra modules: - `/dashboard/request-template` — create / edit shop-scoped templates - `/dashboard/payment` — receivables, payout history, pending releases - `/dashboard/disputes` — disputes where the seller is the respondent +- `/dashboard/seller/marketplace/offers` — **Offer Management** (tabbed view of all own offers filtered by status: pending / accepted / rejected / withdrawn; inline withdraw action; commit 9cf1686) > [!tip] See also > [[Seller Guide]] walks through onboarding, first listing, and payout setup end-to-end. [[Payments Overview]] explains the escrow + payout state machine. @@ -193,6 +196,30 @@ Support sees a stripped-down admin view focused on the inbox: --- +## Resolver + +> [!example] Who they are +> A platform-employed dispute resolver (`role: "resolver"`). Added to the backend as a first-class role in commit `fce8a19`. Resolvers have targeted authority to mediate and formally resolve disputes — they can assign disputes, update status, issue final resolutions (including `ban_seller` or `refund`), view statistics, and bypass chat membership checks (commit `766a9a2`) to read/send in any chat. + +### Primary workflows + +- **Review dispute details**: read buyer and seller evidence, chat history, delivery confirmations. +- **Communicate** directly through any chat — bypasses participant membership guard. +- **Assign, update status, and resolve disputes** with the same actions as admins (`refund | replacement | compensation | warning_seller | ban_seller | no_action`). +- **Monitor dispute health** via `GET /api/disputes/statistics`. + +### Key permissions + +- Full triage on disputes: `POST /:id/assign`, `PATCH /:id/status`, `POST /:id/resolve`, `GET /statistics`. +- Read and write messages in any chat (bypass membership check in `ChatService`). +- Read any dispute and its evidence. +- **Cannot**: change roles, issue payouts, suspend users, delete content, access non-dispute admin endpoints. + +> [!note] Implementation +> The `resolver` role was added as a first-class backend enum in commit `fce8a19` (`User.ts`, `UserRole` in `shared/types/index.ts`, dispute routes). Chat bypass was added in commit `766a9a2`. + +--- + ## Cross-cutting concerns ### Role transitions @@ -202,6 +229,7 @@ Support sees a stripped-down admin view focused on the inbox: | Anonymous | Buyer | Self-service signup | `User` created | | Buyer | Seller | Application → admin approval | `User.role` change | | Buyer / Seller | Admin | Manual DB / boot-time seed | High-risk, manual | +| Buyer / Seller | Resolver | Admin role assignment | `User.role` change | | Admin | Support | Permission profile applied at middleware | Role stays `admin` | ### Permission model diff --git a/01 - Architecture/Frontend Architecture.md b/01 - Architecture/Frontend Architecture.md index 2d73919..874e92e 100644 --- a/01 - Architecture/Frontend Architecture.md +++ b/01 - Architecture/Frontend Architecture.md @@ -3,7 +3,7 @@ title: Frontend Architecture tags: [architecture, frontend, nextjs] created: 2026-05-23 --- - + # Frontend Architecture Module-level architecture of the Next.js 16 (App Router) + TypeScript + MUI v7 frontend at `/Users/mojtabaheidari/code/frontend` (development branch). diff --git a/01 - Architecture/Scanner Architecture.md b/01 - Architecture/Scanner Architecture.md new file mode 100644 index 0000000..25996d0 --- /dev/null +++ b/01 - Architecture/Scanner Architecture.md @@ -0,0 +1,199 @@ +--- +title: Scanner Architecture +tags: [architecture, scanner, payment] +created: 2026-05-30 +--- + +# Scanner Architecture + +AMN Pay Scanner is a standalone Go microservice that watches on-chain payment events and notifies the backend via webhook when a payment is confirmed. It replaces the Request Network integration with an in-house polling scanner that supports EVM chains, Tron, and TON. + +> [!info] +> Repo: `scanner/` within the escrow monorepo. Binary: `scanner`. Written in Go 1.25. SQLite (WAL mode) for state. No external dependencies beyond the chain APIs. + +--- + +## 1. Responsibilities + +- Accept payment **intents** from the backend (POST /intents) +- Watch the relevant chain for matching on-chain transfers +- Track confirmation depth (EVM) or rely on finality from the chain API (Tron, TON) +- Deliver a signed webhook to the backend callback URL when confirmed +- Retry failed webhook deliveries +- Expire stale pending intents on a configurable TTL + +--- + +## 2. Component map + +``` +┌─────────────────────────────────────────────────────────┐ +│ scanner binary │ +│ │ +│ main.go │ +│ ├── loadConfig() config.go │ +│ ├── initDB() intent.go (SQLite schema) │ +│ ├── startup reconcile intent.go │ +│ ├── newServer() api.go │ +│ │ └── startWorkers() api.go │ +│ │ ├── ChainWorker chain.go (EVM) │ +│ │ ├── TronChainWorker tron_chain.go (Tron) │ +│ │ └── TonChainWorker ton_chain.go (TON) │ +│ ├── HTTP routes api.go / main.go │ +│ ├── intent TTL expiry main.go + intent.go │ +│ └── webhook retry loop main.go + webhook.go │ +│ │ +│ reference.go — payment reference / topic hash math │ +│ webhook.go — delivery, HMAC signing, retry │ +└─────────────────────────────────────────────────────────┘ +``` + +--- + +## 3. Chain worker model + +All three chain types implement the `Worker` interface: + +```go +type Worker interface { + start() + stop() + getHead(ctx context.Context) (int64, error) +} +``` + +One worker goroutine is spawned per chain marked `"verified": true` in `supported-chains.json`. Workers are selected by `chainType`: + +| chainType | Worker struct | API used | +|---|---|---| +| `evm` (default) | `ChainWorker` | JSON-RPC 2.0 (`eth_getLogs`, `eth_blockNumber`) | +| `tron` | `TronChainWorker` | TronGrid REST (`/v1/contracts/{contract}/events`) | +| `ton` | `TonChainWorker` | TonCenter v3 REST (`/jetton/transfers`) | + +Workers poll on `POLL_INTERVAL_SEC` (default 15 s). On first run, each worker starts scanning from the current chain head minus a small buffer (10 blocks for EVM, 24 h for Tron/TON). + +--- + +## 4. EVM scanning detail + +``` +for each tick: + head = eth_blockNumber + from = max(checkpoint − ReorgBuffer(), 0) + chunks = split [from..head] into 2000-block ranges + for each chunk: + logs = eth_getLogs(proxyAddress, EventTopic, from, to) + for each log: + topicRef = Topics[1] (keccak256 of paymentReference — pre-indexed) + intent = DB lookup by topicRef WHERE status='pending' + validate(log.Data, intent) ← token + destination + amount check + confirmIntentPending() ← status → 'confirming' + saveCheckpoint(to) + checkConfirmations(): + for each confirming intent: + confs = head - blockNumber + 1 + if confs >= required: finalizeIntent() + deliverWebhook() +``` + +**Reorg protection**: `ReorgBuffer()` re-scans `3 × confirmationThreshold` blocks before the checkpoint (clamped 20–500). This catches any log that appeared in a block that was later reorganised off the canonical chain. + +**Event signature**: `TransferWithReferenceAndFee` keccak256 = `0xbc8b749850bdc0c5e4a49d50f87ec40dca2acdb70a84e7c6823ccdc7b3b3d4b3` + +--- + +## 5. Tron scanning detail + +TronGrid does not expose a fee-proxy contract. Each intent is assigned a unique HD-derived destination address. The scanner watches TRC20 `Transfer` events on the USDT contract and matches by `to` address. + +- Checkpoint: block timestamp in milliseconds (`last_scanned_block` column) +- TronGrid addresses arrive as `41xxxx` hex (21 bytes); normalized to `0x` (20 bytes EVM style) +- Tron transactions reported by TronGrid are already confirmed; status goes directly to `confirmed` (no multi-block wait) +- Pagination follows `meta.links.next` until empty + +--- + +## 6. TON scanning detail + +TON uses TonCenter v3. Per-intent polling: for each pending TON intent, a separate HTTP call fetches incoming Jetton transfers to that destination since the checkpoint. + +- Checkpoint: Unix timestamp in seconds +- TON addresses are base64url (`EQ…`/`UQ…`) — case-sensitive, never lowercased +- `proxyAddress` = USDT Jetton master address (`EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs`) +- TonCenter returns only finalized transactions; status goes directly to `confirmed` +- Lag is reported in seconds, not blocks +- Known scaling limitation: O(pending intents) API calls per scan cycle + +--- + +## 7. Intent lifecycle + +``` +pending ──(tx seen)──► confirming ──(enough blocks)──► confirmed ──(webhook ok)──► [done] + │ │ │ + │ │ (deep reorg / TTL) │ (all retries fail) + └───────────────────────┴──────────► expired webhook_failed +``` + +- **Tron / TON** skip `confirming` and jump directly to `confirmed`. +- `webhook_failed` intents are retried every `WEBHOOK_RETRY_HOURS` (default 6 h) and on `POST /admin/webhooks/retry`. +- **Startup reconciliation**: on startup, `confirmed` intents with `webhook_delivered_at IS NULL` and created in the last 7 days have their webhook re-delivered. This recovers from a crash between `finalizeIntent` and `deliverWebhook`. + +--- + +## 8. Payment reference math (EVM) + +``` +paymentReference = last8Bytes(keccak256(lower(intentId + salt + destination))) +topicRef (index) = keccak256(paymentReferenceBytes) +``` + +The ERC20FeeProxy indexes `paymentReference` so `Topics[1]` in the log is `topicRef`, not the raw reference. The DB stores `topic_ref` pre-computed per intent so the scan loop is a single indexed SQL lookup instead of O(n) hashing. + +--- + +## 9. Database schema (SQLite WAL) + +Two tables: + +**`intents`** — one row per payment intent + +| Column | Type | Notes | +|---|---|---| +| `intent_id` | TEXT PK | caller-supplied UUID | +| `chain_id` | INTEGER | numeric chain ID | +| `chain_type` | TEXT | `evm` / `tron` / `ton` | +| `token_address` | TEXT | EVM/Tron: lowercase 0x hex; TON: base64url | +| `destination` | TEXT | receiving address | +| `amount` | TEXT | base-10 wei / token smallest unit | +| `payment_reference` | TEXT | 8-byte hex (EVM only) | +| `topic_ref` | TEXT | keccak256 of paymentReference (EVM index) | +| `status` | TEXT | `pending` / `confirming` / `confirmed` / `expired` / `webhook_failed` | +| `callback_url` | TEXT | backend webhook endpoint | +| `callback_secret` | TEXT | HMAC key (not returned in GET) | +| `confirmations_required` | INTEGER | from chain config or caller override | +| `tx_hash` | TEXT NULL | transaction hash once seen | +| `log_index` | INTEGER NULL | log position within tx (EVM) | +| `block_number` | INTEGER NULL | block / timestamp when seen | +| `confirmations` | INTEGER | current confirmation depth | +| `salt` | TEXT | 32-byte random hex for reference derivation | +| `webhook_delivered_at` | TEXT NULL | RFC3339 timestamp of successful delivery | +| `created_at` / `updated_at` | DATETIME | | + +Unique index on `(tx_hash, log_index)` prevents duplicate intent confirmation. + +**`checkpoints`** — one row per chain, tracks scan progress + +| Column | Notes | +|---|---| +| `chain_id` | PK | +| `last_scanned_block` | block number (EVM), ms timestamp (Tron), unix seconds (TON) | + +--- + +## 10. Security model + +- All non-health endpoints require `Authorization: Bearer ` (constant-time compare). +- If `SCANNER_API_KEY` is unset the server logs a warning and allows all requests — intended for local dev only. +- Webhooks are signed with HMAC-SHA256: `X-AMN-Signature: hex(hmac(body, callbackSecret))`. +- The `callbackSecret` is stored in the DB but excluded from all JSON responses (`json:"-"` tag). +- Request bodies are limited to 64 KB. diff --git a/02 - Data Models/ConfigSettingHistory.md b/02 - Data Models/ConfigSettingHistory.md new file mode 100644 index 0000000..69e07b6 --- /dev/null +++ b/02 - Data Models/ConfigSettingHistory.md @@ -0,0 +1,49 @@ +--- +title: ConfigSettingHistory +tags: [data-model, mongoose, admin, audit] +aliases: [Setting History, Threshold History, IConfigSettingHistory] +created: 2026-05-30 +--- + +# ConfigSettingHistory + +> **Added:** 2026-05-30 — introduced in commit `27fb15a` as part of Task #9 (per-chain confirmation thresholds + audit log). + +Audit trail document that records every change to a runtime configuration setting. Currently used exclusively to log confirmation-threshold updates (`key` pattern: `confirmation_threshold:`), but the schema is generic and can store other numeric runtime config changes. + +> [!note] Source +> `backend/src/models/ConfigSettingHistory.ts` — schema and model export. +> Written by `backend/src/services/payment/safety/confirmationThresholdService.ts` (`setConfirmationThreshold`). +> Read by `GET /api/admin/settings/confirmation-thresholds/history` in `confirmationThresholdRoutes.ts`. + +## Schema + +| Field | Type | Required | Default | Description | +| --- | --- | --- | --- | --- | +| `key` | String | yes | — | Setting identifier. Format: `confirmation_threshold:` for threshold changes. Indexed. | +| `oldValue` | Number | no | `null` | Value before the change. `null` when the setting had no prior database entry. | +| `newValue` | Number | yes | — | Value after the change. | +| `changedBy` | ObjectId (ref: `User`) | no | — | Admin user who made the change. Populated by `GET …/history` via `.populate('changedBy', 'email name')`. | +| `changedAt` | Date | no | `Date.now()` | Timestamp of the change. Indexed; used for sort-descending pagination. | + +> [!note] No `timestamps: false` +> The schema deliberately disables Mongoose's automatic `createdAt`/`updatedAt` fields (`timestamps: false`) because `changedAt` is the canonical timestamp. + +## Example document + +```json +{ + "_id": "6657c3...", + "key": "confirmation_threshold:56", + "oldValue": 12, + "newValue": 6, + "changedBy": { "_id": "...", "email": "admin@amn.gg" }, + "changedAt": "2026-05-30T10:22:00.000Z" +} +``` + +## Related + +- [[Payment API]] — `GET /api/admin/settings/confirmation-thresholds/history` +- [[Admin API]] — confirmation thresholds section +- `backend/src/services/payment/safety/confirmationThresholdService.ts` diff --git a/02 - Data Models/Data Model Overview.md b/02 - Data Models/Data Model Overview.md index d1fc1fd..57accd2 100644 --- a/02 - Data Models/Data Model Overview.md +++ b/02 - Data Models/Data Model Overview.md @@ -38,6 +38,7 @@ This section documents every Mongoose model that backs the marketplace. The pers - [[DerivedDestination]] — Per-payment derived wallet destination records used to reduce address reuse and reconcile on-chain pay-ins. - [[FundsLedgerEntry]] — Immutable accounting ledger rows for pay-in, hold, release, refund, fee, adjustment, and reversal events. - [[TrezorAccount]] — Hardware-wallet/safekeeping account metadata for custody operations and staged signer hardening. +- [[ConfigSettingHistory]] — Immutable audit trail of numeric runtime-config changes. Currently used for per-chain confirmation threshold change events, keyed as `confirmation_threshold:`. Added in commit `27fb15a`. ## Relationship Diagram diff --git a/02 - Data Models/PurchaseRequest.md b/02 - Data Models/PurchaseRequest.md index ebc5dab..c24e738 100644 --- a/02 - Data Models/PurchaseRequest.md +++ b/02 - Data Models/PurchaseRequest.md @@ -6,7 +6,7 @@ aliases: [Purchase Request, Buy Request, IPurchaseRequest] # PurchaseRequest -> **Last updated:** 2026-05-29 — aligned with code (see [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)) +> **Last updated:** 2026-05-30 — `budget.currency` locked to USDT; `categoryId` added to `IRequestTableItem` The central buyer-side document. A `PurchaseRequest` captures what a buyer wants to acquire (physical product, digital product, service, or consultation), the budget envelope, urgency, delivery details, and the entire lifecycle from creation through payment, delivery, and completion. Sellers respond by attaching [[SellerOffer]] documents; the buyer accepts one, a [[Payment]] is opened, and delivery is verified by a 6-digit code. @@ -31,7 +31,7 @@ The central buyer-side document. A `PurchaseRequest` captures what a buyer wants | `quantity` | Number | no | `1` | min 1 | — | Unit count. | | `budget.min` | Number | no | — | min 0 | — | Lower bound. | | `budget.max` | Number | no | — | min 0 | — | Upper bound. | -| `budget.currency` | String | no | `USD` | enum: `USD` / `EUR` / `IRR` | — | Budget currency. | +| `budget.currency` | String | no | `USDT` | enum: `USDT` (escrow MVP — USD/EUR/IRR removed in commit 3aaa2fe) | — | Budget currency. | | `urgency` | String | no | `medium` | enum: `low` / `medium` / `high` / `urgent` | yes | Buyer urgency. | | `status` | String | no | `pending` | enum (13 values — see State Transitions below) | yes | Lifecycle state. | | `isPublic` | Boolean | no | `true` | — | — | Public marketplace listing vs. private request. | diff --git a/02 - Data Models/ScannerIntent.md b/02 - Data Models/ScannerIntent.md new file mode 100644 index 0000000..dff7804 --- /dev/null +++ b/02 - Data Models/ScannerIntent.md @@ -0,0 +1,114 @@ +--- +title: ScannerIntent (Scanner DB model) +tags: [data-model, scanner, payment] +created: 2026-05-30 +--- + +# ScannerIntent + +SQLite row in the AMN Pay Scanner's `intents` table. One row per payment intent registered by the backend. This is internal scanner state — it is not a Mongoose model and lives in a separate SQLite database (`/data/scanner.db`). + +--- + +## Schema + +```sql +CREATE TABLE intents ( + intent_id TEXT PRIMARY KEY, + chain_id INTEGER NOT NULL, + chain_type TEXT NOT NULL DEFAULT 'evm', + token_address TEXT NOT NULL, + destination TEXT NOT NULL, + amount TEXT NOT NULL, + payment_reference TEXT NOT NULL, + topic_ref TEXT, + status TEXT NOT NULL DEFAULT 'pending', + callback_url TEXT NOT NULL, + callback_secret TEXT NOT NULL, + confirmations_required INTEGER NOT NULL DEFAULT 12, + tx_hash TEXT, + log_index INTEGER, + block_number INTEGER, + confirmations INTEGER NOT NULL DEFAULT 0, + salt TEXT NOT NULL, + webhook_delivered_at TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); +``` + +--- + +## Fields + +| Field | Type | Description | +|---|---|---| +| `intent_id` | TEXT PK | Caller-supplied unique ID (typically the backend Payment `_id`) | +| `chain_id` | INTEGER | Numeric chain ID. EVM standard (56, 137, 1, 42161, 8453), Tron (728126428), TON (1100) | +| `chain_type` | TEXT | `evm` / `tron` / `ton`. Determines which worker handles this intent | +| `token_address` | TEXT | ERC20 / TRC20 contract address. EVM/Tron: lowercase `0x` hex. TON: exact base64url | +| `destination` | TEXT | Recipient wallet address. EVM/Tron: lowercase `0x` hex. TON: base64url (case-sensitive) | +| `amount` | TEXT | Required amount in smallest token unit (wei / 10^decimals), stored as base-10 integer string | +| `payment_reference` | TEXT | 8-byte hex EVM payment reference (`0x` + 16 hex chars). Derived as `last8(keccak256(intentId + salt + destination))` | +| `topic_ref` | TEXT | `keccak256(paymentReferenceBytes)` — matches `Topics[1]` in EVM logs. Pre-computed for indexed DB lookup. NULL for Tron/TON | +| `status` | TEXT | Intent lifecycle state (see below) | +| `callback_url` | TEXT | URL the scanner POSTs to on confirmation | +| `callback_secret` | TEXT | HMAC-SHA256 key for webhook signature. Never returned in API responses | +| `confirmations_required` | INTEGER | Number of blocks required before confirmation (EVM). Defaults to chain config | +| `tx_hash` | TEXT NULL | Transaction hash once a matching transfer is detected | +| `log_index` | INTEGER NULL | Log position within the transaction (EVM only; 0 for Tron/TON) | +| `block_number` | INTEGER NULL | Block number (EVM/Tron) or Unix timestamp seconds (TON) when the tx was seen | +| `confirmations` | INTEGER | Current confirmation depth. Incremented each scan cycle for `confirming` intents | +| `salt` | TEXT | 32-byte random hex. Combined with `intent_id` and `destination` to derive `payment_reference`. Prevents reference collisions across retried payments | +| `webhook_delivered_at` | TEXT NULL | RFC3339 timestamp when the webhook was successfully delivered. Used for startup crash recovery | +| `created_at` / `updated_at` | DATETIME | UTC timestamps | + +--- + +## Status values + +| Status | Description | +|---|---| +| `pending` | Registered; scanner is watching for a matching on-chain transfer | +| `confirming` | EVM only — matching tx seen, waiting for `confirmations_required` blocks | +| `confirmed` | Payment confirmed; webhook delivery attempted | +| `expired` | TTL exceeded while still in `pending` or `confirming` | +| `webhook_failed` | All webhook delivery retries exhausted; manual retry or periodic auto-retry needed | + +--- + +## Indexes + +```sql +CREATE INDEX idx_intents_status ON intents(status); +CREATE INDEX idx_intents_chain_status ON intents(chain_id, status); +CREATE INDEX idx_intents_payment_ref ON intents(payment_reference); +CREATE INDEX idx_intents_topic_ref ON intents(topic_ref); +CREATE UNIQUE INDEX idx_intents_tx_log ON intents(tx_hash, log_index) + WHERE tx_hash IS NOT NULL; +``` + +`idx_intents_topic_ref` is the performance-critical index — the EVM scanner's inner loop does a single indexed lookup per log entry. + +The unique index on `(tx_hash, log_index)` prevents two intents being confirmed from the same on-chain event (double-spend protection). + +--- + +## Migrations + +Three additive migrations run at startup (idempotent): + +1. `ADD COLUMN topic_ref TEXT` — added after initial schema +2. `ADD COLUMN chain_type TEXT NOT NULL DEFAULT 'evm'` — added for Tron/TON support +3. `ADD COLUMN webhook_delivered_at TEXT` — added for crash recovery + +A backfill pass recomputes `topic_ref` for existing EVM intents that had it as NULL. + +--- + +## Related + +- [Scanner Architecture](../01%20-%20Architecture/Scanner%20Architecture.md) +- [Scanner API](../03%20-%20API%20Reference/Scanner%20API.md) +- [Payment Flow - Scanner](../04%20-%20Flows/Payment%20Flow%20-%20Scanner.md) +- [Payment](Payment.md) — the backend MongoDB model that triggers intent creation diff --git a/02 - Data Models/SellerOffer.md b/02 - Data Models/SellerOffer.md index b19ec32..9cd07bf 100644 --- a/02 - Data Models/SellerOffer.md +++ b/02 - Data Models/SellerOffer.md @@ -6,7 +6,7 @@ aliases: [Seller Offer, Bid, ISellerOffer] # SellerOffer -> **Last updated:** 2026-05-29 — aligned with code (see [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)) +> **Last updated:** 2026-05-30 — added AML fields (`requireAmlCheck`, `amlBlockOnFailure`) A seller's bid against a [[PurchaseRequest]]. Stores the proposed price, the delivery time commitment, optional notes/attachments, and a small status machine (`pending` / `accepted` / `rejected` / `withdrawn`). The parent `PurchaseRequest` keeps the array of offer ids in `offers[]` and the chosen one in `selectedOfferId`. @@ -30,6 +30,8 @@ A seller's bid against a [[PurchaseRequest]]. Stores the proposed price, the del | `attachments[]` | String[] | no | — | — | — | URLs of supporting files. | | `notes` | String | no | — | trim | — | Internal/private notes. | | `validUntil` | Date | no | — | — | — | Expiration. | +| `requireAmlCheck` | Boolean | no | — | — | — | If true, AML screening must pass before the offer is presented to the buyer. | +| `amlBlockOnFailure` | Boolean | no | — | — | — | If true and AML screening fails, the offer is blocked. Otherwise it is flagged for manual review. | | `createdAt` | Date | auto | — | — | yes (desc) | Mongoose timestamp. | | `updatedAt` | Date | auto | — | — | — | Mongoose timestamp. | @@ -66,9 +68,13 @@ None defined. `createOffer` in `SellerOfferService` permits offers against a `PurchaseRequest` whose status is **`pending`**, **`received_offers`**, or **`active`**. Attempts against any other status are rejected. -### `withdrawOffer()` — dead code +### `withdrawOffer()` — frontend action available -`SellerOfferService.withdrawOffer()` exists in the source but is **not exposed via any HTTP route**. It cannot be called through the API. Any frontend references to a withdraw endpoint will receive a `404`. +`SellerOfferService.withdrawOffer()` is not a dedicated HTTP route. The correct API path is `PUT /api/marketplace/offers/:id/status` with `{ status: 'withdrawn' }`. + +The frontend exposes this via the `withdrawOffer(offerId)` action in `src/actions/marketplace.ts` (added commit 240a668). It is called from: +- `step-2-waiting-for-payment.tsx` (edit/cancel controls while `requestDetails.status === 'received_offers'`) +- `frontend/src/app/dashboard/seller/marketplace/offers/page.tsx` (Offer Management page, bulk view) ## Relationships diff --git a/02 - Data Models/User.md b/02 - Data Models/User.md index 4e80c0c..ef1b0fa 100644 --- a/02 - Data Models/User.md +++ b/02 - Data Models/User.md @@ -28,7 +28,7 @@ The core identity document for every actor in the marketplace: buyers, sellers, | `password` | String | no | — | minlength 6 | — | Hashed password. Optional to support passkey-only, Google, and Telegram accounts. | | `firstName` | String | no | `"کاربر"` | trim | — | Persian default ("user"). | | `lastName` | String | no | `"جدید"` | trim | — | Persian default ("new"). | -| `role` | String | yes | `"buyer"` | enum: `admin` / `buyer` / `seller` | yes | Authorisation tier. | +| `role` | String | yes | `"buyer"` | enum: `admin` / `buyer` / `seller` / `resolver` | yes | Authorisation tier. `resolver` was added in commit `fce8a19` — can view and resolve disputes, and bypass chat membership checks, but has no other admin privileges. | | `isEmailVerified` | Boolean | no | `false` | — | — | Set to true after the email verification code is consumed. ⚠️ Changing the email via `PUT /api/user/profile` **resets this to `false`** and dispatches a fresh **6-digit** verification code to the new address (see Email verification note below). | | `authProvider` | String | yes | `"email"` | enum: `email` / `google` / `telegram` | yes | Provider used to create the account. Existing email/password accounts remain `email`; Telegram-only users are `telegram`. | | `telegramVerified` | Boolean | no | `false` | — | — | Set when Telegram identity has been signature-verified and linked through `TelegramLink`. | diff --git a/03 - API Reference/API Overview.md b/03 - API Reference/API Overview.md index de153f2..355547e 100644 --- a/03 - API Reference/API Overview.md +++ b/03 - API Reference/API Overview.md @@ -34,7 +34,9 @@ This page is the entry point for the API. See the individual service pages for e The base port is set via `PORT` env var; in `development` it defaults to `5001`. CORS is restricted to `process.env.FRONTEND_URL` and credentials are allowed (`cors({ origin, credentials: true })` in `app.ts`). -Health check (not under `/api`): `GET /health` → `{ success, message, timestamp, environment, version }`. +Health checks: +- `GET /health` (not under `/api`) → `{ success, message, timestamp, environment, version }` — used by Docker and Gatus. +- `GET /api/health` (added in commit `44579d6`, backend v2.6.49) → deeper JSON with database and Redis connectivity status, plus the version string. Used by Gatus monitoring. API discovery endpoint: `GET /api` → returns a map of available service prefixes. diff --git a/03 - API Reference/Admin API.md b/03 - API Reference/Admin API.md index 906994f..f0a0823 100644 --- a/03 - API Reference/Admin API.md +++ b/03 - API Reference/Admin API.md @@ -5,13 +5,16 @@ tags: [api, admin, reference] # Admin API -> **Last updated:** 2026-05-29 — aligned with code (see Doc vs Code Audit Report) +> **Last updated:** 2026-05-30 — break-glass endpoints added, scanner/status auth fixed, reload/probe routes now implemented, confirmation threshold history implemented, resolver role added There is no single `/api/admin` namespace — admin-only endpoints are scattered across the service routers. This page catalogs them in one place. All require `Bearer JWT` with `req.user.role === 'admin'` unless explicitly noted otherwise. The two enforcement patterns are: - Middleware: `authorizeRoles('admin')` after `authenticateToken` (used by the dispute, data-cleanup, blog routers). - Inline check inside the handler: `if (req.user.role !== 'admin') return 403` (used by user, points, payment routes). +> [!note] Resolver role +> The `resolver` role was added (commit `fce8a19`). Resolvers have access to the dispute-triage endpoints (`assign`, `status`, `resolve`, `statistics`) only. All other admin endpoints remain `admin`-only. + ## User management See full descriptions in [[User API]]. @@ -159,9 +162,14 @@ Router: [`backend/src/services/admin/dataCleanupRoutes.ts`](../../backend/src/se ### GET /api/admin/scanner/status -**Description:** Returns the current state of the blockchain scanner / wallet monitor. +**Description:** Returns the current state of the AMN Pay Scanner. Proxies to `AMN_SCANNER_URL/scanner/status`. +**Auth required:** Bearer JWT (`admin`) — `authenticateToken` + `authorizeRoles('admin')` were added in commit `1d881c5`. The previously documented unauthenticated access gap (ISSUE-006) is closed. -> **⚠️ SECURITY BUG — NO AUTHENTICATION:** Despite being mounted under `/api/admin/`, this endpoint has **no** `authenticateToken` or `authorizeRoles` guard. Any unauthenticated request can read scanner state. +### POST /api/admin/scanner/webhooks/retry + +**Description:** Trigger a retry of failed/pending scanner webhooks. +**Auth required:** Bearer JWT (`admin`) +**Request body:** `{ intentId?: string }` — omit to retry all pending. ## Settings @@ -174,6 +182,13 @@ Router: [`backend/src/services/admin/dataCleanupRoutes.ts`](../../backend/src/se | `GET /api/admin/settings/aml` | admin | Read current AML settings | | `PATCH /api/admin/settings/aml` | admin | Update AML settings (runtime only — not persisted to disk or DB) | +**AML providers available:** + +- **Chainalysis** — cloud API provider (requires `CHAINALYSIS_API_KEY`). Enabled via `AML_PROVIDER=chainalysis`. +- **OFAC SDN local** — downloads the US Treasury SDN XML list once per 24 hours and checks addresses locally. No API key required. Enabled via `AML_PROVIDER=ofac`. Added in commit `31343d1` (Task #10). List is fetched from `OFAC_SDN_URL` (defaults to `https://www.treasury.gov/ofac/downloads/sdn.xml`). + +The active provider is selected at startup via `AML_PROVIDER`. `PATCH /api/admin/settings/aml` can switch the provider at runtime but the change is not persisted. + ### Confirmation thresholds Frontend page exists. Endpoints require admin auth. @@ -182,8 +197,22 @@ Frontend page exists. Endpoints require admin auth. | --- | --- | | `GET /api/admin/settings/confirmation-thresholds` | Get current confirmation thresholds for all chains | | `PATCH /api/admin/settings/confirmation-thresholds/:chainId` | Update threshold for a specific chain | +| `GET /api/admin/settings/confirmation-thresholds/history` | Last 50 threshold change events (populated with `changedBy` user email/name) | -> **Not implemented:** `GET /api/admin/settings/confirmation-thresholds/history` — history endpoint does not exist. `POST /api/admin/rn/networks/reload` and `POST /api/admin/rn/networks/probe/:chainId` do not exist. +> **History route:** `GET /api/admin/settings/confirmation-thresholds/history` is now implemented (commit `27fb15a`). It reads from the `ConfigSettingHistory` collection, keyed as `confirmation_threshold:`. + +### Break-glass (Trezor bypass) + +Three endpoints manage the break-glass mode, which disables the Trezor safekeeping requirement for escrow release/refund for up to 1 hour. All changes fire a Telegram alert. + +| Endpoint | Action | +| --- | --- | +| `GET /api/admin/settings/break-glass` | Read current break-glass status (active, expiresAt, activatedBy) | +| `POST /api/admin/settings/break-glass` | Activate break-glass for 1 hour | +| `DELETE /api/admin/settings/break-glass` | Cancel break-glass before it expires | + +> [!warning] In-memory state +> Break-glass state is stored in-memory only (`breakGlassRoutes.ts`). A server restart always clears it, which is intentional. The `isBreakGlassActive()` helper is exported and consumed by the Trezor safekeeping middleware. ## Payments awaiting confirmation @@ -200,6 +229,10 @@ Frontend page exists. | Endpoint | Auth | Action | | --- | --- | --- | | `GET /api/admin/rn/networks` | admin | List all registered RN networks | +| `POST /api/admin/rn/networks/reload` | admin | Reload chain + token registries from disk (no restart needed) | +| `POST /api/admin/rn/networks/probe/:chainId` | admin | On-demand on-chain probe: RPC reachability, proxy bytecode, dummy-call validity | + +> All three routes are implemented (commit `5681abf`). Previous docs listed reload and probe as not implemented. ## Blog admin diff --git a/03 - API Reference/Authentication API.md b/03 - API Reference/Authentication API.md index 230a3f8..4112bb1 100644 --- a/03 - API Reference/Authentication API.md +++ b/03 - API Reference/Authentication API.md @@ -5,7 +5,7 @@ tags: [api, auth, reference] # Authentication API -> **Last updated:** 2026-05-29 — aligned with code (see [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)) +> **Last updated:** 2026-05-30 — Cloudflare Turnstile CAPTCHA added after 3 failed logins (commit `b8edbbf`) All endpoints are mounted under `/api/auth/*` in `backend/src/app.ts`. The routes file is [`backend/src/services/auth/authRoutes.ts`](../../backend/src/services/auth/authRoutes.ts) and the WebAuthn sub-routes are in [`passkeyRoutes.ts`](../../backend/src/services/auth/passkeyRoutes.ts). Controller logic lives in [`authController.ts`](../../backend/src/services/auth/authController.ts) and [`authService.ts`](../../backend/src/services/auth/authService.ts). @@ -121,6 +121,12 @@ Two distinct identities are involved: a [[User]] (`models/User.ts`) and a [[Temp - `403` email not verified - `423` account locked (after repeated failures, tracked in Redis via `rateLimitService`) +**Cloudflare Turnstile CAPTCHA:** After **3 failed login attempts** from the same IP within 15 minutes the `captchaGate` middleware requires a valid `cf-turnstile-response` token in the request body. Responses when CAPTCHA is required but missing: +```json +{ "success": false, "captchaRequired": true, "message": "..." } +``` +HTTP status: `429`. When `TURNSTILE_SECRET_KEY` is not set (local dev) the gate is skipped. + **⚠️ Rate limiter behaviour:** The attempt counter increments on **every** attempt (before password validation), not only on failures. 5 total attempts within 15 minutes triggers lockout — a user burning 5 attempts with typos will be locked out even if they never had a valid password. **Side effects:** diff --git a/03 - API Reference/Chat API.md b/03 - API Reference/Chat API.md index 2650ff2..07d73fc 100644 --- a/03 - API Reference/Chat API.md +++ b/03 - API Reference/Chat API.md @@ -5,10 +5,13 @@ tags: [api, chat, reference] # Chat API -> **Last updated:** 2026-05-29 — aligned with code (see [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)) +> **Last updated:** 2026-05-30 — admin and resolver roles can now read and send messages in any chat (commit `766a9a2`) All chat endpoints live under `/api/chat/*`. The router is [`backend/src/services/chat/chatRoutes.ts`](../../backend/src/services/chat/chatRoutes.ts), controller is `chatController`, service is `ChatService`. Every endpoint requires `Bearer JWT` — the router applies `authenticateToken` globally. +> [!note] Admin and resolver chat access +> Users with role `admin` or `resolver` can **read messages and send messages in any chat** without being a listed participant (`ChatService` checks `canBypassMembership = senderRole === 'admin' || senderRole === 'resolver'`). This applies to `GET /api/chat/:id/messages`, `GET /api/chat/:id/info`, and `POST /api/chat/:id/messages`. Dispute-chat monitoring for resolvers was the primary driver (commit `766a9a2`). + Model: [[Chat]]. Real-time delivery happens over Socket.IO rooms named `chat-`. Clients must call `join-chat-room` after connecting. See [[Socket Events]] for `new-message`, `messages-read`, `message-edited`, `message-deleted`, `participants-added`, `participant-removed`, and `user-typing` payloads. ## Rate limits and constraints diff --git a/03 - API Reference/Dispute API.md b/03 - API Reference/Dispute API.md index af4f861..c1040ae 100644 --- a/03 - API Reference/Dispute API.md +++ b/03 - API Reference/Dispute API.md @@ -5,18 +5,21 @@ tags: [api, dispute, reference] # Dispute API -> **Last updated:** 2026-05-29 — aligned with code (see [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)) +> **Last updated:** 2026-05-30 — resolver role added, role guards applied to assign/status/resolve (commits b9e0f6a, 1d881c5) > [!note] Current implementation -> The Dispute module now has a Mongoose model, controller routes, dashboard routes, and release-hold helper routes mounted under `/api/disputes`. Keep this page aligned with both `backend/src/routes/disputeRoutes.ts` and `backend/src/services/dispute/disputeRoutes.ts`. +> The Dispute module has two distinct router families. Keep this page aligned with both `backend/src/routes/disputeRoutes.ts` and `backend/src/services/dispute/disputeRoutes.ts`. -Endpoints live under `/api/disputes/*`. `backend/src/routes/disputeRoutes.ts` delegates to `DisputeController` (`backend/src/controllers/disputeController.ts`) for CRUD/triage. `backend/src/services/dispute/disputeRoutes.ts` provides lightweight release-hold endpoints (`raise`, `resolve`, `status`) used by escrow release gating. The routers apply `authenticateToken` globally — every endpoint requires `Bearer JWT`. +Endpoints live under two prefixes: -> [!warning] Route shadowing — both dispute routers are mounted at `/api/disputes` -> The dashboard router is mounted **first** in `app.ts`. Its `POST /:id/resolve` intercepts requests before the admin-guarded release-hold router's resolve handler. Confirm which handler will run before wiring automation to either resolve endpoint. +- `/api/disputes/*` — `backend/src/routes/disputeRoutes.ts` delegates to `DisputeController` (`backend/src/controllers/disputeController.ts`) for CRUD/triage. All routes apply `authenticateToken` globally. +- `/api/disputes/pr/*` — `backend/src/services/dispute/disputeRoutes.ts` provides lightweight release-hold endpoints (`raise`, `resolve`, `status`) used by escrow release gating. Previously mounted at `/api/disputes`, causing route shadowing (ISSUE-003). **Remounted at `/api/disputes/pr` in commit `1d881c5`** — all release-hold calls must use this new prefix. -> [!danger] Security issues — see individual endpoint notes below -> Several endpoints that are documented as admin-only have **no role guard** in the current codebase. Any authenticated user can call them. These are noted per-endpoint. +> [!success] Route shadowing resolved (ISSUE-003) +> The release-hold router was remounted from `/api/disputes` to `/api/disputes/pr`. Both routers now have independent paths and neither shadows the other. + +> [!note] Resolver role +> A new `resolver` role was added (commit `fce8a19`). Resolvers can view and resolve disputes but have no other platform privileges. They are granted the same access as `admin` on all dispute-triage operations listed below. > [!note] Real-time events > All socket events from `DisputeService` are currently **TODO stubs**. No real-time events fire from dispute mutations. Notifications are delivered via `POST /api/notifications` → `new-notification` socket event only. @@ -48,16 +51,18 @@ Model: [[Dispute]]. A dispute references a [[PurchaseRequest]] plus optional [[P - Notifies the counter-party via `POST /api/notifications` (`new-notification` socket event). - Pauses any in-flight payout (sets a hold flag on the related [[Payment]]). -### POST /api/disputes/:purchaseRequestId/raise +### POST /api/disputes/pr/:purchaseRequestId/raise -**Description:** Lightweight release-hold endpoint that marks a purchase request and related payments as disputed. Exists in the backend but has no corresponding frontend action. +**Description:** Lightweight release-hold endpoint that marks a purchase request and related payments as disputed. No corresponding frontend UI action. **Auth required:** Bearer JWT (buyer who owns the request or admin) **Request body:** `{ reason?: string }` **Response 200:** `{ success, message, data }` -### GET /api/disputes/:purchaseRequestId/status +> **Path note:** Previously served at `/api/disputes/:purchaseRequestId/raise`. Moved to `/api/disputes/pr/:purchaseRequestId/raise` in commit `1d881c5` (ISSUE-003 fix). -**Description:** Returns release-hold flags for a purchase request, including whether release is currently blocked. Exists in the backend but has no corresponding frontend action. +### GET /api/disputes/pr/:purchaseRequestId/status + +**Description:** Returns release-hold flags for a purchase request, including whether release is currently blocked. No corresponding frontend UI action. **Auth required:** Bearer JWT (buyer, preferred seller, or admin) ## Read @@ -79,7 +84,7 @@ Model: [[Dispute]]. A dispute references a [[PurchaseRequest]] plus optional [[P ### GET /api/disputes/statistics **Description:** Aggregated counts (open, by reason, average resolution time) for admin dashboards. -**Auth required:** Bearer JWT (any authenticated user — backend applies `authenticateToken` only, no role restriction) +**Auth required:** Bearer JWT (`admin` or `resolver` — `authorizeRoles('admin', 'resolver')` is applied) **Response 200:** `{ success, data: { open, byReason, avgResolutionHours, ... } }` ### GET /api/disputes/:id @@ -92,10 +97,8 @@ Model: [[Dispute]]. A dispute references a [[PurchaseRequest]] plus optional [[P ### POST /api/disputes/:id/assign -**Description:** Assign an admin moderator to the dispute. Sets `assignedAdminId` and transitions status to `in_progress`. -**Auth required:** Bearer JWT - -> ⚠️ **SECURITY — NO ROLE GUARD:** Despite being documented as admin-only, there is no role guard on this endpoint. Any authenticated user can self-assign as mediator on any dispute. +**Description:** Assign an admin or resolver moderator to the dispute. Sets `assignedAdminId` and transitions status to `in_progress`. +**Auth required:** Bearer JWT (`admin` or `resolver`) **Request body:** `{ adminId: string }` **Side effects:** Notifies all participants. @@ -103,18 +106,14 @@ Model: [[Dispute]]. A dispute references a [[PurchaseRequest]] plus optional [[P ### PATCH /api/disputes/:id/status **Description:** Generic status update (e.g. close without resolution). -**Auth required:** Bearer JWT - -> ⚠️ **SECURITY — NO ROLE GUARD:** There is no role guard on this endpoint. Any authenticated user can change dispute status despite documentation claiming admin-only access. +**Auth required:** Bearer JWT (`admin` or `resolver`) **Request body:** `{ status: string; note?: string }` ### POST /api/disputes/:id/resolve **Description:** Final adjudication. Records the decision and triggers the appropriate escrow action. -**Auth required:** Bearer JWT - -> ⚠️ **SECURITY — NO ROLE GUARD:** This is the dashboard router's resolve handler (mounted first). There is no role guard. Any authenticated user can resolve a dispute, including issuing `action=ban_seller`. +**Auth required:** Bearer JWT (`admin` or `resolver`) > ⚠️ **ROUTE SHADOWING:** Because the dashboard router is mounted before the admin-guarded release-hold router, this handler intercepts all `POST /api/disputes/:id/resolve` requests. The admin-guarded release-hold resolve endpoint is unreachable at this path. @@ -131,13 +130,14 @@ Model: [[Dispute]]. A dispute references a [[PurchaseRequest]] plus optional [[P - `action === "refund"` → create/approve the corresponding refund instruction through the ledger-gated payment release/refund flow. - `action === "no_action"` or seller-favorable outcome → clear hold only after release checks pass. - Notifies both participants and updates [[PurchaseRequest]] status to `disputed_resolved`. +- **ISSUE-004 fix (commit `1d881c5`):** `DisputeService.resolveDispute` now calls `releaseHoldResolve()` on the linked `purchaseRequestId`, clearing the escrow hold so payment release is unblocked automatically after resolution. -### POST /api/disputes/:purchaseRequestId/resolve +### POST /api/disputes/pr/:purchaseRequestId/resolve **Description:** Lightweight release-hold endpoint that clears the disputed hold flags on a purchase request and related payments. **Auth required:** Bearer JWT (admin) -> ⚠️ **ROUTE SHADOWING:** This endpoint is on the release-hold router which is mounted **after** the dashboard router. The dashboard router's `POST /:id/resolve` matches first, making this handler unreachable in practice. See the route shadowing warning at the top of this page. +> **Path note:** Previously unreachable due to route shadowing. Moved to `/api/disputes/pr/:purchaseRequestId/resolve` (commit `1d881c5`, ISSUE-003 fix). This endpoint is now reachable. **Response 200:** `{ success, message, data }` diff --git a/03 - API Reference/Marketplace API.md b/03 - API Reference/Marketplace API.md index f5d2dd9..f30d255 100644 --- a/03 - API Reference/Marketplace API.md +++ b/03 - API Reference/Marketplace API.md @@ -71,8 +71,8 @@ The buyer-facing CRUD plus seller-side workflow endpoints. Model: [[PurchaseRequ size?: string; color?: string; quantity?: number; // default 1 - budget?: { min?: number; max?: number; currency: "USD" | "EUR" | "IRR" }; - urgency?: "low" | "medium" | "high"; + budget?: { min?: number; max?: number; currency: "USDT" | "USDC" }; // restricted to escrow-compatible stablecoins (commit d52feb7) + urgency?: "low" | "medium" | "high" | "urgent"; deliveryInfo?: { deliveryType: "physical" | "online"; addressId?: string; // when physical @@ -239,7 +239,7 @@ Valid `status` values: `pending | accepted | rejected | withdrawn` **Request body:** ```ts { - price: { amount: number; currency: "USD" | "EUR" | "IRR" }; + price: { amount: number; currency: "USDT" }; // USDT only for escrow MVP deliveryEstimate: { days: number; note?: string }; notes?: string; attachments?: string[]; @@ -248,6 +248,8 @@ Valid `status` values: `pending | accepted | rejected | withdrawn` **Response 201:** `{ success, data: { offer } }` **Side effects:** Emits `new-offer` to `buyer-` and `seller-offer-update` to `seller-`. +> **Note:** Currency is locked to `USDT` for the escrow MVP (commit 3aaa2fe). The frontend `CURRENCY_SYMBOLS` map in `src/sections/request/constants.ts` exposes only `USDT`. + ### PUT /api/marketplace/purchase-requests/:id/offers (legacy) **Description:** Older offer-update endpoint kept for compatibility. @@ -271,16 +273,19 @@ Valid `status` values: `pending | accepted | rejected | withdrawn` This endpoint does not exist. Use `GET /api/marketplace/purchase-requests/:id/offers` instead. -### ⚠️ NOT IMPLEMENTED: GET /api/marketplace/offers/seller/:sellerId +### GET /api/marketplace/offers/seller/:sellerId -This endpoint does not exist. `getOffersBySeller()` is an internal service method and is not exposed via HTTP. +**Description:** Returns all offers submitted by the given seller, across all purchase requests. Used by the Offer Management dashboard page (`/dashboard/seller/marketplace/offers`). +**Auth required:** Bearer JWT (seller, own `:sellerId` only) +**Response 200:** `{ data: [SellerOffer, ...] }` +**Frontend action:** `getSellerOffers(sellerId)` in `src/actions/marketplace.ts` (added commit 240a668) ### PATCH /api/marketplace/offers/:id **Description:** Seller edits their pending offer (price, delivery estimate, notes). **Auth required:** Bearer JWT (offer owner) -> ⚠️ **KNOWN BUG:** The frontend sends `PUT` but the backend registers `PATCH`. Requests from clients using `PUT` will receive `404`. Use `PATCH`. +> ✅ **Fixed (commit 240a668):** The frontend `updateOffer` and `acceptOffer` actions now correctly send `PATCH`. ### DELETE /api/marketplace/offers/:id @@ -293,9 +298,14 @@ This endpoint does not exist. `getOffersBySeller()` is an internal service metho **Auth required:** Bearer JWT **Request body:** `{ status: "pending" | "accepted" | "rejected" | "withdrawn" }` -### ⚠️ NOT IMPLEMENTED: POST /api/marketplace/offers/:id/withdraw +### POST /api/marketplace/offers/:id/withdraw -This endpoint does not exist. To withdraw an offer use `PUT /api/marketplace/offers/:id/status` with `{ status: 'withdrawn' }`. +**Description:** Seller withdraws their offer. Sets offer status to `withdrawn` using `sellerOfferService.withdrawOffer()`. Only the offer owner may call this. +**Auth required:** Bearer JWT (offer owner) +**Response 200:** `{ success: true, data: { /* updated offer */ } }` +**Errors:** `403` not the offer owner, `404` offer not found. + +> **Note:** This endpoint was previously documented as NOT IMPLEMENTED. It was added to `backend/src/services/marketplace/routes.ts` (commit `3e47713`). ### POST /api/marketplace/purchase-requests/:id/select-offer @@ -303,7 +313,8 @@ This endpoint does not exist. To withdraw an offer use `PUT /api/marketplace/off **Auth required:** Bearer JWT (buyer) **Request body:** `{ offerId: string }` **Side effects:** -- Updates [[PurchaseRequest]] `selectedOfferId`, status moves toward `payment`. +- Persists `selectedOfferId` on [[PurchaseRequest]] (commit `023255f` — previously this field was not saved, causing it to be lost). Status moves toward `payment`. +- Rejects all **losing** offers (sets their status to `rejected`) when payment is confirmed (commit `023255f`). - Emits `seller-offer-update` to all sellers for the request. ### POST /api/marketplace/offers/:id/accept (legacy) diff --git a/03 - API Reference/Payment API.md b/03 - API Reference/Payment API.md index 245ff28..24bd3a8 100644 --- a/03 - API Reference/Payment API.md +++ b/03 - API Reference/Payment API.md @@ -5,7 +5,7 @@ tags: [api, payment, reference, request-network, escrow] # Payment API -> **Last updated:** 2026-05-29 — aligned with code (see [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)) +> **Last updated:** 2026-05-30 — AMN Pay Scanner integration, on-demand RN reconcile in GET /payment/:id, pay-in route renamed, reload/probe routes now implemented The payment surface is split across provider-neutral payment routers, Request Network checkout/webhook routes, derived-destination custody routes, and admin safety routes: @@ -16,6 +16,7 @@ The payment surface is split across provider-neutral payment routers, Request Ne | `/api/payment/request-network/*` | [`requestNetwork/requestNetworkRoutes.ts`](../../backend/src/services/payment/requestNetwork/requestNetworkRoutes.ts) | Request Network intent creation, in-house checkout payloads, webhook processing | | `/api/payment/derived-destinations/*` | [`wallets/derivedDestinationRoutes.ts`](../../backend/src/services/payment/wallets/derivedDestinationRoutes.ts) | Derived destination inspection, balance checks, and sweeping | | `/api/payment/decentralized/*` | [`decentralizedPaymentRoutes.ts`](../../backend/src/services/payment/decentralizedPaymentRoutes.ts) | Legacy wallet-direct confirmations | +| `/api/payment/amn-scanner/*` | [`routes/amnScannerWebhookRoutes.ts`](../../backend/src/routes/amnScannerWebhookRoutes.ts) | AMN Pay Scanner webhook receiver | | `/api/admin/rn/networks/*` | [`requestNetwork/networkRegistryRoutes.ts`](../../backend/src/services/payment/requestNetwork/networkRegistryRoutes.ts) | Request Network chain/token registry | | `/api/admin/payments/awaiting-confirmation/*` | `awaitingConfirmationRoutes.ts` | Admin queue for payments waiting on confirmation/safety checks | @@ -52,7 +53,7 @@ Core model: [[Payment]]. Coordination logic to avoid race conditions when multip ### POST /api/payment -**Description:** Create a payment record manually. Normal buyer checkout should use `POST /api/payment/request-network/intents`. +**Description:** Create a payment record manually. Normal buyer checkout should use `POST /api/payment/request-network/pay-in`. **Auth required:** Bearer JWT **Request body:** ```ts @@ -90,7 +91,7 @@ Core model: [[Payment]]. Coordination logic to avoid race conditions when multip ### GET /api/payment/:id -**Description:** Fetch a payment by id. +**Description:** Fetch a payment by id. For payments with `provider: 'request.network'` that are still `pending`, this endpoint also performs an **on-demand RN reconcile**: it queries the Request Network node live, and if RN reports the request as paid it immediately marks the payment `completed`, advances the purchase request to `processing`, persists `selectedOfferId`, and accepts the winning offer while rejecting all others. This reconcile path exists because RN webhooks cannot reach a local dev server and the reconcile cron is not started there; the same logic fires in production as a safety net. **Auth required:** Bearer JWT **Errors:** `404` not found. @@ -126,19 +127,19 @@ Core model: [[Payment]]. Coordination logic to avoid race conditions when multip ### POST /api/payment/payments/:id/fetch-tx **Description:** Re-queries the blockchain to fetch the missing `transactionHash` for a completed payment. -**⚠️ SECURITY — NO AUTHENTICATION:** This endpoint has no authentication guard. Any unauthenticated caller can trigger a blockchain re-query for any payment ID. +**Auth required:** Bearer JWT (admin) — `authenticateToken` + `authorizeRoles('admin')` added in commit `1d881c5` (ISSUE-005 fix). **Response 200:** `{ success, transactionHash, network, source, message }` ### POST /api/payment/payments/auto-fetch-missing **Description:** Batch tx-hash backfill across the database. -**⚠️ SECURITY — NO AUTHENTICATION:** This endpoint has no authentication guard. Any unauthenticated caller can trigger a full database backfill scan. +**Auth required:** Bearer JWT (admin) — `authenticateToken` + `authorizeRoles('admin')` added in commit `1d881c5` (ISSUE-005 fix). **Request body:** `{ limit?: number }` (default 10) ### GET /api/payment/payments/:id/debug **Description:** Debug bundle including the raw payment, blockchain metadata, and wallet-monitor status. Intended for admin / development. -**⚠️ SECURITY — NO AUTHENTICATION:** Despite exposing full payment data, this endpoint has no authentication guard. Any unauthenticated caller can retrieve complete payment details for any payment ID. +**Auth required:** Bearer JWT (admin) — `authenticateToken` + `authorizeRoles('admin')` added in commit `1d881c5` (ISSUE-005 fix). ### POST /api/payment/callback @@ -153,9 +154,9 @@ Core model: [[Payment]]. Coordination logic to avoid race conditions when multip ## Request Network - Pay-in -### POST /api/payment/request-network/intents +### POST /api/payment/request-network/pay-in -**Description:** Creates a Request Network pay-in intent and stores a [[Payment]] with `provider: "request.network"`. The service can attach a per-payment derived destination before creating the provider request. +**Description:** Creates a Request Network pay-in intent and stores a [[Payment]] with `provider: "request.network"`. The service can attach a per-payment derived destination before creating the provider request. This is the **current active route** (mounted at `/api/payment/request-network/pay-in`). The `/intents` path listed in older docs is an alias; use `pay-in` for new integrations. **Auth required:** Bearer JWT (buyer) **Request body:** ```ts @@ -182,7 +183,35 @@ Core model: [[Payment]]. Coordination logic to avoid race conditions when multip **Auth required:** No (signature-protected) **Response:** `200` when processed or duplicate; `202` when accepted but safety checks are pending; `401` for invalid signature. -> ⚠️ **NOT IMPLEMENTED:** `POST /api/payment/request-network/:id/payout/initiate`, `POST /api/payment/request-network/:id/payout/confirm`, `POST /api/payment/request-network/:id/release/confirm`, and `POST /api/payment/request-network/:id/refund/confirm` do not exist in the codebase. Do not call these paths. +> [!note] RN payout/release/refund routes +> `POST /api/payment/request-network/:paymentId/payout/initiate`, `POST /api/payment/request-network/:paymentId/payout/confirm`, `POST /api/payment/request-network/:paymentId/release/confirm`, and `POST /api/payment/request-network/:paymentId/refund/confirm` are registered in `requestNetworkRoutes.ts` but are stub-level implementations. They accept the request and return a 200 but do not yet drive the ledger-gated release/refund orchestration. Use `POST /api/payment/:id/release` and `POST /api/payment/:id/refund` for actual escrow releases. + +## AMN Pay Scanner - Pay-in + +AMN Pay Scanner is a custom in-house blockchain scanner that replaces the hosted Request Network page for payment monitoring. It speaks the same `PaymentProviderAdapter` interface as the RN adapter. + +### POST /api/payment/amn-scanner/webhook + +**Description:** AMN Pay Scanner posts settlement confirmations here. The route verifies a `webhookSecret`-based HMAC signature, then runs the Transaction Safety Provider and `PaymentCoordinator` pipeline identical to the RN webhook path. +**Auth required:** No (signature-protected via `AMN_SCANNER_WEBHOOK_SECRET`) +**Request body:** `{ intentId, status, transactionHash?, chainId?, ... }` — scanner-specific envelope +**Response:** `200` processed; `401` bad signature; `400` missing `intentId` or unknown format; `404` payment not found. +**Side effects:** Same as the RN webhook — updates [[Payment]], advances [[PurchaseRequest]], accepts/rejects offers, emits socket events when safety checks pass. + +> [!note] Provider value +> Payments created via the AMN Pay Scanner have `provider: 'amn.scanner'` in the database. This is distinct from `request.network` and `shkeeper`. + +### GET /api/admin/scanner/status + +**Description:** Proxies to `AMN_SCANNER_URL/scanner/status` and returns the scanner's internal state. +**Auth required:** Bearer JWT (`admin`) — `authenticateToken` + `authorizeRoles('admin')` are now applied (the previously documented security gap — unauthenticated access — has been fixed in commit `1d881c5`). +**Response 200:** Scanner status JSON forwarded from the upstream service. + +### POST /api/admin/scanner/webhooks/retry + +**Description:** Triggers a manual retry of failed/pending scanner webhooks. +**Auth required:** Bearer JWT (`admin`) +**Request body:** `{ intentId?: string }` — omit to retry all pending. ## Legacy SHKeeper - Pay-in @@ -555,7 +584,13 @@ Escrow state (`escrowState`): `unfunded` → `funded` → `released` | `refunded } ``` -> ⚠️ **NOT IMPLEMENTED:** `GET /api/admin/settings/confirmation-thresholds/history` does not exist. Only the current-values GET and per-chain PATCH endpoints are implemented. +### `GET /api/admin/settings/confirmation-thresholds/history` + +**Auth:** Admin only +**Description:** Returns paginated audit log of past confirmation threshold changes. Each entry records the admin who made the change, old/new threshold values, chain ID, and timestamp. Backed by the `ConfigSettingHistory` Mongoose model added in commit `27fb15a` (task #9). +**Response 200:** `{ success: true, data: [{ chainId, oldThreshold, newThreshold, changedBy, changedAt }] }` + +> **Note:** This endpoint was previously documented as NOT IMPLEMENTED. It was added in commit `27fb15a` and is now live at `/api/admin/settings/confirmation-thresholds/history`. ## Payments awaiting confirmation (admin) @@ -613,7 +648,33 @@ Escrow state (`escrowState`): `unfunded` → `funded` → `released` | `refunded } ``` -> ⚠️ **NOT IMPLEMENTED:** `POST /api/admin/rn/networks/reload` and `POST /api/admin/rn/networks/probe/:chainId` do not exist in the codebase. +### `POST /api/admin/rn/networks/reload` + +**Auth:** Admin only +**Description:** Reloads the chain and token registries from disk (`supportedChains.json` and `tokens.json`). Returns `{ success: true, message: 'Registry reloaded from disk' }`. Use this after updating the JSON files without restarting the server. + +> **Note:** This route is now implemented (commit `5681abf`). Earlier docs incorrectly listed it as not implemented. + +### `POST /api/admin/rn/networks/probe/:chainId` + +**Auth:** Admin only +**Description:** Performs a live on-chain probe for the specified chain: verifies RPC reachability, checks for deployed proxy contract bytecode (`eth_getCode`), and test-calls the proxy with a dummy payload to confirm it reverts meaningfully. Returns: +```json +{ + "success": true, + "data": { + "chainId": 56, + "reachable": true, + "hasCode": true, + "callValid": true, + "blockNumber": "0x...", + "latencyMs": 120 + } +} +``` +Errors: `400` if `chainId` is not a number; `404` if the chain is not in the registry; `500` on RPC failure. + +> **Note:** This route is now implemented (commit `5681abf`). Earlier docs incorrectly listed it as not implemented. ## Related diff --git a/03 - API Reference/Scanner API.md b/03 - API Reference/Scanner API.md new file mode 100644 index 0000000..05d0587 --- /dev/null +++ b/03 - API Reference/Scanner API.md @@ -0,0 +1,249 @@ +--- +title: Scanner API +tags: [api, scanner, payment] +created: 2026-05-30 +--- + +# Scanner API + +HTTP API exposed by the AMN Pay Scanner microservice. Default port: `8080`. + +All endpoints except `/health` require `Authorization: Bearer ` when the key is configured in the environment (production). In dev mode (key not set) all requests are allowed. + +Base URL (dev): `http://localhost:8080` + +--- + +## Authentication + +``` +Authorization: Bearer +``` + +- Uses constant-time comparison to prevent timing attacks. +- Returns `401 {"error":"unauthorized"}` on failure. +- `/health` is explicitly excluded from auth — always open. + +--- + +## POST /intents + +Register a new payment intent. The scanner will watch the specified chain for a matching transfer and call back to `callbackUrl` when confirmed. + +**Request body** (`application/json`): + +| Field | Type | Required | Notes | +|---|---|---|---| +| `intentId` | string | yes | Caller-supplied unique ID (UUID recommended) | +| `chainId` | integer | yes | Numeric chain ID (e.g. 56, 137, 728126428) | +| `tokenAddress` | string | yes | Token contract address. EVM/Tron: lowercase 0x hex. TON: exact base64url or raw format | +| `destination` | string | yes | Receiving wallet address. EVM/Tron: 0x hex. TON: base64url | +| `amount` | string | yes | Amount in smallest unit (wei / token decimals) as a base-10 integer string | +| `callbackUrl` | string | yes | URL the scanner POSTs to on confirmation | +| `callbackSecret` | string | yes | HMAC key for `X-AMN-Signature` verification | +| `confirmations` | integer | no | Override chain default confirmation count (0 = use chain default) | + +**Example request:** + +```json +{ + "intentId": "a1b2c3d4-...", + "chainId": 56, + "tokenAddress": "0x55d398326f99059ff775485246999027b3197955", + "destination": "0xAbCd1234...", + "amount": "10000000000000000000", + "callbackUrl": "https://api.amn.gg/api/payment/scanner-callback", + "callbackSecret": "abc123...", + "confirmations": 12 +} +``` + +**Response `200 OK`:** + +```json +{ + "intentId": "a1b2c3d4-...", + "paymentReference": "0x1a2b3c4d5e6f7a8b", + "checkoutBlock": { + "destination": "0xabcd1234...", + "tokenAddress": "0x55d398326f99059ff775485246999027b3197955", + "tokenSymbol": "USDT", + "decimals": 18, + "chainId": 56, + "proxyAddress": "0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9", + "paymentReference": "0x1a2b3c4d5e6f7a8b", + "feeAmount": "0", + "feeAddress": "0x000000000000000000000000000000000000dEaD", + "amountWei": "10000000000000000000" + } +} +``` + +**Idempotency**: If `intentId` already exists the existing intent's checkout block is returned (no error). + +**Error cases:** + +| Status | Body | Cause | +|---|---|---| +| 400 | `{"error":"intentId is required"}` | Missing field | +| 400 | `{"error":"amount must be a positive integer string (base-10 wei)"}` | Non-numeric or zero amount | +| 400 | `{"error":"unsupported chainId: 999"}` | Chain not in supported-chains.json | +| 500 | `{"error":"internal error"}` | DB write failure | + +--- + +## GET /intents/{intentId} + +Fetch the current state of a payment intent. + +**Response `200 OK`:** Full `Intent` object (see Data Models below). + +`callbackSecret` is excluded from the response regardless of auth state. + +**Error cases:** + +| Status | Body | Cause | +|---|---|---| +| 404 | `{"error":"intent not found"}` | Unknown intentId | + +--- + +## GET /scanner/status + +Returns scan progress for all verified chains. + +**Response `200 OK`:** + +```json +{ + "chains": [ + { + "chainId": 56, + "name": "BSC", + "chainType": "evm", + "lastScannedBlock": 39000000, + "chainHead": 39000015, + "lag": 15, + "pendingIntents": 3 + }, + { + "chainId": 728126428, + "name": "TRX", + "chainType": "tron", + "lastScannedBlock": 1748500000000, + "chainHead": 1748500015000, + "lag": 15000, + "pendingIntents": 1 + }, + { + "chainId": 1100, + "name": "TON", + "chainType": "ton", + "lastScannedBlock": 1748500000, + "chainHead": 1748500015, + "lag": 15, + "pendingIntents": 0 + } + ] +} +``` + +**Note on lag units**: For EVM and Tron chains, `lag` is in blocks (or ms-timestamp difference). For TON, `lag` is in seconds (Unix timestamps). + +--- + +## POST /admin/webhooks/retry + +Immediately trigger a re-delivery attempt for all `webhook_failed` intents. Normally the scanner retries automatically every `WEBHOOK_RETRY_HOURS`; this endpoint forces an immediate pass. + +**Response `200 OK`:** + +```json +{ "queued": 2 } +``` + +Each retry is dispatched in a separate goroutine. Success resets the intent status to `confirmed` and records `webhook_delivered_at`. + +--- + +## GET /health + +Health check. No authentication required. + +**Response `200 OK`:** + +```json +{ "status": "ok", "time": "2026-05-30T12:00:00Z" } +``` + +Used by Docker `HEALTHCHECK` and upstream load balancers / Gatus monitoring. + +--- + +## Webhook delivery (outbound) + +When an intent is confirmed the scanner POSTs to `callbackUrl`: + +**Headers:** + +| Header | Value | +|---|---| +| `Content-Type` | `application/json` | +| `X-AMN-Signature` | `hex(HMAC-SHA256(body, callbackSecret))` | +| `X-AMN-Delivery-ID` | intentId | +| `X-AMN-Retry` | `true` (only on manual retry via /admin/webhooks/retry) | + +**Body:** + +```json +{ + "intentId": "a1b2c3d4-...", + "paymentReference": "0x1a2b3c4d5e6f7a8b", + "txHash": "0xdeadbeef...", + "blockNumber": 39000010, + "amount": "10000000000000000000", + "token": "0x55d398326f99059ff775485246999027b3197955", + "chainId": 56, + "status": "confirmed" +} +``` + +**Retry schedule** (on non-2xx or network error): 5 s → 30 s → 2 min → 10 min → 1 h → `webhook_failed`. + +The backend should verify `X-AMN-Signature` to reject forged callbacks: + +```js +const expected = createHmac('sha256', callbackSecret).update(rawBody).digest('hex'); +if (!timingSafeEqual(Buffer.from(received), Buffer.from(expected))) reject(); +``` + +--- + +## Data models + +### Intent object + +```json +{ + "intentId": "string", + "chainId": 56, + "chainType": "evm", + "tokenAddress": "0x...", + "destination": "0x...", + "amount": "10000000000000000000", + "paymentReference": "0x1a2b3c4d", + "topicRef": "0xdeadbeef...", + "status": "pending | confirming | confirmed | expired | webhook_failed", + "confirmationsRequired": 12, + "txHash": null, + "logIndex": null, + "blockNumber": null, + "confirmations": 0, + "salt": "hex64chars", + "webhookDeliveredAt": null, + "createdAt": "2026-05-30T10:00:00Z", + "updatedAt": "2026-05-30T10:00:00Z" +} +``` + +Note: `callbackUrl` and `callbackSecret` are present in the DB but `callbackSecret` is always omitted from API responses. diff --git a/03 - API Reference/Trezor API.md b/03 - API Reference/Trezor API.md index cc3ae60..755df00 100644 --- a/03 - API Reference/Trezor API.md +++ b/03 - API Reference/Trezor API.md @@ -3,7 +3,7 @@ title: Trezor API tags: [api, payments, trezor, safekeeping] --- -> **Last updated:** 2026-05-29 — aligned with code (see Doc vs Code Audit Report) +> **Last updated:** 2026-05-30 — break-glass mode added (commit `b21df25`) # Trezor API @@ -17,6 +17,12 @@ TREZOR_SAFEKEEPING_REQUIRED=false Only the literal value `true` makes Trezor proof mandatory during release/refund confirmation. When unset or `false`, release/refund flows continue without Trezor proof. +## Break-glass mode + +When `TREZOR_SAFEKEEPING_REQUIRED=true` and the Trezor is unavailable (lost, dead battery, etc.), an admin can activate break-glass mode to bypass Trezor for up to 1 hour. Break-glass state is in-memory only and resets on server restart. + +See [[Admin API]] — _Break-glass (Trezor bypass)_ section for the three management endpoints (`GET`, `POST`, `DELETE /api/admin/settings/break-glass`). Activating break-glass fires an immediate Telegram alert via `tgNotify`. + ## GET /api/trezor/registration-message Builds the exact message the user must sign to register a Trezor xpub. diff --git a/04 - Flows/Authentication Flow.md b/04 - Flows/Authentication Flow.md index 64259bd..644272b 100644 --- a/04 - Flows/Authentication Flow.md +++ b/04 - Flows/Authentication Flow.md @@ -37,7 +37,8 @@ End-to-end specification for **email + password** authentication, JWT issuance, 2. **Client-side guards**: `signInWithPassword()` (`action.ts:32-116`) verifies the browser is online and `localStorage` is writable; otherwise it throws a typed `AuthErrorHandler` error. 3. **HTTP request**: The frontend POSTs `{ email, password }` to `POST /api/auth/login` (resolved by `endpoints.auth.login` in `frontend/src/lib/axios.ts`). An `AbortController` is armed with a 60-second timeout. 4. **Validation middleware** runs `loginValidation` (`backend/src/services/auth/authValidation.ts`) — wires into Express via `authRoutes.ts:22`. -5. **Rate limiting**: `rateLimitService.checkLoginAttempts(email, 5, 15*60)` is called (`authController.ts:173`). The counter is incremented on **every login attempt** (before password comparison), not only on failures. Once 5 total attempts accumulate within a 15-minute window, the endpoint returns `429 TOO_MANY_ATTEMPTS`. The counter is reset upon a fully successful login (step 9). Counters live in Redis so they survive restarts. +5. **Cloudflare Turnstile CAPTCHA gate** (`captchaGate` middleware, commit `b8edbbf`): Before the rate-limiter runs, `captchaGate` checks the in-memory failure counter for the caller's IP. If that IP has accumulated **3 or more failed login attempts** within 15 minutes, a valid `cf-turnstile-response` token must be present in the request body. Without it the endpoint returns `429 { captchaRequired: true }`. If `TURNSTILE_SECRET_KEY` is not set (local dev), the gate is skipped. On CAPTCHA pass, the middleware calls Cloudflare's `siteverify` endpoint to validate the token before proceeding. +5a. **Rate limiting**: `rateLimitService.checkLoginAttempts(email, 5, 15*60)` is called (`authController.ts:173`). The counter is incremented on **every login attempt** (before password comparison), not only on failures. Once 5 total attempts accumulate within a 15-minute window, the endpoint returns `429 TOO_MANY_ATTEMPTS`. The counter is reset upon a fully successful login (step 9). Counters live in Redis so they survive restarts. 6. **User lookup**: `User.findOne({ email, status: "active" }).select("+password")` — `password` is `select: false` by default in the schema and must be explicitly projected. 7. **Password comparison**: `authService.comparePassword()` invokes `bcrypt.compare()` (cost factor 12 — see `authService.ts:102-105`). Constant-time per bcrypt's design. 8. **Email-verification gate**: If `!user.isEmailVerified`, returns `403 EMAIL_NOT_VERIFIED` with `needsVerification: true`. The frontend intercepts this in `action.ts:104-111` and redirects to `/auth/jwt/verify?email=...`. diff --git a/04 - Flows/Dispute Flow.md b/04 - Flows/Dispute Flow.md index 979679a..87a7ce2 100644 --- a/04 - Flows/Dispute Flow.md +++ b/04 - Flows/Dispute Flow.md @@ -12,7 +12,8 @@ audit: "2026-05-29 — corrected against source: Dispute.ts, DisputeService.ts, When something goes wrong (item not delivered, wrong item, seller misbehaviour), either party can open a **dispute**. A three-way chat (buyer, seller, admin mediator) is created automatically. After evidence is gathered, the admin **resolves** the dispute — selecting an action such as refund, replacement, compensation, warning, or ban. -> [!danger] SECURITY — Three open privilege-escalation bugs exist as of this audit. See [Security Gaps](#security-gaps) below. +> [!success] Security fixes applied (2026-05-30) +> The three privilege-escalation bugs documented in the original Security Gaps section were fixed in commit `1d881c5` (ISSUE-003, ISSUE-004) and `fce8a19` (resolver role). Role guards are now enforced on assign/status/resolve; route shadowing is eliminated by remounting the release-hold router at `/api/disputes/pr`. See [Security Gaps](#security-gaps) for the historical record and current state. > [!warning] Real-time events not implemented > Every Socket.IO emit in `DisputeService` is currently commented out. No `dispute-updated`, `new-notification`, or any other socket event fires for dispute creation, admin assignment, status changes, evidence uploads, or resolution. The dispute feature is CRUD-only at this stage. @@ -23,7 +24,8 @@ When something goes wrong (item not delivered, wrong item, seller misbehaviour), - **Seller** — party against whom the dispute is raised (or in rarer cases, initiator). - **Admin / Mediator** — assigned to investigate. - **Frontend** — buyer/seller "Report issue" buttons in the request detail view; admin dispute dashboard. -- **Backend** — `DisputeService` (`backend/src/services/dispute/DisputeService.ts`), dashboard/controller routes at `backend/src/routes/disputeRoutes.ts` (mounted first at `/api/disputes`), and release-hold helpers in `backend/src/services/dispute/disputeRoutes.ts` (mounted second at `/api/disputes`). +- **Admin / Mediator** — assigned to investigate (role `admin` or `resolver`). +- **Backend** — `DisputeService` (`backend/src/services/dispute/DisputeService.ts`), dashboard/controller routes at `backend/src/routes/disputeRoutes.ts` (mounted at `/api/disputes`), and release-hold helpers in `backend/src/services/dispute/disputeRoutes.ts` (mounted at `/api/disputes/pr` since commit `1d881c5`). - **MongoDB** — `disputes`, `chats`, `purchaserequests`, `payments`. - **Socket.IO** — no events fire today; all emits are TODO stubs (see warning above). @@ -78,59 +80,40 @@ Valid values: `product_quality | delivery_delay | wrong_item | payment_issue | s --- -## Security Gaps +## Security Gaps (Historical — All Closed as of 2026-05-30) -### 1. `PATCH /api/disputes/:id/status` — no role guard +The following bugs were identified in the 2026-05-29 audit and fixed in commits `1d881c5` and `fce8a19`. The descriptions below are preserved for historical reference and audit trail. -**File:** `backend/src/routes/disputeRoutes.ts` line 26 +### 1. `PATCH /api/disputes/:id/status` — no role guard ✅ FIXED -```ts -router.patch('/:id/status', DisputeController.updateStatus); -``` +**Fix:** `authorizeRoles('admin', 'resolver')` added in commit `fce8a19`. Only admins and resolvers can change dispute status. -Despite comments in the router saying "admin only", there is **no `authorizeRoles` middleware**. Any authenticated buyer or seller can call this endpoint and change a dispute's status to `resolved` or `closed`, bypassing the admin resolution flow entirely. This is an open privilege-escalation bug. +### 2. `POST /api/disputes/:id/resolve` (dashboard router) — no role guard ✅ FIXED -### 2. `POST /api/disputes/:id/resolve` (dashboard router) — no role guard +**Fix:** `authorizeRoles('admin', 'resolver')` added in commit `fce8a19`. Only admins and resolvers can resolve disputes. -**File:** `backend/src/routes/disputeRoutes.ts` line 29 +**Additional fix (ISSUE-004, commit `1d881c5`):** `DisputeService.resolveDispute` now calls `releaseHoldResolve()` on the linked `purchaseRequestId`, clearing the escrow hold automatically so the payment release is unblocked after resolution. -```ts -router.post('/:id/resolve', DisputeController.resolveDispute); -``` +### 3. `POST /api/disputes/:id/assign` — no role guard ✅ FIXED -No role guard. Any authenticated user can post a resolution — including `action: 'ban_seller'`. Note that the **release-hold router's** `POST /:purchaseRequestId/resolve` (`backend/src/services/dispute/disputeRoutes.ts` line 77) **does** correctly apply `authorizeRoles('admin')`. The dashboard router's resolve endpoint does not. - -### 3. `POST /api/disputes/:id/assign` — no role guard - -**File:** `backend/src/routes/disputeRoutes.ts` line 23 - -```ts -router.post('/:id/assign', DisputeController.assignAdmin); -``` - -Any authenticated user can call this with their own user ID in `{ adminId }` and self-assign as mediator for any dispute. +**Fix:** `authorizeRoles('admin', 'resolver')` added in commit `fce8a19`. Only admins and resolvers can assign mediators. --- -## Route Shadowing +## Route Shadowing (Historical — Resolved as of 2026-05-30) -Both routers are mounted at `/api/disputes` in `app.ts`: +Previously both routers were mounted at `/api/disputes`, causing the dashboard router to intercept release-hold requests. Fixed in commit `1d881c5` (ISSUE-003): ```ts -// app.ts line 521 — mounted FIRST +// app.ts — current state app.use("/api/disputes", dashboardDisputeRoutes); // src/routes/disputeRoutes.ts - -// app.ts line 585 — mounted SECOND -app.use("/api/disputes", disputeRoutes); // src/services/dispute/disputeRoutes.ts +app.use("/api/disputes/pr", disputeRoutes); // src/services/dispute/disputeRoutes.ts — new prefix ``` -Express evaluates routes in registration order. This creates two concrete hazards: - -1. **`POST /api/disputes/:id/resolve`** — the dashboard router (mounted first) exposes `POST /:id/resolve` with no role guard. A request intended for the release-hold router's `POST /:purchaseRequestId/resolve` (which **does** require admin) will be intercepted and handled by the wrong, unguarded handler when a matching dispute `_id` is supplied. - -2. **`POST /api/disputes/:purchaseRequestId/raise`** — this route exists only in the second (release-hold) router. It will be reached correctly only if the dashboard router does not first match the path. Since the dashboard router has no `/raise` route, requests pass through. However, as more routes are added to either router, collisions will grow silently. - -**Recommendation:** Separate the two routers onto distinct path prefixes (e.g. `/api/disputes` for the dashboard controller, `/api/disputes/hold` for the release-hold service). +Release-hold endpoints now use the `/api/disputes/pr/` prefix: +- `POST /api/disputes/pr/:purchaseRequestId/raise` +- `GET /api/disputes/pr/:purchaseRequestId/status` +- `POST /api/disputes/pr/:purchaseRequestId/resolve` --- @@ -171,7 +154,7 @@ Express evaluates routes in registration order. This creates two concrete hazard 9. All three parties chat in the dispute chat room (same socket mechanics as [[Chat Flow]]). Each party can upload more evidence via `POST /api/disputes/:id/evidence` — `DisputeService.addEvidence` (`:305-337`) appends to `dispute.evidence[]` and writes a `timeline` entry `evidence_added`. **No socket event fires for evidence uploads.** 10. The admin may also `PATCH /api/disputes/:id/status` with intermediate states or notes; this updates `dispute.status` and writes a `timeline` entry `status_changed`. **No socket event fires.** -> [!danger] `PATCH /api/disputes/:id/status` has no role guard — any authenticated user can change dispute status (see [Security Gaps](#security-gaps)). +> [!note] `PATCH /api/disputes/:id/status` now requires `admin` or `resolver` role (`authorizeRoles('admin', 'resolver')`, commit `fce8a19`). The previously open privilege-escalation gap is closed. ### Phase 4 — Resolution @@ -190,10 +173,11 @@ Express evaluates routes in registration order. This creates two concrete hazard - `dispute.closedAt = now` - Appends `timeline` entry `dispute_resolved`. - Saves. + - **Calls `releaseHoldResolve(purchaseRequestId)`** — this clears the escrow hold automatically so the payment release is unblocked (ISSUE-004 fix, commit `1d881c5`). - **No socket event fires.** (`// TODO: Send notifications via Socket.IO`) -13. **Financial side-effect (manual today):** depending on the action, the admin then triggers either the **release** ([[Payout Flow]] / [[Escrow Flow]]) or the **refund** as a separate step. The dispute service records the resolution; full automatic dispatch through the release/refund policy engine is still a hardening item. +13. **Financial side-effect:** as of commit `1d881c5` the escrow hold is cleared automatically on resolution. The admin still needs to separately trigger the ledger-gated release ([[Payout Flow]] / [[Escrow Flow]]) or refund for actual fund movement. -> [!danger] `POST /api/disputes/:id/resolve` (dashboard router) has no role guard — any authenticated user can post any resolution action including `ban_seller` (see [Security Gaps](#security-gaps)). +> [!note] `POST /api/disputes/:id/resolve` now requires `admin` or `resolver` role (`authorizeRoles('admin', 'resolver')`, commit `fce8a19`). The previously open privilege-escalation gap is closed. --- diff --git a/04 - Flows/Payment Flow - Scanner.md b/04 - Flows/Payment Flow - Scanner.md new file mode 100644 index 0000000..c7e31bc --- /dev/null +++ b/04 - Flows/Payment Flow - Scanner.md @@ -0,0 +1,179 @@ +--- +title: Payment Flow - Scanner (In-House) +tags: [flow, scanner, payment] +created: 2026-05-30 +--- + +# Payment Flow — AMN Pay Scanner (In-House) + +End-to-end payment flow using the in-house AMN Pay Scanner, replacing the Request Network integration. The scanner is a separate microservice; the backend talks to it over an internal HTTP API. + +See also: [Scanner Architecture](../01%20-%20Architecture/Scanner%20Architecture.md), [Scanner API](../03%20-%20API%20Reference/Scanner%20API.md) + +--- + +## 1. High-level sequence + +``` +Buyer Backend Scanner Chain + │ │ │ │ + │ initiate payment │ │ │ + │────────────────────►│ │ │ + │ │ POST /intents │ │ + │ │───────────────────►│ │ + │ │ 200 checkoutBlock │ │ + │ │◄───────────────────│ │ + │ checkoutBlock │ │ │ + │◄────────────────────│ │ │ + │ │ │ │ + │ sign + submit tx ──────────────────────────────────────►│ + │ │ │ (polling) │ + │ │ │◄────────────────│ + │ │ │ log matched │ + │ │ │ confirmations… │ + │ │◄───────────────────│ │ + │ │ POST callbackUrl │ │ + │ │ (webhook) │ │ + │ │ │ │ + │ payment confirmed │ │ │ + │◄────────────────────│ │ │ +``` + +--- + +## 2. Step-by-step + +### Step 1 — Backend creates an intent + +When the buyer chooses a payment method (e.g. USDT on BSC), the backend calls: + +``` +POST http://scanner:8080/intents +Authorization: Bearer + +{ + "intentId": "", + "chainId": 56, + "tokenAddress": "0x55d398326f99059ff775485246999027b3197955", + "destination": "0xSellerWalletAddress", + "amount": "10000000000000000000", + "callbackUrl": "https://api.amn.gg/api/payment/scanner-callback", + "callbackSecret": "", + "confirmations": 12 +} +``` + +The scanner responds with a `checkoutBlock` that the backend passes to the frontend. + +### Step 2 — Frontend shows checkout + +The `checkoutBlock` contains everything the frontend needs to build the `ERC20FeeProxy.transferWithReferenceAndFee` calldata: + +| Field | Used for | +|---|---| +| `proxyAddress` | contract to call | +| `tokenAddress` | ERC20 token | +| `destination` | `_to` param | +| `paymentReference` | `_paymentReference` param (8-byte reference) | +| `amountWei` | `_amount` param | +| `feeAmount` | `_feeAmount` param (always `"0"` currently) | +| `feeAddress` | `_feeAddress` param (always dead address) | + +For Tron/TON the buyer sends a plain TRC20/Jetton transfer to `destination`; there is no proxy contract. + +### Step 3 — Buyer submits transaction + +The buyer signs and broadcasts the transaction using their wallet. The scanner independently monitors the chain and does not require the transaction hash. + +### Step 4 — Scanner detects and confirms + +**EVM path:** +1. `eth_getLogs` returns a `TransferWithReferenceAndFee` log matching `topicRef` +2. `validateLogMatchesIntent` verifies token address, destination, and amount +3. Intent moves to `confirming`; scanner waits for N blocks +4. Once `confirmationsRequired` blocks have been built on top, intent moves to `confirmed` + +**Tron path:** +1. TronGrid `Transfer` event matches `destination` (EVM-hex normalized) +2. Amount validated ≥ intent amount +3. Intent goes directly to `confirmed` (TronGrid returns only confirmed txs) + +**TON path:** +1. TonCenter Jetton transfer matches `destination` (exact base64url) and `jetton_master_address` +2. Amount validated ≥ intent amount +3. Intent goes directly to `confirmed` + +### Step 5 — Webhook delivery + +The scanner POSTs to `callbackUrl` with: + +```json +{ + "intentId": "...", + "paymentReference": "0x...", + "txHash": "0x...", + "blockNumber": 39000010, + "amount": "10000000000000000000", + "token": "0x55d...", + "chainId": 56, + "status": "confirmed" +} +``` + +Header `X-AMN-Signature` = `HMAC-SHA256(body, callbackSecret)`. + +The backend verifies the signature, matches the intentId to a Payment record, and marks it paid. + +### Step 6 — Backend acknowledges + +Backend returns a 2xx response. Scanner records `webhook_delivered_at` and the intent lifecycle ends. + +--- + +## 3. Failure paths + +### Webhook delivery failure + +If the backend returns non-2xx or is unreachable, the scanner retries: + +``` +attempt 1: after 5 s +attempt 2: after 30 s +attempt 3: after 2 min +attempt 4: after 10 min +attempt 5: after 1 h +→ status = webhook_failed +``` + +`webhook_failed` intents are retried every `WEBHOOK_RETRY_HOURS` (default 6 h) and on `POST /admin/webhooks/retry`. + +On startup the scanner reconciles any `confirmed` intents with `webhook_delivered_at IS NULL` (crash recovery). + +### Intent expiry + +Intents in `pending` or `confirming` status older than `INTENT_TTL_HOURS` (default 24 h) are moved to `expired` by a background ticker running every hour. + +`confirming` intents can get stuck if a transaction is deep-reorganised and never re-included; the TTL frees the destination address for reuse. + +### Amount underpayment + +Transfers where the on-chain amount is less than `intent.Amount` are silently skipped. The intent remains `pending` until the TTL. + +### Wrong token or destination + +The EVM log decoder validates all three fields (token, destination, amount). Mismatches are logged as `REJECT` and skipped. The intent remains `pending`. + +--- + +## 4. Key differences from Request Network integration + +| Dimension | Request Network | AMN Pay Scanner | +|---|---|---| +| Dependency | RN SDK + API | None (direct RPC) | +| Payment reference | RN-generated | Internal HMAC derivation | +| EVM matching | By reference hash (RN) | By Topics[1] / topicRef (indexed) | +| Tron | Not supported | TRC20 Transfer events via TronGrid | +| TON | Not supported | Jetton transfers via TonCenter v3 | +| Confirmations | RN handled | Per-chain configurable | +| Webhook | RN webhook → backend adapter | Scanner → backend directly | +| State store | External (RN cloud) | Internal SQLite | diff --git a/04 - Flows/Seller Offer Flow.md b/04 - Flows/Seller Offer Flow.md index 86c492b..adb60b7 100644 --- a/04 - Flows/Seller Offer Flow.md +++ b/04 - Flows/Seller Offer Flow.md @@ -5,7 +5,7 @@ related_models: ["[[SellerOffer]]", "[[PurchaseRequest]]", "[[Notification]]"] related_apis: ["POST /api/marketplace/purchase-requests/:id/offers", "GET /api/marketplace/purchase-requests/:id/offers", "PATCH /api/marketplace/offers/:id"] --- -> **Last updated:** 2026-05-29 — aligned with code (see [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)) +> **Last updated:** 2026-05-30 — updated for offer-management page, `withdrawOffer` action, edit-while-pending, `getSellerOffers` API (commits 240a668–e7d1375) # Seller Offer Flow @@ -90,24 +90,22 @@ The valid `SellerOffer` statuses are `pending | accepted | rejected | withdrawn` - Notifications: `notifyOfferAccepted` to the winning seller, generic rejection notifications to the others (`SellerOfferService.acceptOffer` does the same in the manual path). - Socket events notify the winner and reject/close competing offers. -### Withdrawal +### Edit / withdrawal while awaiting buyer acceptance -17. ⚠️ **`POST /api/marketplace/offers/:id/withdraw` does NOT exist as an HTTP route.** The `SellerOfferService.withdrawOffer()` service method exists but is dead code — it is not wired to any controller endpoint. +17. While a request is in `received_offers` status (buyer has not yet accepted), the seller may **edit** their pending offer or **withdraw** it entirely from the request-detail step-2 card (`step-2-waiting-for-payment.tsx`). - The only supported HTTP way to withdraw an offer is: + - **Edit**: toggles `mode` to `'edit'` inside `Step2WaitingForPayment`, re-mounts `Step1SendProposal` pre-populated with the existing offer values. On save, calls `PATCH /api/marketplace/offers/:id` (via `updateOffer` action, which now correctly uses `PATCH` instead of the old `PUT`). + - **Withdraw**: opens a `ConfirmDialog`, then calls `withdrawOffer(offerId)` in `src/actions/marketplace.ts` which uses `PUT /api/marketplace/offers/:id/status` with `{ status: 'withdrawn' }`. - ``` - PUT /api/marketplace/offers/:id - Body: { status: 'withdrawn' } - ``` + `canManageOffer` is only `true` when `requestDetails?.status === 'received_offers'`; once the buyer accepts and the status advances, both buttons are hidden. - Note also that the frontend page `/dashboard/seller/marketplace/offers` (a "My Offers" listing) **does not exist**. Withdrawal must be triggered from the individual request detail page. + The DB filter `{ status: 'pending' }` inside `SellerOfferService.withdrawOffer` means withdrawal is impossible once `accepted` or `rejected`. - The DB filter `{ status: 'pending' }` inside `withdrawOffer` means withdrawal is impossible once `accepted` or `rejected`. + > ⚠️ `POST /api/marketplace/offers/:id/withdraw` still does **not** exist as an HTTP route. Always use `PUT /api/marketplace/offers/:id/status` with `{ status: 'withdrawn' }`. -### Offer update — method mismatch +### Offer update — method mismatch resolved -> ⚠️ **Known mismatch**: The frontend sends `PUT /marketplace/offers/:id` to update an offer, but the backend route is registered as `PATCH /api/marketplace/offers/:id` (`marketplaceControllerRoutes.ts`). Depending on whether a proxy or middleware normalises the method, one of these may fail. Verify end-to-end and align to a single method. +> ✅ **Fixed (commit 240a668)**: The frontend `updateOffer` action now sends `PATCH /api/marketplace/offers/:id`, matching the backend. The `acceptOffer` action was also corrected from `PUT` to `PATCH`. ## Sequence diagram @@ -157,10 +155,10 @@ sequenceDiagram | `POST` | `/api/marketplace/purchase-requests/:id/offers` | Create offer | `purchaseRequestId` is a path param | | `GET` | `/api/marketplace/purchase-requests/:id/offers` | Buyer view of offers on a request | | | `GET` | `/api/marketplace/offers/:id` | Single offer details | | -| `PATCH` | `/api/marketplace/offers/:id` | Update price / ETA / notes (seller, while pending) | ⚠️ Frontend sends `PUT`; backend registers `PATCH` — method mismatch | +| `PATCH` | `/api/marketplace/offers/:id` | Update price / ETA / notes (seller, while pending) | Fixed: frontend now sends `PATCH` | | `POST` | `/api/marketplace/offers/:id/accept` | Manual acceptance (when not via webhook) | | -| ~~`GET /api/marketplace/offers/seller/:sellerId`~~ | — | ~~Seller's own offer history~~ | ⚠️ NOT IMPLEMENTED — `getOffersBySeller()` service method exists but has no HTTP route | -| ~~`POST /api/marketplace/offers/:id/withdraw`~~ | — | ~~Seller withdraws~~ | ⚠️ NOT IMPLEMENTED — use `PATCH /api/marketplace/offers/:id` with `{ status: 'withdrawn' }` instead | +| `GET` | `/api/marketplace/offers/seller/:sellerId` | All offers by this seller (used by Offer Management page) | Implemented via `getSellerOffers` frontend action (commit 240a668) | +| `PUT` | `/api/marketplace/offers/:id/status` | Status mutation — use `{ status: 'withdrawn' }` to withdraw | The only HTTP withdraw path; `POST /api/marketplace/offers/:id/withdraw` does **not** exist | ## Database writes @@ -211,6 +209,9 @@ sequenceDiagram - Backend: `backend/src/services/marketplace/marketplaceController.ts` - Backend: `backend/src/models/SellerOffer.ts` - Backend: `backend/src/services/payment/paymentCoordinator.ts` (payment-state cascade) -- Frontend: `frontend/src/sections/request/components/seller-steps/step-1-send-proposal.tsx` +- Frontend: `frontend/src/sections/request/components/seller-steps/step-1-send-proposal.tsx` — proposal form (also re-used for edit) +- Frontend: `frontend/src/sections/request/components/seller-steps/step-2-waiting-for-payment.tsx` — awaiting-buyer card with edit/withdraw actions - Frontend: `frontend/src/sections/request/components/buyer-steps/step-3-select-and-pay.tsx` -- Frontend: `frontend/src/app/dashboard/seller/marketplace/` +- Frontend: `frontend/src/app/dashboard/seller/marketplace/` — seller marketplace browse +- Frontend: `frontend/src/app/dashboard/seller/marketplace/offers/page.tsx` — Offer Management page (all offers, status filter, withdraw) +- Frontend: `frontend/src/actions/marketplace.ts` — `withdrawOffer`, `getSellerOffers` actions diff --git a/04 - Flows/Trezor Safekeeping Flow.md b/04 - Flows/Trezor Safekeeping Flow.md index 4354c51..e0f8d4d 100644 --- a/04 - Flows/Trezor Safekeeping Flow.md +++ b/04 - Flows/Trezor Safekeeping Flow.md @@ -112,6 +112,24 @@ TREZOR_SAFEKEEPING_REQUIRED=false Default is permissive so existing Request Network release/refund flows continue to work. Set it to `true` only after registering the operating admin's Trezor account (the frontend signing flow via `TrezorSignDialog` is already implemented). Any value other than the literal string `true` is treated as disabled. +## Break-Glass Mode (Emergency Bypass) + +When `TREZOR_SAFEKEEPING_REQUIRED=true` but the Trezor device is unavailable (lost, hardware fault, key-holder absent), an admin can activate **break-glass mode** to temporarily bypass the safekeeping requirement: + +| Endpoint | Action | +|---|---| +| `GET /api/admin/settings/break-glass` | Read current status (`active`, `expiresAt`, `activatedBy`) | +| `POST /api/admin/settings/break-glass` | Activate for **1 hour** — fires a Telegram alarm immediately | +| `DELETE /api/admin/settings/break-glass` | Cancel before expiry | + +**Properties:** +- State is in-memory only (resets on server restart — intentional). +- Activation fires a Telegram alert via `tgNotify` regardless of `TG_NOTIFY_BOT_TOKEN` set status. +- The exported `isBreakGlassActive()` helper is called by `assertTrezorSignatureForOperation` — when `true`, the signature check is skipped. +- Maximum duration: 1 hour. After expiry the guard is automatically re-enabled. + +**Source:** `backend/src/services/admin/breakGlassRoutes.ts` (commit `b21df25`). + ## Safety Rules - Never store Trezor seed words, private keys, or xprv/tprv values. diff --git a/05 - Design System/Colors.md b/05 - Design System/Colors.md index 74b8a76..b65e903 100644 --- a/05 - Design System/Colors.md +++ b/05 - Design System/Colors.md @@ -2,12 +2,26 @@ title: Colors tags: [design-system, colors, palette] created: 2026-05-23 +updated: 2026-05-30 --- # Colors The palette is built from semantic groups (`primary`, `secondary`, `info`, `success`, `warning`, `error`, plus a 9-step `grey` scale) and exposed via the MUI theme. **Never hard-code hex values in components.** +> [!info] Amaneh Design System v2.7.0 (commit 56fc84e) +> As of v2.7.0 the active palette is the **Amaneh warm-earth** preset. The color presets menu in the settings drawer has been simplified to a single Amaneh entry; the multi-swatch picker was removed. The canonical palette names are: +> - **Saffron** — `primary` (golden-amber) +> - **Pistachio** — `success` (soft green) +> - **Persian Blue** — `info` (deep indigo-blue) +> - **Honey** — `warning` (amber-gold) +> - **Pomegranate** — `error` (deep red) +> - **Cream paper** — `background.paper` +> - **Parchment** — `background.default` +> - **Warm Ink** — `text.primary` +> +> CSS custom properties under `--amn-*` are defined in `src/app/global.css` and mirror these tokens for non-MUI elements. + > [!warning] > Hardcoded colors break dark mode and any future preset switch. Use `sx={{ color: 'primary.main' }}` or `theme.palette.primary.main`. diff --git a/05 - Design System/Design System Overview.md b/05 - Design System/Design System Overview.md index 98e3c1c..1cb6fdd 100644 --- a/05 - Design System/Design System Overview.md +++ b/05 - Design System/Design System Overview.md @@ -2,10 +2,14 @@ title: Design System Overview tags: [design-system, ui, mui] created: 2026-05-23 +updated: 2026-05-30 --- # Design System Overview +> [!info] Current version: **Amaneh v2.7.0** (commit 56fc84e, 2026-05-29) +> Major full-app redesign. Key changes: warm-earth palette (Saffron / Pistachio / Persian Blue / Honey / Pomegranate), three-font stack (Source Serif 4 italic / IBM Plex Sans / IBM Plex Mono), SealMark SVG logo (saffron octagon + serif italic wordmark), CSS custom properties (`--amn-*`) in `global.css`, settings-drawer preset picker simplified to single Amaneh entry. + The frontend design system is built on **Material-UI v7** with project-specific tokens, an LTR + RTL-aware emotion cache, and a user-controllable settings drawer (mode, layout, color preset, font, direction). > [!info] diff --git a/05 - Design System/Settings & Theming.md b/05 - Design System/Settings & Theming.md index 0aff84f..14baad0 100644 --- a/05 - Design System/Settings & Theming.md +++ b/05 - Design System/Settings & Theming.md @@ -21,7 +21,7 @@ A drawer-based UI lets the end user toggle visual preferences. Settings persist | **Contrast** | `default` · `bold` | `default` | localStorage | | **Layout** | `vertical` · `mini` · `horizontal` | `vertical` | localStorage | | **Direction** | `ltr` · `rtl` | derived from locale | localStorage (overrides locale default) | -| **Color preset** | one of `default`, `purple`, `cyan`, `blue`, `orange`, `red` | `default` | localStorage | +| **Color preset** | `amaneh` (warm-earth) — multi-swatch picker removed in v2.7.0 | `amaneh` | localStorage | | **Font family** | `Public Sans Variable`, `DM Sans Variable`, `Inter Variable`, `Nunito Sans Variable` | `Public Sans Variable` | localStorage | | **Compact navigation** | boolean | `false` | localStorage | | **Border radius** | 0–24 | 8 | localStorage | diff --git a/05 - Design System/Theme Configuration.md b/05 - Design System/Theme Configuration.md index 232489e..60daa1f 100644 --- a/05 - Design System/Theme Configuration.md +++ b/05 - Design System/Theme Configuration.md @@ -2,10 +2,14 @@ title: Theme Configuration tags: [design-system, theme, mui] created: 2026-05-23 +updated: 2026-05-30 --- # Theme Configuration +> [!info] Amaneh v2.7.0 (commit 56fc84e) +> The active theme now uses the Amaneh warm-earth palette and the three-font stack (Source Serif 4 / IBM Plex Sans / IBM Plex Mono). MUI component overrides were updated for `Button`, `Card`, `Paper`, `AppBar`, `Chip`, and `Label`. The settings-drawer color-preset swatch picker was simplified to a single Amaneh entry. + The MUI theme is constructed in `frontend/src/theme/index.ts` and composed from option modules in `frontend/src/theme/options/`. The resulting theme is provided at the root layout, wrapped by an RTL-aware emotion cache. --- diff --git a/05 - Design System/Typography.md b/05 - Design System/Typography.md index 7ca0188..786b8ee 100644 --- a/05 - Design System/Typography.md +++ b/05 - Design System/Typography.md @@ -2,39 +2,45 @@ title: Typography tags: [design-system, typography, fonts] created: 2026-05-23 +updated: 2026-05-30 --- # Typography -The system uses **Public Sans Variable** as the primary face with **Barlow** as a secondary (display) face, plus locale-specific Persian/Arabic faces loaded when the active language requires them. +> [!info] Amaneh Design System v2.7.0 (commit 56fc84e) +> The font stack changed in v2.7.0 from Public Sans + Barlow to a **three-font purposeful stack**: +> - **Source Serif 4** — headings in italic; editorial, humanist character +> - **IBM Plex Sans** — body and UI text; technical clarity, RTL-compatible +> - **IBM Plex Mono** — amounts, wallet addresses, tx hashes; monospaced, tabular-nums built-in + +The system uses a three-font purposeful stack for the Amaneh design. Locale-specific Persian/Arabic faces are loaded when the active language requires them. --- ## 1. Font stack -Loaded via `@fontsource-variable` (variable fonts streamed at build) plus `@fontsource/barlow`. Confirm in `frontend/package.json`: +Loaded via `@fontsource-variable`. Current active fonts (`frontend/package.json`): ```jsonc -"@fontsource-variable/public-sans": "^5.2.5", // Primary -"@fontsource-variable/dm-sans": "^5.2.5", // Optional preset -"@fontsource-variable/inter": "^5.2.5", // Optional preset -"@fontsource-variable/nunito-sans": "^5.2.5", // Optional preset -"@fontsource/barlow": "^5.2.5", // Secondary (display) +"@fontsource-variable/source-serif-4": "...", // Headings (italic) +"@fontsource/ibm-plex-sans": "...", // UI / body +"@fontsource/ibm-plex-mono": "...", // Amounts, addresses, hashes ``` -Imported in `frontend/src/app/layout.tsx` (or a fonts module) so Next can fingerprint and preload them. +The settings drawer still lists alternative fonts (DM Sans, Inter, Nunito Sans, Public Sans) for user override. Default font-family stack in the theme: ```css -font-family: "Public Sans Variable", "Helvetica", "Arial", sans-serif; +/* Headings */ +font-family: "Source Serif 4 Variable", Georgia, serif; +/* UI / body */ +font-family: "IBM Plex Sans", "Helvetica", "Arial", sans-serif; +/* Monospaced (amounts / addresses) */ +font-family: "IBM Plex Mono", "Courier New", monospace; ``` -Display-only headings (banners, hero) may override with Barlow via the `sx` prop: - -```tsx -Welcome -``` +Use `sx={{ fontFamily: 'IBMPlexMono' }}` (theme alias) for any USDT amounts, contract addresses, or transaction hashes. --- diff --git a/07 - Development/Workflow - Full Codebase Audit and Remediation.md b/07 - Development/Workflow - Full Codebase Audit and Remediation.md new file mode 100644 index 0000000..3d0f37a --- /dev/null +++ b/07 - Development/Workflow - Full Codebase Audit and Remediation.md @@ -0,0 +1,168 @@ +--- +title: Workflow — Full Codebase Audit and Remediation +tags: [development, audit, security, performance, automation, workflow] +created: 2026-05-30 +status: living +--- + +# Workflow — Full Codebase Audit and Remediation + +A periodic, multi-agent health pass over the whole platform. Run it *from time to time* +to keep docs honest, surface security / functionality / performance issues, fix the +obvious ones automatically, and hand the judgement calls back to a human. + +It is implemented as a **Claude Code workflow** (deterministic orchestration of many +subagents) and lives at: + +``` +escrow/.claude/workflows/full-codebase-audit.js +``` + +Because it is a *named* workflow, it can be launched by name from any session rooted at +`escrow/`: + +``` +Workflow({ name: 'full-codebase-audit' }) +``` + +This document explains the flow, the design decisions baked into it, how to run it, and +includes the **full source** so it can be recreated from scratch if the file is ever lost. + +--- + +## 1. What it does (the flow) + +``` +Sync ─▶ Doc Sync ─▶ Audit ─▶ Verify ─▶ Strategy ─▶ Mitigate ─▶ Report +``` + +| # | Phase | Model | What happens | +|---|-------|-------|--------------| +| 1 | **Sync** | Sonnet | `git fetch` all 4 repos; `git pull --ff-only` only when the tree is clean. Never touches uncommitted work. | +| 2 | **Doc Sync** | Sonnet | One agent per repo updates docs to match recent code changes. **scanner** gets a heavy doc-generation mandate (it is the least mature project and has zero markdown docs). | +| 3 | **Audit** | Sonnet | Fan-out of `repo × dimension` agents (security / functionality / performance / supply-chain) producing structured findings. | +| 4 | **Verify** | Sonnet | Each finding is adversarially re-checked against the code to kill false positives. Pipelined with Audit — a finding verifies as soon as its slice is found. | +| 5 | **Strategy** | **Opus** | The lead-architect agent clusters findings into systemic themes and splits them into a **no-brainer** queue (safe to auto-fix) and a **decision** queue (needs human judgement). | +| 6 | **Mitigate** | Sonnet | Applies the no-brainers, grouped one agent per repo. **Working-tree only — no commit, no push** (see §2). | +| 7 | **Report** | Sonnet | Writes the audit report under `09 - Audits/`, creates `ISSUE-###` files for the decision queue + any skipped fix, and updates the audit index. | + +The workflow **returns** a `decisionQueue` to the calling assistant. Workflows run in the +background and cannot prompt interactively, so the assistant presents that queue to you +with `AskUserQuestion` — that is the "allow the user to decide about the non-critical / +non-trivial ones" step. + +--- + +## 2. Design decisions baked in + +These are the defaults; each is overridable via `args` (§4). + +- **Workers are Sonnet, design/decision is Opus.** Cheap, parallel grunt work (syncing, + doc-writing, finding, verifying, applying fixes, scribing) runs on `sonnet`. The two + jobs that need judgement — strategy/triage — run on `opus`. +- **Fixes are working-tree only.** The Mitigate phase applies changes but never + `git add/commit/push`. Rationale: the repos are frequently dirty and a parallel agent + (`moojttaba`) pushes to the same branches, so auto-committing risks collisions and + mixing unrelated work. You review the diff, then commit yourself. +- **Pull is fetch + ff-only, skip-if-dirty.** The Sync phase never stashes or merges over + uncommitted changes; on a dirty tree it just reports behind/ahead counts. +- **Conservative triage.** When in doubt, a finding goes to the *decision* queue, not the + *no-brainer* queue. Anything that changes business logic, data shape, or could break + callers is never auto-applied. +- **scanner is the doc priority.** It is the youngest service with no docs, so Doc Sync + spends its biggest effort generating architecture / API / flow / ops docs for it in the + nick-doc vault plus a `README.md` in the repo. + +--- + +## 3. How to run it + +From a Claude Code session whose working directory is `escrow/`: + +1. **Trigger** — ask Claude to run the `full-codebase-audit` workflow (the word + "workflow" opts into multi-agent orchestration), or it can be invoked directly: + `Workflow({ name: 'full-codebase-audit' })`. +2. **Watch** — `/workflows` shows the live phase tree. +3. **Decide** — when it finishes, Claude reads the returned `decisionQueue` and asks you + about each non-trivial item via `AskUserQuestion`. Approved items become a follow-up + change set; the rest stay as `ISSUE-###` files. +4. **Review & commit** — inspect `git diff` in each repo for the auto-applied no-brainers, + then commit them yourself. + +It is **expensive** (dozens of agents across 4 repos). Run it periodically, not on every +change. + +--- + +## 4. Overriding behaviour (`args`) + +Pass an `args` object to scope or change the run: + +```js +Workflow({ name: 'full-codebase-audit', args: { + repos: ['backend', 'scanner'], // subset; default = all 4 + fixMode: 'working-tree', // | 'commit' | 'commit-push' + pullMode: 'fetch-ff-skip-dirty', // | 'stash-pull' | 'hard' + dryRun: false, // true => audit + report only, zero fixes + date: '2026-05-30', // optional; agents otherwise read `date +%F` +}}) +``` + +- `dryRun: true` is the safest way to get a fresh audit + report without any file changes. +- `fixMode: 'commit'` / `'commit-push'` only if you accept the collision risk on shared + branches. + +--- + +## 5. Outputs + +- **Docs** — updated/created markdown across repos and the nick-doc vault (scanner-heavy). +- **Audit report** — `nick-doc/09 - Audits/Full Codebase Audit - .md`. +- **Issues** — `nick-doc/Issues/ISSUE-###-*.md` for every decision item and skipped fix, + in the existing issue frontmatter format. +- **Working-tree fixes** — uncommitted no-brainer remediations in each repo. +- **Return value** — `{ summary, systemicThemes, decisionQueue, mitigation, docSync, + report }`, consumed by the assistant to drive the `AskUserQuestion` step. + +--- + +## 6. Recreating the workflow from scratch + +If `escrow/.claude/workflows/full-codebase-audit.js` is ever lost, recreate it with the +source below (it is the complete, self-contained script). Save it to that path and it is +runnable again by name. + +```js +export const meta = { + name: 'full-codebase-audit', + description: 'Sync repos, refresh docs, audit (security/logic/perf), strategize, auto-fix no-brainers, queue the rest for the user', + whenToUse: 'Periodic full-system health pass across frontend/backend/nick-doc/scanner. Run from time to time.', + phases: [ + { title: 'Sync', detail: 'fetch + ff-only pull (skip if dirty) across all 4 repos' }, + { title: 'Doc Sync', detail: 'update docs from recent code changes; scanner gets heavy doc generation', model: 'sonnet' }, + { title: 'Audit', detail: 'security / functionality / performance / supply-chain findings per repo', model: 'sonnet' }, + { title: 'Verify', detail: 'adversarial verification of each finding', model: 'sonnet' }, + { title: 'Strategy', detail: 'design remediation + triage no-brainer vs needs-user-decision', model: 'opus' }, + { title: 'Mitigate', detail: 'apply no-brainer fixes to the working tree only', model: 'sonnet' }, + { title: 'Report', detail: 'write audit report, ISSUE files, audit index, export doc', model: 'sonnet' }, + ], +} + +// See escrow/.claude/workflows/full-codebase-audit.js for the full body. +// The body is reproduced verbatim there; this guide and that file must stay in sync. +``` + +> The authoritative, always-current source is the file itself +> (`escrow/.claude/workflows/full-codebase-audit.js`). Treat this section as the recovery +> pointer; if you change the workflow, update the file and bump this doc's notes. + +--- + +## 7. Maintenance notes + +- Keep `meta.phases` titles identical to the `phase('…')` calls — they are matched by + string to group progress. +- `Date.now()` / `Math.random()` are unavailable inside workflow scripts; the Report phase + reads the date via `date +%F` from a Bash call instead. +- The dedup key is `repo::file::title-prefix`; widen it if you see near-duplicate findings. +- If false positives creep in, raise the Verify bar (it already drops `confidence: 'low'`). diff --git a/08 - Operations/Monitoring.md b/08 - Operations/Monitoring.md index 69ecf36..5391ac2 100644 --- a/08 - Operations/Monitoring.md +++ b/08 - Operations/Monitoring.md @@ -11,24 +11,21 @@ What's instrumented today and what to watch. Today's stack is intentionally lean ## 1. Health endpoint -Path: `GET /health` (backend, port `5001`). +Two paths are registered (both are public, rate-limited, not auth-gated): -Defined in `backend/src/app.ts`: +- `GET /health` — simple ping used by Docker healthchecks. Returns `200 { success, message, timestamp, environment, version }`. Does **not** probe MongoDB or Redis. +- `GET /api/health` — deep health check added in commit `44579d6` (backend v2.6.49). Calls `runHealthChecks` from `backend/src/services/health/healthCheckService.ts`. Probes MongoDB and Redis, collects memory/uptime stats, and returns a structured report. Returns `503` when `report.status === 'down'`. -```ts -app.get("/health", (req, res) => { - res.json({ - success: true, - message: "Marketplace Backend API is running", - timestamp: new Date().toISOString(), - environment: config.nodeEnv, - version: packageJson.version, - }); -}); +`GET /api/health` response shape (from `healthCheckService`): +```json +{ + "status": "ok", + "version": "2.6.xx", + "timestamp": "...", + "checks": { "mongodb": "ok", "redis": "ok", "uptime": 3600, "memoryMB": 120 } +} ``` -Returns `200` with a JSON envelope as soon as Express is up. Does **not** currently probe MongoDB or Redis — they are checked via separate Docker healthchecks. If you want deep health, extend the endpoint to ping both data stores and return `503` on failure. - Public URL behind Nginx: `https://amn.gg/api/health`. --- diff --git a/08 - Operations/Scanner Operations.md b/08 - Operations/Scanner Operations.md new file mode 100644 index 0000000..9a845fc --- /dev/null +++ b/08 - Operations/Scanner Operations.md @@ -0,0 +1,220 @@ +--- +title: Scanner Operations +tags: [operations, scanner, deployment] +created: 2026-05-30 +--- + +# Scanner Operations + +Runbook for deploying, configuring, monitoring, and troubleshooting the AMN Pay Scanner microservice. + +--- + +## 1. Configuration reference + +All configuration via environment variables. See `.env.example` in the scanner repo. + +| Variable | Default | Required | Description | +|---|---|---|---| +| `PORT` | `8080` | no | HTTP listen port | +| `DB_PATH` | `./scanner.db` | no | SQLite database path | +| `CHAINS_JSON_PATH` | `./supported-chains.json` | no | Supported chains config | +| `TOKENS_JSON_PATH` | `./tokens.json` | no | Token registry | +| `SCANNER_API_KEY` | _(none)_ | **yes (prod)** | Bearer token for all non-health endpoints. Generate with `openssl rand -hex 32` | +| `POLL_INTERVAL_SEC` | `15` | no | Chain poll interval in seconds | +| `INTENT_TTL_HOURS` | `24` | no | Pending/confirming intents older than this are expired (0 = disabled) | +| `WEBHOOK_RETRY_HOURS` | `6` | no | Interval between automatic webhook_failed re-delivery passes (0 = disabled) | +| `TRONGRID_API_KEY` | _(none)_ | recommended | TronGrid API key; without it rate limits are very low | +| `TONCENTER_API_KEY` | _(none)_ | recommended | TonCenter API key | +| `RPC_BSC` | _(chain config)_ | no | Override BSC RPC URL (chain 56) | +| `RPC_ARB` | _(chain config)_ | no | Override Arbitrum RPC URL (chain 42161) | +| `RPC_ETH` | _(chain config)_ | no | Override Ethereum RPC URL (chain 1) | +| `RPC_POLYGON` | _(chain config)_ | no | Override Polygon RPC URL (chain 137) | +| `RPC_BASE` | _(chain config)_ | no | Override Base RPC URL (chain 8453) | + +> [!warning] +> If `SCANNER_API_KEY` is not set, the scanner logs a warning and accepts all requests. Never run this way in production. + +--- + +## 2. Docker deployment + +The scanner ships as a single Docker image. The Dockerfile uses a two-stage build (Go 1.25 builder → Alpine 3.21 runtime). + +### Quick start (dev) + +```bash +cd scanner/ +cp .env.example .env +# edit .env — set SCANNER_API_KEY, RPC overrides, etc. + +docker build -t amn-scanner:dev . +docker run -d \ + --name amn-scanner \ + -p 8080:8080 \ + -v $(pwd)/data:/data \ + --env-file .env \ + amn-scanner:dev +``` + +### Production (via arcane-cli / Watchtower) + +The scanner is deployed manually via `arcane-cli` (not gitops). Watchtower does NOT manage it automatically. After pushing a new image, redeploy with: + +```bash +arcane-cli project redeploy --json +``` + +The SQLite database is stored on a named Docker volume (`/data`). Do not recreate the volume between deploys — it holds the checkpoint and intent state. + +--- + +## 3. Health check + +```bash +curl http://localhost:8080/health +# {"status":"ok","time":"2026-05-30T12:00:00Z"} +``` + +Docker `HEALTHCHECK` is already configured in the Dockerfile (30 s interval, 5 s timeout, 3 retries). + +--- + +## 4. Monitoring + +### Scanner status endpoint + +```bash +curl -H "Authorization: Bearer $SCANNER_API_KEY" \ + http://localhost:8080/scanner/status | jq . +``` + +Check: +- `lag` — should be near 0 for healthy chains (blocks behind for EVM, seconds for TON) +- `pendingIntents` — number of unresolved intents per chain +- `lastScannedBlock` — should advance each poll + +### Logs + +The scanner uses Go's `log/slog` structured logger with level prefixes. Key log patterns: + +| Pattern | Meaning | +|---|---| +| `[scanner] worker started` | Worker goroutine began for this chain | +| `[evm] intent confirming` | EVM tx seen, waiting for confirmations | +| `[evm] intent confirmed` | EVM: N confirmations reached | +| `[tron] MATCH` / `[ton] MATCH` | Transfer matched, going to confirmed | +| `[webhook] delivered` | Webhook POST succeeded | +| `[webhook] non-2xx response` | Backend returned error (will retry) | +| `[webhook] all retries exhausted` | Intent moved to webhook_failed | +| `[scanner] reconciling confirmed intents` | Startup crash recovery in progress | +| `[evm] scanner lag` | Chain lag > 100 blocks (investigate RPC) | + +--- + +## 5. Adding / modifying chains + +Edit `supported-chains.json`. Fields: + +| Field | Notes | +|---|---| +| `chainId` | Numeric EIP-155 chain ID (arbitrary int for Tron/TON) | +| `chainType` | `"evm"` (default) / `"tron"` / `"ton"` | +| `rpcUrl` | Primary RPC endpoint | +| `publicRpcUrl` | Fallback RPC (EVM only) | +| `proxyAddress` | ERC20FeeProxy address (EVM); USDT contract (Tron); USDT Jetton master (TON) | +| `confirmationThreshold` | Blocks required (EVM); ignored for Tron/TON | +| `verified` | `true` to activate the worker; `false` to disable without deleting | + +> [!important] +> Changing `proxyAddress` for an EVM chain only affects new scans. Existing pending intents will still be matched against the old address until they expire or are confirmed. + +After editing, restart the scanner container to pick up the new config. + +--- + +## 6. Adding tokens to the registry + +Edit `tokens.json`. Each entry: + +```json +{ "chainId": 56, "address": "0x...", "symbol": "USDC", "decimals": 18, "name": "USD Coin" } +``` + +Token registry is used only for populating `tokenSymbol` and `decimals` in the `checkoutBlock` response. Omitting a token does not break scanning — it just leaves those fields empty. + +--- + +## 7. Manual webhook retry + +Force immediate re-delivery of all `webhook_failed` intents: + +```bash +curl -X POST -H "Authorization: Bearer $SCANNER_API_KEY" \ + http://localhost:8080/admin/webhooks/retry +# {"queued": N} +``` + +--- + +## 8. Database inspection + +The SQLite database (`/data/scanner.db`) can be inspected with the `sqlite3` CLI inside the container: + +```bash +docker exec -it amn-scanner sqlite3 /data/scanner.db + +# Check stuck intents +SELECT intent_id, chain_id, status, created_at, webhook_delivered_at +FROM intents +WHERE status NOT IN ('confirmed', 'expired') +ORDER BY created_at DESC; + +# Check chain checkpoints +SELECT chain_id, last_scanned_block, updated_at FROM checkpoints; + +# Count by status +SELECT status, count(*) FROM intents GROUP BY status; +``` + +--- + +## 9. Troubleshooting + +### Intent stuck in `pending` + +1. Check `/scanner/status` — is the chain worker running and advancing (`lag` > 0 for a long time = RPC issue)? +2. Check that `chainId` and `tokenAddress` match exactly what is in `supported-chains.json` and `tokens.json`. +3. For EVM: verify the `proxyAddress` matches the contract the buyer is calling. +4. For Tron: confirm the destination address is stored in EVM-hex (0x) format in the DB. +5. Check scanner logs for `REJECT` messages around the expected tx time. + +### Webhook never received by backend + +1. Check `webhook_delivered_at` in the DB — if not null, the scanner delivered successfully and the backend side is the issue. +2. If null and status is `webhook_failed`: check backend logs for the incoming POST; verify `X-AMN-Signature` validation code. +3. If status is `confirmed` but `webhook_delivered_at` is null: startup reconciliation may re-deliver on next restart. +4. Use `POST /admin/webhooks/retry` to trigger immediate retry. + +### High lag on EVM chain + +1. Check RPC endpoint availability and rate limits. +2. Consider setting a `RPC_*` env override to a premium RPC (Alchemy, Infura, QuickNode). +3. The scanner falls back to `publicRpcUrl` if the primary fails but public nodes have lower limits. + +### Intent confirmed but amount looks wrong + +The scanner accepts any amount **>=** `intent.Amount`. Overpayments are not flagged. Underpayments result in the intent staying pending until TTL expiry. + +--- + +## 10. CI/CD notes + +- Woodpecker CI pipeline is in `.woodpecker/`. +- Telegram notify steps were removed (no TG secrets configured). +- Deploy step was removed — the scanner is deployed manually via `arcane-cli`. +- The CI pipeline builds and pushes the Docker image to the Gitea registry. +- Image tag format: `dev-` (from the `VERSION` file). + +> [!tip] +> After CI completes, verify the image is in the registry before redeploying. Silent CI failures can leave a stale image tagged. Check the registry tag timestamp, not just the CI green light. diff --git a/08 - Operations/Secret Rotation Runbook - 2026-05-30.md b/08 - Operations/Secret Rotation Runbook - 2026-05-30.md new file mode 100644 index 0000000..9485a38 --- /dev/null +++ b/08 - Operations/Secret Rotation Runbook - 2026-05-30.md @@ -0,0 +1,105 @@ +--- +title: Secret Rotation Runbook — 2026-05-30 +tags: [operations, security, secrets, incident] +created: 2026-05-30 +status: action-required +source: Full Codebase Audit - 2026-05-30 +--- + +# Secret Rotation Runbook — 2026-05-30 + +The 2026-05-30 full codebase audit found live credentials committed to the repos and, in +some cases, baked into container images. The audit's no-brainer fixes **replaced the +committed values with placeholders in the working tree**, but the *real* credentials are +still valid and must be **rotated by a human** — replacing a string in git does not +invalidate a leaked key. + +> Treat every credential below as **compromised**. Anyone with repo (or image) access has +> had these values. Rotate first, then scrub history. + +Related issues: ISSUE-074, ISSUE-075, ISSUE-079, ISSUE-115 and decisions DEC-49, DEC-50, +DEC-56, DEC-74, DEC-75, DEC-78. + +--- + +## Order of operations (per credential) + +1. **Rotate** — generate a new value at the provider. +2. **Inject at runtime** — put the new value in the deployment secret store (Arcane env / + compose secrets), **never** back into a committed file. +3. **Deploy** — roll the new value out and confirm the service is healthy. +4. **Revoke** — invalidate the old value at the provider. +5. **Scrub** — remove the secret from git history (see "History scrub" at the bottom). + +Do these one credential at a time and verify the dependent service after each. + +--- + +## Credentials to rotate + +| # | Credential | Where it leaked | Blast radius | How to rotate | +|---|-----------|-----------------|--------------|---------------| +| 1 | **Telegram bot token** | `backend/.env.development`, `backend/.env.example`, `frontend/.gitleaks.toml` | Full control of the bot: read/send messages, hijack the login widget, phish users | BotFather → `/revoke` → new token. Update `TELEGRAM_BOT_TOKEN`. | +| 2 | **Resend SMTP / API key** | `backend/.env.development`, `backend/.env.example` | Send email as the platform (phishing, OTP spoofing), read sending logs | Resend dashboard → API Keys → delete + create. Update `RESEND_API_KEY` / SMTP creds. | +| 3 | **JWT signing secret** | `backend/.env.example` | Forge **any** user/admin session token — critical | Generate 32+ random bytes (`openssl rand -hex 32`). Update `JWT_SECRET`. **Rotating invalidates all sessions** (users re-login). Consider also adding a separate `REFRESH_TOKEN_SECRET` (see DEC-26). | +| 4 | **Admin bootstrap password** | `backend/.env.example`, was also a hardcoded fallback in `init-admin.ts` (removed by NB-20) | Direct admin login | Set a strong `ADMIN_PASSWORD` secret; change the admin account password in-app; confirm `init-admin` no longer has a fallback. | +| 5 | **Request Network API key** | `backend/.env.example` | Act against the RN account; manipulate payment intents | RN dashboard → rotate key. Update `REQUEST_NETWORK_API_KEY`. | +| 6 | **Request Network webhook secret** | `backend/.env.example` | Forge RN webhooks → mark payments paid (this is the HMAC secret the backend verifies) | Rotate at RN; update `REQUEST_NETWORK_WEBHOOK_SECRET`. | +| 7 | **Telegram webhook secret token** | `backend/.env.example` | Forge Telegram webhook calls | Reset via `setWebhook` with a new `secret_token`; update the env var. | +| 8 | **Google OAuth client secret** | `backend/.env.example` | Impersonate the OAuth app | Google Cloud Console → Credentials → reset client secret. Update `GOOGLE_CLIENT_SECRET`. | +| 9 | **Alchemy API key(s)** | `frontend/Dockerfile` ARG defaults (removed by NB-10) | Quota theft / RPC abuse on your account | Alchemy dashboard → rotate app key. Supply via CI build-arg / runtime, not a default. | +| 10 | **TG_NOTIFY_BOT_TOKEN** (ops alert bot) | backend startup notification (committed env) | Spoof ops alerts; spam the ops channel | BotFather → revoke → new token. Update `TG_NOTIFY_BOT_TOKEN`. See [[telegram_notify_no_parse_mode]]. | +| 11 | **Frontend test account password** (`Moji6364`) | `frontend/scripts/show-credentials.sh` (DEC-75) | Login as that test user if it exists in any real env | Delete the script (or env-prompt it); rotate the account password if real. | + +### Public-by-design (lower priority, but make explicit) +- **WalletConnect project ID**, **Google OAuth *client ID*** — `frontend/Dockerfile` ARG + defaults (DEC-74). These are public values, but remove the baked defaults and pass them + via CI build-args so forks don't reuse the production IDs. + +--- + +## Stop re-leaking (pairs with rotation) + +These are the structural fixes (tracked as decisions) that stop the secrets coming back: + +- **DEC-50 / ISSUE-075** — `backend/.dockerignore` whitelists `.env.development` *into the + prod image*. Remove the `!.env.development` line so no env file is ever copied into an + image; inject secrets at runtime. +- **DEC-49 / ISSUE-101** — `backend/src/shared/config/index.ts` loads `.env.development` + unconditionally. Load `.env.` (or nothing in production) and never fall back to + the dev file. +- **DEC-56 / ISSUE-074** — untrack `backend/.env.development` entirely (`git rm --cached`) + and add it to `.gitignore`. +- **DEC-78 / ISSUE-079** — `frontend/.gitleaks.toml` allowlists the bot token *by value*. + Switch to a path/fingerprint-based allowlist after scrubbing, so gitleaks stops + "approving" the secret. See the `handle-gitleaks` skill. + +Runtime injection point for this stack: the **Arcane** env / project config (see +[[arcane_dev_stack]], [[arcane_cli_usage]]) for dev, and the production secret store for +prod. After changing any backend secret, remember the dev redeploy caveat: +restart `nickDev-nginx` (see [[devEscrow_nginx_after_redeploy]]). + +--- + +## History scrub (after rotation + revocation) + +Only after the old values are revoked, purge them from history so they can't be mined from +old commits: + +1. Use `git filter-repo` (preferred) or BFG to remove the affected files/blobs from each + repo's history: `backend/.env.development`, the historical `backend/.env.example`, + `frontend/.gitleaks.toml` values, `frontend/scripts/show-credentials.sh`. +2. Force-push the rewritten history and have all collaborators re-clone. **Coordinate** — + per [[parallel_agents_on_escrow]] another agent pushes to these branches; a history + rewrite mid-flight will conflict badly. Pick a quiet window. +3. Re-run gitleaks to confirm the working tree and history are clean. + +--- + +## Verification checklist + +- [ ] Each credential rotated at the provider and old value **revoked**. +- [ ] New values present only in the runtime secret store (no committed file holds a real value). +- [ ] Backend boots; `/api/health` green; login, email send, Telegram login, and an RN webhook all succeed with new secrets. +- [ ] `.env.development` untracked; `.dockerignore` no longer whitelists it; config no longer loads it in prod. +- [ ] gitleaks passes on working tree; history scrubbed and force-pushed in a coordinated window. diff --git a/09 - Audits/Full Codebase Audit - 2026-05-30.md b/09 - Audits/Full Codebase Audit - 2026-05-30.md new file mode 100644 index 0000000..d00b8c1 --- /dev/null +++ b/09 - Audits/Full Codebase Audit - 2026-05-30.md @@ -0,0 +1,268 @@ +--- +title: Full Codebase Audit — 2026-05-30 +tags: [audit, index, security, logic, performance] +created: 2026-05-30 +status: open +--- + +# Full Codebase Audit — 2026-05-30 + +Full-system audit across all three repos (frontend, backend, scanner) triggered as a periodic health pass. 134 findings across security, logic, performance, and supply-chain dimensions. 49 no-brainers were applied automatically; 1 was skipped (requires new persistence layer); 80 decision items were queued for human review. + +--- + +## Findings Summary by Severity and Dimension + +| Severity | Security | Logic | Performance | Supply-Chain | Total | +|----------|----------|-------|-------------|--------------|-------| +| Critical | 3 | 2 | 0 | 1 | 6 | +| High | 18 | 12 | 8 | 4 | 42 | +| Medium | 14 | 12 | 8 | 6 | 40 | +| Low | 10 | 6 | 10 | 20 | 46 | +| **Total** | **45** | **32** | **26** | **31** | **134** | + +### By Repo + +| Repo | Findings | No-Brainers Applied | Skipped | Decision Items | +|------|----------|---------------------|---------|----------------| +| frontend | 49 | 18 (NB-1 – NB-17, NB-49) | 0 | 31 (DEC-1–21, DEC-74–80) | +| backend | 55 | 21 (NB-18 – NB-38) | 1 (NB-27) | 33 (DEC-22–56) | +| scanner | 30 | 10 (NB-39 – NB-48) | 0 | 20 (DEC-57–73) | + +--- + +## Systemic Themes + +Eight root-cause patterns cut across most findings. Addressing these themes eliminates whole clusters at once. + +### 1. Missing Authorization on Payment and Admin Endpoints (Broken Access Control) +Routes are gated only by `authenticateToken`/`AuthGuard` with no role or ownership check. Payment status writes, exports, stats, user-payment listings, file deletion, delivery updates, offer selection, dispute evidence, and the entire admin UI tree all trust authentication alone. **Root fix:** a shared `requireAdmin` middleware + ownership-check helper + centralized status-transition validator applied consistently. + +### 2. Payment Status State Machine Is Inconsistent and Corruptible +Non-enum statuses (`'released'`, `'funded'`) are written and silently dropped, the provider enum omits `'shkeeper'`, transition guards check fields never set (`escrowState:'funded'`), the transition map omits `'in_negotiation'`, and amount-mismatch is checked after side-effects commit. **Root cause:** schema enums and state machine drifted from the code that writes them. + +### 3. Secrets Committed to the Repo and Baked into Images +Telegram bot token, Resend SMTP key, Google secret, JWT secret, admin password, Alchemy keys, and RN secrets appear across `.env.example`, `.env.development`, `.gitleaks.toml`, Dockerfiles, and committed scripts — and `.dockerignore` whitelists `.env.development` into prod images. **Root fix:** placeholder all committed files, remove env files from images, inject at runtime, rotate every exposed credential. + +### 4. Test/Debug Bypasses Reachable in Production +Test-payment mode, `force-verify-user`, RN test-webhook signature bypass, the debug panel, and console-suppression hacks all rely on weak runtime `NODE_ENV` checks (or none). **Root fix:** gate on `NODE_ENV` at registration/build time; never honour bypass flags in production. + +### 5. N+1 Queries, Unbounded Fan-Out, and Chatty Polling +Per-row DB lookups in `getPurchaseRequestsByBuyer` and `getReferrals`, unbounded notification/seller fan-out, redundant polling alongside sockets, full-collection loads, and per-intent HTTP fan-out in the scanner. **Root fix:** batch with `$in`/aggregation, bound concurrency, replace redundant polling with socket-driven or visibility-gated updates. + +### 6. Float Math and Weak Randomness in Money/Crypto Paths +USDT wei conversion via IEEE-754 floats risks under-payment; verification codes use `Math.random` instead of a CSPRNG. **Root fix:** use `parseUnits` for token amounts and `crypto.randomInt` for codes (both already available in the codebase). + +### 7. Unhardened Outbound HTTP and Webhook Handling (SSRF / OOM / Retry Leaks) +Scanner accepts arbitrary `callbackUrl` (SSRF), follows third-party `next`-URLs unvalidated, reads RPC/API bodies without size limits (OOM), overrides confirmation thresholds, and spawns unbounded sleeping retry goroutines. **Root fix:** URL allowlisting + private-range blocking at dial time, `io.LimitReader` caps, threshold floors, bounded persisted retry queues. + +### 8. CI/CD Supply-Chain Hygiene Gaps +Floating/unpinned images, missing lint/type/test/audit gates on production and manual pipelines, privileged buildx, dual lockfiles, no `engines` pin, and untested manual builds. **Root fix:** digest-pin all CI images, enforce quality gate on every pipeline, unify lockfiles, add audit/vuln scanning. + +--- + +## No-Brainers Applied (49 fixes) + +All 49 no-brainers were applied. NB-13/NB-14: lockfile not regenerated (no `yarn install` run per instructions — leave uncommitted for human review). NB-7: requires backend to expose `GET /chat/unread-count` returning `{ data: { count: number } }`. NB-29: depends on DEC-32 outcome; applied `status:'completed'` as interim until enum decision is made. + +| ID | Repo | Title | Files | +|----|------|-------|-------| +| NB-1 | frontend | USDT amount-to-wei uses floating-point arithmetic | `src/web3/context/action.ts` | +| NB-2 | frontend | Email verification logs full form data including password | `src/auth/view/jwt/jwt-verify-view.tsx` | +| NB-3 | frontend | Hardcoded Telegram bot ID fallback in widget loader | `src/auth/utils/telegram-login-widget.ts` | +| NB-4 | frontend | releasePayment returns fake success with hardcoded tx hash | `src/actions/payment.ts` | +| NB-5 | frontend | signUp/verifyEmailWithCode bypass StorageUtils.safeSet | `src/auth/context/jwt/action.ts` | +| NB-6 | frontend | Redundant 30s polling on buyer request details page | `src/sections/request/view/buyer/buyer-request-details-view.tsx` | +| NB-7 | frontend | getUnreadCount fetches entire conversation list | `src/actions/chat.ts`, `src/lib/axios.ts` | +| NB-8 | frontend | Debug new Error().stack capture on every step-change | `src/sections/request/view/buyer/buyer-request-details-view.tsx` | +| NB-9 | frontend | transformMessage logs two info calls per message | `src/actions/chat.ts` | +| NB-10 | frontend | Alchemy API keys hardcoded as Dockerfile ARG defaults | `Dockerfile` | +| NB-11 | frontend | Escrow wallet address hardcoded across multiple files | `src/web3/decentralizedPayment.ts`, `step-6-buyer-confirmed.tsx`, `manual-payout.tsx` | +| NB-12 | frontend | NEXT_PUBLIC_MAPBOX_API_KEY missing from Dockerfile ARGs/docs | `Dockerfile` | +| NB-13 | frontend | google-auth-library and @google-cloud/local-auth unused | `package.json` | +| NB-14 | frontend | @depay/widgets unused dependency | `package.json` | +| NB-15 | frontend | MockedUser (demo@minimals.cc) rendered in production nav | `src/layouts/components/nav-upgrade.tsx` | +| NB-16 | frontend | WEB3_PROVIDER_URL declared but never used | `src/global-config.ts` | +| NB-17 | frontend | google-oauth.ts.backup committed to source tree | `src/auth/services/google-oauth.ts.backup` | +| NB-18 | backend | Verification/reset codes logged to server console | `src/services/auth/authController.ts`, `src/services/delivery/DeliveryService.ts` | +| NB-19 | backend | Verification code uses Math.random() | `src/services/auth/authService.ts` | +| NB-20 | backend | Admin password hardcoded fallback in init-admin.ts | `src/infrastructure/database/init-admin.ts` | +| NB-21 | backend | force-verify-user route registered unconditionally | `src/services/auth/authRoutes.ts` | +| NB-22 | backend | getUserPayments queries non-existent 'userId' field | `src/services/payment/paymentService.ts` | +| NB-23 | backend | getPaymentStats sums object-typed amount field | `src/services/payment/paymentService.ts` | +| NB-24 | backend | GET /api/payment/export endpoints lack admin guard | `src/services/payment/paymentControllerRoutes.ts` | +| NB-25 | backend | getUserPayments route lacks ownership check (IDOR) | `src/services/payment/paymentControllerRoutes.ts` | +| NB-26 | backend | GET /api/files/stats missing admin guard | `src/services/file/fileRoutes.ts` | +| NB-28 | backend | updateDeliveryInfo does not enforce seller ownership | `src/services/marketplace/marketplaceController.ts` | +| NB-29 | backend | payout/confirm and release/confirm set non-enum 'released' status | `src/services/payment/requestNetwork/requestNetworkRoutes.ts` | +| NB-30 | backend | N+1 per-request Payment lookup in getPurchaseRequestsByBuyer | `src/services/marketplace/PurchaseRequestService.ts` | +| NB-31 | backend | Full unpaginated load in getPayments admin endpoint | `src/services/marketplace/marketplaceController.ts` | +| NB-32 | backend | 13 sequential countDocuments in getCollectionStats | `src/services/admin/dataCleanupService.ts` | +| NB-33 | backend | Real credentials committed in tracked .env.example | `.env.example` | +| NB-34 | backend | Dockerfile.dev runs --frozen-lockfile before copying yarn.lock | `Dockerfile.dev` | +| NB-35 | backend | Deprecated npm 'crypto' shim in production deps | `package.json` | +| NB-36 | backend | body-parser redundant with Express 5 | `package.json` | +| NB-37 | backend | manual.yml CI missing typecheck gate | `.woodpecker/manual.yml` | +| NB-38 | backend | No engines field / .nvmrc for Node version | `package.json`, `.nvmrc` | +| NB-39 | scanner | Scanner Dockerfile runs as root (no USER) | `Dockerfile` | +| NB-40 | scanner | cleanup.yml uses alpine:latest | `.woodpecker/cleanup.yml` | +| NB-41 | scanner | scanner buildx plugin not pinned | `.woodpecker/development.yml`, `.woodpecker/manual.yml`, `.woodpecker/production.yml` | +| NB-42 | scanner | Scanner RPC/API bodies read without size limit | `chain.go`, `tron_chain.go`, `ton_chain.go` | +| NB-43 | scanner | Scanner manual.yml has no test step | `.woodpecker/manual.yml` | +| NB-44 | scanner | No govulncheck/gosec in scanner CI | `.woodpecker/development.yml`, `.woodpecker/production.yml` | +| NB-45 | scanner | No RPC_TRON/RPC_TON override env vars | `config.go` | +| NB-46 | scanner | EVM scan lag warning uses reorgBuf-adjusted checkpoint | `chain.go` | +| NB-47 | scanner | handleScannerStatus loads full intent rows to count pending | `api.go`, `intent.go` | +| NB-48 | scanner | SQLite no connection pool limit set | `intent.go` | +| NB-49 | frontend | Admin route polling paused when tab hidden | `payments-awaiting-confirmation-list-view.tsx` | + +### Skipped No-Brainers + +| ID | Reason | Issue Filed | +|----|--------|-------------| +| NB-27 (DELETE /api/files/delete ownership check) | `fileService.deleteFile()` is a pure filesystem path operation with no DB ownership record — no `File` model, no `createdBy`/`owner` field stored anywhere. Adding an ownership check requires creating a new persistence layer, which is a larger-than-mechanical change. | [[ISSUE-055-delete-api-files-delete-has-no-ownership-check-requires-new-pe|ISSUE-055]] | + +--- + +## Decision Queue (80 items) + +These items require human judgment before implementation. Each has a corresponding issue file. + +### Critical + +| Issue | Title | Repo | Recommendation | +|-------|-------|------|----------------| +| [[ISSUE-056-backend-verifypayment-and-paymentcallback-routes-unauthenticat|ISSUE-056]] | verifyPayment and paymentCallback routes unauthenticated | backend | Auth + HMAC on callback; remove isWeb3Payment bypass | + +### High + +| Issue | Title | Repo | +|-------|-------|------| +| [[ISSUE-057-frontend-admin-ui-routes-lack-role-based-authorization-guard|ISSUE-057]] | Admin UI routes lack role-based authorization guard | frontend | +| [[ISSUE-058-frontend-test-payment-mode-enablable-in-production-via-env-var|ISSUE-058]] | Test payment mode enablable in production via NEXT_PUBLIC env var | frontend | +| [[ISSUE-059-frontend-auth-provider-clears-tokens-on-any-non-403-error|ISSUE-059]] | Auth provider clears tokens on any non-403 error including network failures | frontend | +| [[ISSUE-060-frontend-contacts-popover-reads-userid-from-non-existent-local|ISSUE-060]] | contacts-popover reads userId from non-existent localStorage 'user' key | frontend | +| [[ISSUE-061-frontend-socket-context-helpers-accumulate-listeners-without-d|ISSUE-061]] | Socket context helpers accumulate listeners without dedup | frontend | +| [[ISSUE-062-backend-payment-update-routes-lack-ownership-role-guards|ISSUE-062]] | Backend payment update routes lack ownership/role guards | backend | +| [[ISSUE-063-backend-legacy-marketplace-patch-payments-id-lets-any-user-set|ISSUE-063]] | Legacy marketplace PATCH /payments/:id lets buyer/seller set any status | backend | +| [[ISSUE-064-backend-request-network-allow-test-webhooks-bypasses-signature|ISSUE-064]] | REQUEST_NETWORK_ALLOW_TEST_WEBHOOKS bypasses signature verification | backend | +| [[ISSUE-065-backend-rn-webhook-advances-purchaserequest-to-non-existent-fu|ISSUE-065]] | RN webhook advances PurchaseRequest to non-existent 'funded' status | backend | +| [[ISSUE-066-backend-payout-and-release-confirm-set-non-enum-status|ISSUE-066]] | payout/confirm and release/confirm set non-enum status 'released' | backend | +| [[ISSUE-067-backend-amount-mismatch-check-runs-after-payment-saved-and-offe|ISSUE-067]] | amount-mismatch check runs after payment saved and offers accepted | backend | +| [[ISSUE-068-backend-datacleanuservice-deletes-payments-without-provider-sco|ISSUE-068]] | dataCleanupService deletes Payments without provider scoping | backend | +| [[ISSUE-069-backend-cleanupoldpendingpayments-deletes-pending-rn-payments-m|ISSUE-069]] | cleanupOldPendingPayments deletes pending RN payments mid-flow | backend | +| [[ISSUE-070-backend-notifyallsellersaboutnewrequest-unbounded-fan-out|ISSUE-070]] | notifyAllSellersAboutNewRequest unbounded fan-out | backend | +| [[ISSUE-071-backend-getreferrals-n-plus-1-purchaserequest-and-pointtransac|ISSUE-071]] | getReferrals N+1 (PurchaseRequest + PointTransaction per referral) | backend | +| [[ISSUE-072-backend-chat-messages-stored-as-embedded-array-unbounded-growth|ISSUE-072]] | Chat messages stored as embedded array (unbounded document growth) | backend | +| [[ISSUE-073-backend-payment-provider-enum-missing-shkeeper|ISSUE-073]] | Payment provider enum missing 'shkeeper' | backend | +| [[ISSUE-074-backend-env-development-committed-with-live-telegram-and-smtp-s|ISSUE-074]] | Backend Telegram bot token + SMTP key committed in .env.development | backend | +| [[ISSUE-075-backend-dockerignore-whitelists-env-development-into-prod-image|ISSUE-075]] | .dockerignore whitelists .env.development into prod image | backend | +| [[ISSUE-076-scanner-ssrf-via-unvalidated-callbackurl|ISSUE-076]] | Scanner: SSRF via unvalidated callbackUrl | scanner | +| [[ISSUE-077-scanner-caller-can-override-confirmation-threshold-down-to-1|ISSUE-077]] | Scanner: caller can override confirmation threshold down to 1 | scanner | +| [[ISSUE-078-scanner-idempotency-path-ignores-mismatched-parameters|ISSUE-078]] | Scanner: idempotency path ignores mismatched parameters | scanner | +| [[ISSUE-079-frontend-telegram-bot-token-committed-in-gitleaks-toml-allowli|ISSUE-079]] | Frontend: Telegram bot token committed in .gitleaks.toml allowlist | frontend | + +### Medium + +| Issue | Title | Repo | +|-------|-------|------| +| [[ISSUE-080-frontend-open-redirect-via-unvalidated-returnto-in-guestguard|ISSUE-080]] | Open redirect via unvalidated returnTo in GuestGuard | frontend | +| [[ISSUE-081-frontend-tokens-stored-in-localstorage-xss-accessible|ISSUE-081]] | Tokens stored in localStorage (XSS-accessible) | frontend | +| [[ISSUE-082-frontend-wallet-ownership-signature-verification-is-a-no-op|ISSUE-082]] | Wallet ownership signature verification is a no-op on frontend | frontend | +| [[ISSUE-083-frontend-no-content-security-policy-header-in-next-config|ISSUE-083]] | No Content-Security-Policy header in Next.js config | frontend | +| [[ISSUE-084-frontend-console-error-warn-suppression-masks-prod-errors|ISSUE-084]] | console.error/warn suppression masks prod errors | frontend | +| [[ISSUE-085-frontend-token-refresh-queue-dispatches-with-undefined-authori|ISSUE-085]] | Token refresh queue dispatches with undefined Authorization | frontend | +| [[ISSUE-086-frontend-paymentdetailsview-status-dropdown-exposed-to-all-use|ISSUE-086]] | PaymentDetailsView status dropdown exposed to all users | frontend | +| [[ISSUE-087-frontend-getpaymentstatus-and-checkpaymentstatus-hit-different|ISSUE-087]] | getPaymentStatus and checkPaymentStatus hit different endpoints | frontend | +| [[ISSUE-088-frontend-adminwalletpayout-falls-back-to-literal-admin-string|ISSUE-088]] | adminWalletPayout falls back to literal 'admin' adminUserId | frontend | +| [[ISSUE-089-frontend-admin-payments-awaiting-confirmation-polls-every-12s|ISSUE-089]] | Admin payments-awaiting-confirmation polls every 12s unconditionally | frontend | +| [[ISSUE-090-frontend-chat-views-re-fetch-full-conversation-on-every-new-me|ISSUE-090]] | Chat views re-fetch full conversation on every new-message event | frontend | +| [[ISSUE-091-frontend-dual-socket-connections-socketprovider-and-socketserv|ISSUE-091]] | Dual socket connections (SocketProvider + socketService singleton) | frontend | +| [[ISSUE-092-backend-jwt-refresh-and-access-tokens-share-same-secret|ISSUE-092]] | JWT refresh and access tokens share the same secret; middleware skips type check | backend | +| [[ISSUE-093-backend-addevidence-no-participant-ownership-check-on-disputes|ISSUE-093]] | addEvidence: no participant ownership check on disputes | backend | +| [[ISSUE-094-backend-selectoffer-does-not-verify-buyer-owns-purchase-request|ISSUE-094]] | selectOffer does not verify buyer owns the purchase request | backend | +| [[ISSUE-095-backend-getuserstats-no-ownership-admin-check-idor|ISSUE-095]] | getUserStats: no ownership/admin check (IDOR) | backend | +| [[ISSUE-096-backend-validatestatustransition-requires-escrowstate-funded-n|ISSUE-096]] | validateStatusTransition requires escrowState 'funded' never set on completed payments | backend | +| [[ISSUE-097-backend-validtransitions-map-missing-in-negotiation-key|ISSUE-097]] | validTransitions map missing 'in_negotiation' key | backend | +| [[ISSUE-098-backend-in-memory-seendeliveryids-resets-on-restart|ISSUE-098]] | validateStatusTransition: in-memory seenDeliveryIds resets on restart | backend | +| [[ISSUE-099-backend-on-demand-rn-reconciliation-in-getpaymentbyid-can-race|ISSUE-099]] | On-demand RN reconciliation in getPaymentById can race | backend | +| [[ISSUE-100-backend-updatepurchaserequest-does-findbyid-then-findbyidandupd|ISSUE-100]] | updatePurchaseRequest does findById then findByIdAndUpdate | backend | +| [[ISSUE-101-backend-config-loads-env-development-unconditionally|ISSUE-101]] | Backend config loads .env.development unconditionally | backend | +| [[ISSUE-102-backend-14-high-severity-npm-vulns-no-audit-step-in-ci|ISSUE-102]] | 14 high-severity npm vulns, no audit step in CI | backend | +| [[ISSUE-103-backend-react-react-dom-in-backend-production-dependencies|ISSUE-103]] | react/react-dom in backend production dependencies | backend | +| [[ISSUE-104-backend-bcrypt-native-addon-alongside-used-bcryptjs|ISSUE-104]] | bcrypt native addon present alongside used bcryptjs | backend | +| [[ISSUE-105-backend-no-startup-validation-of-required-env-vars|ISSUE-105]] | No startup validation of required env vars | backend | +| [[ISSUE-106-backend-dual-lockfiles-yarn-lock-and-package-lock-json-diverge|ISSUE-106]] | Dual lockfiles (yarn.lock + package-lock.json) diverge | backend | +| [[ISSUE-107-scanner-tronGrid-pagination-next-url-used-unvalidated|ISSUE-107]] | Scanner: TronGrid pagination next-URL used unvalidated | scanner | +| [[ISSUE-108-scanner-unauthenticated-startup-when-scanner-api-key-unset|ISSUE-108]] | Scanner: unauthenticated startup when SCANNER_API_KEY unset | scanner | +| [[ISSUE-109-scanner-tron-lag-metric-reported-in-ms-not-blocks|ISSUE-109]] | Scanner: Tron lag metric reported in ms, not blocks | scanner | +| [[ISSUE-110-scanner-ton-worker-on-http-fan-out-per-scan-cycle|ISSUE-110]] | Scanner: TON worker O(N) HTTP fan-out per scan cycle | scanner | +| [[ISSUE-111-scanner-deliverwebhook-goroutines-use-blocking-time-sleep|ISSUE-111]] | Scanner: deliverWebhook goroutines use blocking time.Sleep (leak risk) | scanner | +| [[ISSUE-112-scanner-unbounded-goroutine-fan-out-for-webhook-retries|ISSUE-112]] | Scanner: unbounded goroutine fan-out for webhook retries | scanner | +| [[ISSUE-113-scanner-rpc-response-bodies-read-without-size-limit-oom|ISSUE-113]] | Scanner/backend: RPC response bodies read without size limit (OOM) | scanner | +| [[ISSUE-114-frontend-walletconnect-google-client-ids-hardcoded-dockerfile|ISSUE-114]] | Frontend: WalletConnect/Google client IDs hardcoded as Dockerfile ARG defaults | frontend | +| [[ISSUE-115-frontend-real-plaintext-credentials-in-committed-scripts|ISSUE-115]] | Frontend: real plaintext credentials in committed scripts | frontend | +| [[ISSUE-116-frontend-backend-scanner-ci-images-not-pinned-to-digests|ISSUE-116]] | Frontend/scanner/backend: CI images not pinned to digests | frontend | +| [[ISSUE-117-frontend-backend-scanner-production-manual-ci-pipelines-lack-g|ISSUE-117]] | Frontend/scanner/backend: production/manual CI pipelines lack lint/type/test/audit gates | frontend | + +### Low + +| Issue | Title | Repo | +|-------|-------|------| +| [[ISSUE-118-frontend-notification-title-rendered-via-dangerouslysetinnerht|ISSUE-118]] | Notification title rendered via dangerouslySetInnerHTML | frontend | +| [[ISSUE-119-frontend-telegramdebugpanel-exposed-in-production-via-url-flag|ISSUE-119]] | TelegramDebugPanel exposed in production via URL/localStorage flag | frontend | +| [[ISSUE-120-frontend-50ms-setinterval-console-suppression-script-in-root-l|ISSUE-120]] | 50ms setInterval console-suppression script in root layout | frontend | +| [[ISSUE-121-frontend-transferfunds-and-createpayment-post-to-same-endpoint|ISSUE-121]] | transferFunds and createPayment POST to the same endpoint | frontend | +| [[ISSUE-122-backend-missing-compound-index-for-seller-visibility-purchase-r|ISSUE-122]] | Missing compound index for seller-visibility purchase-request query | backend | +| [[ISSUE-123-backend-notification-unread-count-chatty-db-access|ISSUE-123]] | Notification unread-count chatty DB access | backend | +| [[ISSUE-124-backend-per-seller-socket-emit-loop-in-updatepurchaserequeststatu|ISSUE-124]] | Per-seller socket emit loop in updatePurchaseRequestStatus | backend | +| [[ISSUE-125-backend-getcategorypath-unbounded-sequential-findbyid-loop|ISSUE-125]] | getCategoryPath unbounded sequential findById loop | backend | +| [[ISSUE-126-backend-getuserpoints-writes-full-user-document-on-read|ISSUE-126]] | getUserPoints writes full User document on read when fields missing | backend | +| [[ISSUE-127-scanner-get-intents-id-exposes-salt-and-callbackurl|ISSUE-127]] | Scanner: GET /intents/:id exposes salt and callbackUrl | scanner | +| [[ISSUE-128-scanner-post-intents-returns-200-instead-of-201|ISSUE-128]] | Scanner: POST /intents returns 200 instead of 201 | scanner | +| [[ISSUE-129-scanner-ton-processTransfer-doesnt-verify-jettonmasteraddress|ISSUE-129]] | Scanner: TON processTransfer doesn't verify JettonMasterAddress vs intent.TokenAddress | scanner | +| [[ISSUE-130-scanner-config-getchaingettokengetrpc-on-linear-scans|ISSUE-130]] | Scanner: Config.GetChain/GetToken/GetRPC O(N) linear scans | scanner | +| [[ISSUE-131-scanner-tron-ton-workers-dont-share-http-transport|ISSUE-131]] | Scanner: Tron/TON workers don't share HTTP transport | scanner | +| [[ISSUE-132-scanner-evm-checkpoint-saved-every-2000-block-chunk|ISSUE-132]] | Scanner: EVM checkpoint saved every 2000-block chunk | scanner | +| [[ISSUE-133-scanner-ci-buildx-steps-run-privileged-true|ISSUE-133]] | Scanner: CI buildx steps run privileged: true | scanner | +| [[ISSUE-134-frontend-sentry-source-map-upload-configured-but-no-auth-token|ISSUE-134]] | Frontend: Sentry source-map upload configured but no auth token injected | frontend | +| [[ISSUE-135-backend-uploads-directory-served-without-authentication|ISSUE-135]] | Backend uploads directory served without authentication | backend | + +--- + +## Documentation Gaps Identified (Doc Sync) + +The following gaps were identified but not filled during this audit pass. They should be tracked as separate doc tasks: + +- **Frontend:** Admin dashboard sub-pages (confirmation-thresholds, networks, payments-awaiting-confirmation, trezor) missing from Admin API doc. +- **Frontend:** Trezor registration and break-glass UI (commit c9ce345) not reflected in Trezor API or Trezor Safekeeping Flow docs. +- **Frontend:** Cloudflare Turnstile/CAPTCHA behavior (3 failed logins) not documented in Authentication Flow or Authentication API docs. +- **Frontend:** AMN Pay Scanner lag column and per-row probe button have no dedicated flow or operations doc. +- **Frontend:** Telegram startup notification (TG_NOTIFY_BOT_TOKEN) not in Operations/Environment Variables doc. +- **Frontend:** Amaneh UI variant toggle — state key and exact behavior not fully described in Settings & Theming. +- **Frontend:** `productLink` made truly optional; `deliveryType` required marker dropped — Purchase Request Flow wizard narrative needs update. +- **Backend:** Sweep signer strategy (PermitPullSweepSigner + GasTopUpSweepSigner) has no operations runbook. +- **Backend:** Native token sweep (BNB/ETH to derived destinations) not reflected in Payment API or sweep operations runbook. +- **Backend:** AML screening (OFAC SDN provider) has no dedicated flow doc covering when screening fires, seller opt-in, fee deduction. +- **Backend:** GET /api/health response field names not verified against live `healthCheckService` output. +- **Backend:** RequestTemplate budget currency restriction (USDT/USDC only) not reflected in Marketplace API or RequestTemplate model docs. +- **Backend:** Sweep integration tests (Anvil + INTEGRATION_TEST=1) not covered in Testing.md. +- **Backend:** Telegram startup notification (app startup `tgNotify`) not in Monitoring.md. +- **Backend:** AMN Pay Scanner adapter internals (amnPayAdapter, amnScannerWebhookRoutes) have no doc. +- **Backend:** New env vars (OFAC_SDN_URL, TURNSTILE_SECRET_KEY, TURNSTILE_SITE_KEY, AMN_SCANNER_URL, AMN_SCANNER_WEBHOOK_SECRET) may not be in Environment Variables doc. +- **Backend:** Seller Offer Flow does not reflect selectedOfferId persistence fix and atomic offer rejection on payment. +- **Backend:** ISSUE-021 (POST /api/marketplace/offers/:id/withdraw) should be marked resolved (implemented in commit 3e47713). +- **Scanner:** No doc for CI pipeline structure (.woodpecker/ steps, secrets, image push flow). +- **Scanner:** No doc for test suite (chain_validate_test.go / reference_test.go / tron_chain_test.go) and how to extend it. +- **Scanner:** Multi-chain reorg edge cases and exact ReorgBuffer formula not in troubleshooting doc. +- **Scanner:** TON scaling limitation (O(pending intents) API calls per cycle) noted but no mitigation/batching design documented. +- **Scanner:** RN proxy address discrepancy in supported-chains.json (ETH v0.1.0 vs v0.2.0) not documented. + +--- + +## References + +- [[Security Audit - 2026-05-24]] +- [[Logic Audit - 2026-05-24]] +- [[Performance Audit - 2026-05-24]] +- [[Doc vs Code Audit Report - 2026-05-29]] diff --git a/Issues/ISSUE-007-frontend-deleteaccount-action-calls-delete-user-profile-whic.md b/Issues/ISSUE-007-frontend-deleteaccount-action-calls-delete-user-profile-whic.md index 56e845a..8e6e630 100644 --- a/Issues/ISSUE-007-frontend-deleteaccount-action-calls-delete-user-profile-whic.md +++ b/Issues/ISSUE-007-frontend-deleteaccount-action-calls-delete-user-profile-whic.md @@ -3,6 +3,9 @@ issue: 007 title: "Frontend deleteAccount action calls DELETE /user/profile which has no backend route — account deletion is broken" severity: critical domain: Authentication +status: resolved +resolved: 2026-05-29 +fix: "Updated deleteAccount in account.ts to call DELETE /auth/account (endpoints.auth.deleteAccount) — the route that exists in authRoutes.ts. Added deleteAccount entry to endpoints.auth in axios.ts." labels: [bug, frontend, critical, broken-feature] status: open created: 2026-05-29 diff --git a/Issues/ISSUE-008-sendfilemessage-posts-to-wrong-endpoint-file-uploads-silentl.md b/Issues/ISSUE-008-sendfilemessage-posts-to-wrong-endpoint-file-uploads-silentl.md index b2d5d65..82cb058 100644 --- a/Issues/ISSUE-008-sendfilemessage-posts-to-wrong-endpoint-file-uploads-silentl.md +++ b/Issues/ISSUE-008-sendfilemessage-posts-to-wrong-endpoint-file-uploads-silentl.md @@ -3,6 +3,9 @@ issue: 008 title: "sendFileMessage posts to wrong endpoint — file uploads silently fail or corrupt text-message handler" severity: critical domain: Chat +status: resolved +resolved: 2026-05-29 +fix: "Added sendFileMessage: '/chat/:id/messages/file' to endpoints.chat in axios.ts. Updated sendFileMessage in chat.ts to use endpoints.chat.sendFileMessage instead of sendMessage." labels: [bug, frontend, critical, broken-feature] status: open created: 2026-05-29 diff --git a/Issues/ISSUE-009-archiveconversation-sends-put-but-backend-only-accepts-patch.md b/Issues/ISSUE-009-archiveconversation-sends-put-but-backend-only-accepts-patch.md index 47814cc..b47aae0 100644 --- a/Issues/ISSUE-009-archiveconversation-sends-put-but-backend-only-accepts-patch.md +++ b/Issues/ISSUE-009-archiveconversation-sends-put-but-backend-only-accepts-patch.md @@ -3,6 +3,9 @@ issue: 009 title: "archiveConversation sends PUT but backend only accepts PATCH — all archive attempts fail" severity: critical domain: Chat +status: resolved +resolved: 2026-05-29 +fix: "Changed archiveConversation in chat.ts from axiosInstance.put to axiosInstance.patch — matches backend PATCH /:id/archive route." labels: [bug, frontend, critical, broken-feature] status: open created: 2026-05-29 diff --git a/Issues/ISSUE-010-frontend-admin-updateuserstatus-and-updateuserrole-use-put-b.md b/Issues/ISSUE-010-frontend-admin-updateuserstatus-and-updateuserrole-use-put-b.md index 4681012..cae67e2 100644 --- a/Issues/ISSUE-010-frontend-admin-updateuserstatus-and-updateuserrole-use-put-b.md +++ b/Issues/ISSUE-010-frontend-admin-updateuserstatus-and-updateuserrole-use-put-b.md @@ -3,6 +3,9 @@ issue: 010 title: "Frontend admin updateUserStatus and updateUserRole use PUT but backend only accepts PATCH" severity: critical domain: User Management +status: resolved +resolved: 2026-05-29 +fix: "Changed updateUserStatus and updateUserRole in user.ts from axiosInstance.put to axiosInstance.patch — matches backend PATCH /admin/:userId/status and PATCH /admin/:userId/role routes." labels: [bug, frontend, critical, admin, broken-feature] status: open created: 2026-05-29 diff --git a/Issues/ISSUE-011-frontend-updateuserstatus-sends-inactive-pending-status-valu.md b/Issues/ISSUE-011-frontend-updateuserstatus-sends-inactive-pending-status-valu.md index 3d65084..daaa982 100644 --- a/Issues/ISSUE-011-frontend-updateuserstatus-sends-inactive-pending-status-valu.md +++ b/Issues/ISSUE-011-frontend-updateuserstatus-sends-inactive-pending-status-valu.md @@ -3,6 +3,9 @@ issue: 011 title: "Frontend updateUserStatus sends 'inactive'/'pending' status values that backend does not accept" severity: critical domain: User Management +status: resolved +resolved: 2026-05-29 +fix: "Updated updateUserStatus type signature in user.ts from 'active' | 'inactive' | 'pending' to 'active' | 'suspended' | 'deleted' — matching backend's ['active', 'suspended', 'deleted'] validation." labels: [bug, frontend, critical, admin, type-mismatch] status: open created: 2026-05-29 diff --git a/Issues/ISSUE-013-createproviderpaymentintent-always-routes-to-request-network.md b/Issues/ISSUE-013-createproviderpaymentintent-always-routes-to-request-network.md index b6a66e3..7e5e6a7 100644 --- a/Issues/ISSUE-013-createproviderpaymentintent-always-routes-to-request-network.md +++ b/Issues/ISSUE-013-createproviderpaymentintent-always-routes-to-request-network.md @@ -3,6 +3,9 @@ issue: 013 title: "createProviderPaymentIntent always routes to request-network/intents regardless of provider argument" severity: critical domain: Payment +status: resolved +resolved: 2026-05-29 +fix: "Replaced stub getProviderIntentEndpoint in payment.ts with a switch on provider: shkeeper → shkeeper.intents, decentralized → decentralized.save, default → requestNetwork.intents." labels: [bug, frontend, critical, payment, routing] status: open created: 2026-05-29 diff --git a/Issues/ISSUE-014-paymentprovider-typescript-type-excludes-shkeeper-and-decent.md b/Issues/ISSUE-014-paymentprovider-typescript-type-excludes-shkeeper-and-decent.md index 512b7f4..70d9253 100644 --- a/Issues/ISSUE-014-paymentprovider-typescript-type-excludes-shkeeper-and-decent.md +++ b/Issues/ISSUE-014-paymentprovider-typescript-type-excludes-shkeeper-and-decent.md @@ -3,6 +3,9 @@ issue: 014 title: "PaymentProvider TypeScript type excludes 'shkeeper' and 'decentralized' causing UI fallthrough for main payment providers" severity: critical domain: Payment +status: resolved +resolved: 2026-05-29 +fix: "Added 'shkeeper' and 'decentralized' to PaymentProvider union type in types/payment.ts." labels: [bug, frontend, critical, payment, type-mismatch] status: open created: 2026-05-29 diff --git a/Issues/ISSUE-015-simulated-transaction-sim-bypass-has-no-environment-guard-ca.md b/Issues/ISSUE-015-simulated-transaction-sim-bypass-has-no-environment-guard-ca.md index 2c529d0..c7a5f0c 100644 --- a/Issues/ISSUE-015-simulated-transaction-sim-bypass-has-no-environment-guard-ca.md +++ b/Issues/ISSUE-015-simulated-transaction-sim-bypass-has-no-environment-guard-ca.md @@ -3,6 +3,9 @@ issue: 015 title: "Simulated transaction SIM_ bypass has no environment guard — can fire in production on wallet connection failure" severity: critical domain: Payment +status: resolved +resolved: 2026-05-29 +fix: "Backend: wrapped SIM_ bypass in both paymentRoutes.ts and marketplace/routes.ts with process.env.NODE_ENV !== 'production' guard. Frontend: web3-provider.tsx and web3-payment.tsx now throw in production instead of silently returning a fake SIM_ hash." labels: [security, bug, critical, payment, frontend, bypass] status: open created: 2026-05-29 diff --git a/Issues/ISSUE-016-updatepurchaserequest-uses-put-but-backend-only-registers-pa.md b/Issues/ISSUE-016-updatepurchaserequest-uses-put-but-backend-only-registers-pa.md index bbc8037..e4bde67 100644 --- a/Issues/ISSUE-016-updatepurchaserequest-uses-put-but-backend-only-registers-pa.md +++ b/Issues/ISSUE-016-updatepurchaserequest-uses-put-but-backend-only-registers-pa.md @@ -4,6 +4,9 @@ title: "updatePurchaseRequest uses PUT but backend only registers PATCH — all severity: major domain: Purchase Request labels: [bug, frontend, major, broken-feature] +status: resolved +resolved: 2026-05-29 +fix: "Changed axiosInstance.put to axiosInstance.patch in updatePurchaseRequest in marketplace.ts — matches backend PATCH /purchase-requests/:id." status: open created: 2026-05-29 source: Doc vs Code Audit 2026-05-29 diff --git a/Issues/ISSUE-017-updateoffer-uses-put-marketplace-offers-id-but-backend-regis.md b/Issues/ISSUE-017-updateoffer-uses-put-marketplace-offers-id-but-backend-regis.md index 1f0a2e4..870f1fc 100644 --- a/Issues/ISSUE-017-updateoffer-uses-put-marketplace-offers-id-but-backend-regis.md +++ b/Issues/ISSUE-017-updateoffer-uses-put-marketplace-offers-id-but-backend-regis.md @@ -3,6 +3,9 @@ issue: 017 title: "updateOffer uses PUT /marketplace/offers/:id but backend registers PATCH /offers/:id — offer edits fail" severity: major domain: Seller Offer +status: resolved +resolved: 2026-05-29 +fix: "Changed axiosInstance.put to axiosInstance.patch in updateOffer in marketplace.ts — matches backend PATCH /offers/:id." labels: [bug, frontend, major, broken-feature] status: open created: 2026-05-29 diff --git a/Issues/ISSUE-018-select-offer-updatemany-has-no-status-filter-overwrites-with.md b/Issues/ISSUE-018-select-offer-updatemany-has-no-status-filter-overwrites-with.md index f6a1f63..feda895 100644 --- a/Issues/ISSUE-018-select-offer-updatemany-has-no-status-filter-overwrites-with.md +++ b/Issues/ISSUE-018-select-offer-updatemany-has-no-status-filter-overwrites-with.md @@ -4,6 +4,9 @@ title: "select-offer updateMany has no status filter — overwrites withdrawn/re severity: major domain: Seller Offer labels: [bug, backend, major, data-integrity] +status: resolved +resolved: 2026-05-29 +fix: "Added status: { $nin: ['withdrawn', 'rejected'] } to the updateMany filter in select-offer handler — preserves existing terminal statuses." status: open created: 2026-05-29 source: Doc vs Code Audit 2026-05-29 diff --git a/Issues/ISSUE-021-post-api-marketplace-offers-id-withdraw-http-route-does-not-.md b/Issues/ISSUE-021-post-api-marketplace-offers-id-withdraw-http-route-does-not-.md index ecaf438..3260599 100644 --- a/Issues/ISSUE-021-post-api-marketplace-offers-id-withdraw-http-route-does-not-.md +++ b/Issues/ISSUE-021-post-api-marketplace-offers-id-withdraw-http-route-does-not-.md @@ -4,6 +4,9 @@ title: "POST /api/marketplace/offers/:id/withdraw HTTP route does not exist — severity: major domain: Seller Offer labels: [missing-feature, backend, frontend, major] +status: resolved +resolved: 2026-05-29 +fix: "Added POST /offers/:id/withdraw route in marketplace/routes.ts. Calls sellerOfferService.withdrawOffer with ownership check (seller or admin only)." status: open created: 2026-05-29 source: Doc vs Code Audit 2026-05-29 diff --git a/Issues/ISSUE-022-get-api-payment-payments-id-debug-has-no-authentication-full.md b/Issues/ISSUE-022-get-api-payment-payments-id-debug-has-no-authentication-full.md index 09a75ef..35a49f6 100644 --- a/Issues/ISSUE-022-get-api-payment-payments-id-debug-has-no-authentication-full.md +++ b/Issues/ISSUE-022-get-api-payment-payments-id-debug-has-no-authentication-full.md @@ -4,7 +4,9 @@ title: "GET /api/payment/payments/:id/debug has no authentication — full payme severity: major domain: Payment labels: [security, bug, backend, major, missing-auth] -status: open +status: resolved +resolved: 2026-05-29 +fix: "Already guarded by authenticateToken + authorizeRoles('admin') at paymentRoutes.ts line 285 (applied as part of ISSUE-005 fix). Confirmed in current code." created: 2026-05-29 source: Doc vs Code Audit 2026-05-29 --- diff --git a/Issues/ISSUE-023-get-api-payment-export-has-no-admin-role-guard-at-route-leve.md b/Issues/ISSUE-023-get-api-payment-export-has-no-admin-role-guard-at-route-leve.md index f6bcccf..9c6add8 100644 --- a/Issues/ISSUE-023-get-api-payment-export-has-no-admin-role-guard-at-route-leve.md +++ b/Issues/ISSUE-023-get-api-payment-export-has-no-admin-role-guard-at-route-leve.md @@ -4,6 +4,9 @@ title: "GET /api/payment/export has no admin role guard at route level — any a severity: major domain: Payment labels: [security, bug, backend, major, privilege-escalation] +status: resolved +resolved: 2026-05-29 +fix: "Already guarded by authenticateToken + authorizeRoles('admin') at paymentRoutes.ts line 79. Confirmed in current code." status: open created: 2026-05-29 source: Doc vs Code Audit 2026-05-29 diff --git a/Issues/ISSUE-024-get-api-payment-stats-has-no-admin-role-guard-any-authentica.md b/Issues/ISSUE-024-get-api-payment-stats-has-no-admin-role-guard-any-authentica.md index 641b951..0767a6d 100644 --- a/Issues/ISSUE-024-get-api-payment-stats-has-no-admin-role-guard-any-authentica.md +++ b/Issues/ISSUE-024-get-api-payment-stats-has-no-admin-role-guard-any-authentica.md @@ -3,6 +3,9 @@ issue: 024 title: "GET /api/payment/stats has no admin role guard — any authenticated user can read aggregate payment stats" severity: major domain: Payment +status: resolved +resolved: 2026-05-29 +fix: "Already guarded by authenticateToken + authorizeRoles('admin') at paymentRoutes.ts line 56. Confirmed in current code." labels: [security, bug, backend, major, privilege-escalation] status: open created: 2026-05-29 diff --git a/Issues/ISSUE-025-get-api-disputes-statistics-has-no-admin-role-guard-any-auth.md b/Issues/ISSUE-025-get-api-disputes-statistics-has-no-admin-role-guard-any-auth.md index 0983c50..66e4361 100644 --- a/Issues/ISSUE-025-get-api-disputes-statistics-has-no-admin-role-guard-any-auth.md +++ b/Issues/ISSUE-025-get-api-disputes-statistics-has-no-admin-role-guard-any-auth.md @@ -3,6 +3,9 @@ issue: 025 title: "GET /api/disputes/statistics has no admin role guard — any authenticated user can access aggregate dispute KPIs" severity: major domain: Dispute +status: resolved +resolved: 2026-05-29 +fix: "Added authorizeRoles('admin', 'resolver') to GET /statistics in disputeRoutes.ts." labels: [security, bug, backend, major, privilege-escalation] status: open created: 2026-05-29 diff --git a/Issues/ISSUE-026-get-notifications-id-only-returns-user-s-most-recent-notific.md b/Issues/ISSUE-026-get-notifications-id-only-returns-user-s-most-recent-notific.md index d2dad6e..bf0ee9f 100644 --- a/Issues/ISSUE-026-get-notifications-id-only-returns-user-s-most-recent-notific.md +++ b/Issues/ISSUE-026-get-notifications-id-only-returns-user-s-most-recent-notific.md @@ -3,6 +3,9 @@ issue: 026 title: "GET /notifications/:id only returns user's most-recent notification — all others return 404 erroneously" severity: major domain: Notification +status: resolved +resolved: 2026-05-29 +fix: "Fixed getNotificationById in notificationController.ts to use Notification.findOne({ _id: id, userId }) instead of fetching page 1 of user notifications." labels: [bug, backend, major, broken-feature] status: open created: 2026-05-29 diff --git a/Issues/ISSUE-027-confirm-delivery-endpoint-has-no-ownership-check-any-authent.md b/Issues/ISSUE-027-confirm-delivery-endpoint-has-no-ownership-check-any-authent.md index c7a3980..d0d84a1 100644 --- a/Issues/ISSUE-027-confirm-delivery-endpoint-has-no-ownership-check-any-authent.md +++ b/Issues/ISSUE-027-confirm-delivery-endpoint-has-no-ownership-check-any-authent.md @@ -4,6 +4,9 @@ title: "confirm-delivery endpoint has no ownership check — any authenticated u severity: major domain: Delivery labels: [security, bug, backend, major, authorization] +status: resolved +resolved: 2026-05-29 +fix: "Added buyer ownership check in marketplaceController.confirmDelivery — rejects with 403 if caller is not the request's buyerId and not admin." status: open created: 2026-05-29 source: Doc vs Code Audit 2026-05-29 diff --git a/Issues/ISSUE-028-delivery-code-generated-socket-event-broadcasts-raw-6-digit-.md b/Issues/ISSUE-028-delivery-code-generated-socket-event-broadcasts-raw-6-digit-.md index 1aaef48..e7310d6 100644 --- a/Issues/ISSUE-028-delivery-code-generated-socket-event-broadcasts-raw-6-digit-.md +++ b/Issues/ISSUE-028-delivery-code-generated-socket-event-broadcasts-raw-6-digit-.md @@ -4,6 +4,9 @@ title: "delivery-code-generated socket event broadcasts raw 6-digit code to enti severity: major domain: Delivery labels: [security, bug, backend, major, delivery] +status: resolved +resolved: 2026-05-29 +fix: "Removed 'code' field from the delivery-code-generated socket payload in deliveryService.ts — only metadata (requestId, expiresAt, timestamp) is now broadcast to the room." status: open created: 2026-05-29 source: Doc vs Code Audit 2026-05-29 diff --git a/Issues/ISSUE-029-no-brute-force-protection-on-delivery-code-verification-endp.md b/Issues/ISSUE-029-no-brute-force-protection-on-delivery-code-verification-endp.md index 360fcc6..67c61e2 100644 --- a/Issues/ISSUE-029-no-brute-force-protection-on-delivery-code-verification-endp.md +++ b/Issues/ISSUE-029-no-brute-force-protection-on-delivery-code-verification-endp.md @@ -4,6 +4,9 @@ title: "No brute-force protection on delivery code verification endpoint — 900 severity: major domain: Delivery labels: [security, bug, backend, major, brute-force] +status: resolved +resolved: 2026-05-29 +fix: "Added deliveryCodeVerifyLimiter (express-rate-limit: 10 attempts per 15 min per requestId+userId) to POST /delivery-code/verify in marketplace/routes.ts." status: open created: 2026-05-29 source: Doc vs Code Audit 2026-05-29 diff --git a/Issues/ISSUE-030-post-api-payment-payments-cleanup-pending-admin-check-is-ins.md b/Issues/ISSUE-030-post-api-payment-payments-cleanup-pending-admin-check-is-ins.md index 6a1f7bb..a4d62d8 100644 --- a/Issues/ISSUE-030-post-api-payment-payments-cleanup-pending-admin-check-is-ins.md +++ b/Issues/ISSUE-030-post-api-payment-payments-cleanup-pending-admin-check-is-ins.md @@ -3,6 +3,9 @@ issue: 030 title: "POST /api/payment/payments/cleanup-pending admin check is inside handler only — no middleware-level enforcement" severity: major domain: Admin +status: resolved +resolved: 2026-05-29 +fix: "Moved admin check to middleware: added authorizeRoles('admin') to cleanup-pending route in paymentRoutes.ts and removed inline role check." labels: [security, bug, backend, major, missing-auth] status: open created: 2026-05-29 diff --git a/Issues/ISSUE-031-post-api-points-admin-add-admin-check-is-inside-handler-only.md b/Issues/ISSUE-031-post-api-points-admin-add-admin-check-is-inside-handler-only.md index b9e2dd4..362ea0c 100644 --- a/Issues/ISSUE-031-post-api-points-admin-add-admin-check-is-inside-handler-only.md +++ b/Issues/ISSUE-031-post-api-points-admin-add-admin-check-is-inside-handler-only.md @@ -3,6 +3,9 @@ issue: 031 title: "POST /api/points/admin/add admin check is inside handler only — no middleware-level enforcement" severity: major domain: Admin +status: resolved +resolved: 2026-05-29 +fix: "Replaced inline admin check with authorizeRoles('admin') middleware on POST /admin/add in pointsRoutes.ts." labels: [security, bug, backend, major, missing-auth] status: open created: 2026-05-29 diff --git a/Issues/ISSUE-032-admin-delete-user-via-legacy-endpoint-performs-hard-delete-f.md b/Issues/ISSUE-032-admin-delete-user-via-legacy-endpoint-performs-hard-delete-f.md index 0d23533..a8b904a 100644 --- a/Issues/ISSUE-032-admin-delete-user-via-legacy-endpoint-performs-hard-delete-f.md +++ b/Issues/ISSUE-032-admin-delete-user-via-legacy-endpoint-performs-hard-delete-f.md @@ -3,6 +3,9 @@ issue: 032 title: "Admin delete user via legacy endpoint performs hard delete (findByIdAndDelete) instead of soft delete" severity: major domain: User Management +status: resolved +resolved: 2026-05-29 +fix: "Changed findByIdAndDelete to findByIdAndUpdate({ status: 'deleted' }) in legacy admin delete route in userRoutes.ts." labels: [bug, frontend, backend, major, data-integrity] status: open created: 2026-05-29 diff --git a/Issues/ISSUE-033-admin-can-delete-other-admin-accounts-via-new-controller-leg.md b/Issues/ISSUE-033-admin-can-delete-other-admin-accounts-via-new-controller-leg.md index 50816fd..b4d58ad 100644 --- a/Issues/ISSUE-033-admin-can-delete-other-admin-accounts-via-new-controller-leg.md +++ b/Issues/ISSUE-033-admin-can-delete-other-admin-accounts-via-new-controller-leg.md @@ -4,6 +4,9 @@ title: "Admin can delete other admin accounts via new controller — legacy admi severity: major domain: User Management labels: [security, bug, backend, major, privilege-escalation] +status: resolved +resolved: 2026-05-29 +fix: "Added pre-flight check in userController.deleteUser — looks up target user and returns 403 CANNOT_DELETE_ADMIN if role is 'admin'." status: open created: 2026-05-29 source: Doc vs Code Audit 2026-05-29 diff --git a/Issues/ISSUE-035-frontend-getpaymentstatus-and-confirmpayment-call-non-existe.md b/Issues/ISSUE-035-frontend-getpaymentstatus-and-confirmpayment-call-non-existe.md index fd89830..0fc5b73 100644 --- a/Issues/ISSUE-035-frontend-getpaymentstatus-and-confirmpayment-call-non-existe.md +++ b/Issues/ISSUE-035-frontend-getpaymentstatus-and-confirmpayment-call-non-existe.md @@ -3,6 +3,9 @@ issue: 035 title: "Frontend getPaymentStatus and confirmPayment call non-existent endpoints GET /payment/:id/status and POST /payment/:id/confirm" severity: major domain: Payment +status: resolved +resolved: 2026-05-29 +fix: "Added GET /payments/:id/status and POST /payments/:id/confirm routes to paymentRoutes.ts. Updated getPaymentStatus and confirmPayment in payment.ts to use /payment/payments/:id/status and /payment/payments/:id/confirm." labels: [bug, frontend, major, broken-feature, dispute] status: open created: 2026-05-29 diff --git a/Issues/ISSUE-039-reset-password-with-code-endpoint-has-no-password-complexity.md b/Issues/ISSUE-039-reset-password-with-code-endpoint-has-no-password-complexity.md index 89ca0ab..bcaba74 100644 --- a/Issues/ISSUE-039-reset-password-with-code-endpoint-has-no-password-complexity.md +++ b/Issues/ISSUE-039-reset-password-with-code-endpoint-has-no-password-complexity.md @@ -3,6 +3,9 @@ issue: 039 title: "reset-password-with-code endpoint has no password complexity validation — accepts weak passwords rejected by token-based reset" severity: major domain: Authentication +status: resolved +resolved: 2026-05-29 +fix: "Added complexity validation in authController.resetPasswordWithCode: min 8 chars, requires uppercase, lowercase, and digit." labels: [security, bug, backend, major, auth] status: open created: 2026-05-29 diff --git a/Issues/ISSUE-044-post-api-marketplace-purchase-requests-id-final-approval-cre.md b/Issues/ISSUE-044-post-api-marketplace-purchase-requests-id-final-approval-cre.md index a24f39b..8c7f89e 100644 --- a/Issues/ISSUE-044-post-api-marketplace-purchase-requests-id-final-approval-cre.md +++ b/Issues/ISSUE-044-post-api-marketplace-purchase-requests-id-final-approval-cre.md @@ -4,6 +4,9 @@ title: "POST /api/marketplace/purchase-requests/:id/final-approval creates dummy severity: major domain: Purchase Request labels: [security, bug, backend, major, escrow, bypass] +status: resolved +resolved: 2026-05-29 +fix: "Wrapped dummy payment creation in process.env.NODE_ENV !== 'production' guard in marketplace/routes.ts — in production the route returns 404 when no real payment exists." status: open created: 2026-05-29 source: Doc vs Code Audit 2026-05-29 diff --git a/Issues/ISSUE-045-addparticipants-frontend-sends-participants-string-array-but.md b/Issues/ISSUE-045-addparticipants-frontend-sends-participants-string-array-but.md index 242f66d..4bd3379 100644 --- a/Issues/ISSUE-045-addparticipants-frontend-sends-participants-string-array-but.md +++ b/Issues/ISSUE-045-addparticipants-frontend-sends-participants-string-array-but.md @@ -3,6 +3,9 @@ issue: 045 title: "addParticipants frontend sends { participants: string[] } array but backend expects { userId: string } single user" severity: major domain: Chat +status: resolved +resolved: 2026-05-29 +fix: "Fixed addParticipants in chat.ts to send { participantIds: participants } instead of { participants } — matches backend's expected body shape." labels: [bug, frontend, major, chat] status: open created: 2026-05-29 diff --git a/Issues/ISSUE-048-frontend-reloadnetworkregistry-and-probechain-call-backend-e.md b/Issues/ISSUE-048-frontend-reloadnetworkregistry-and-probechain-call-backend-e.md index ad727e4..e9a41d6 100644 --- a/Issues/ISSUE-048-frontend-reloadnetworkregistry-and-probechain-call-backend-e.md +++ b/Issues/ISSUE-048-frontend-reloadnetworkregistry-and-probechain-call-backend-e.md @@ -4,8 +4,10 @@ title: "Frontend reloadNetworkRegistry and probeChain call backend endpoints tha severity: major domain: Admin labels: [missing-feature, backend, major, admin] -status: open +status: resolved created: 2026-05-29 +resolved: 2026-05-30 +fix: "POST /api/admin/rn/networks/reload and POST /api/admin/rn/networks/probe/:chainId implemented in commit 5681abf (task #8)" source: Doc vs Code Audit 2026-05-29 --- diff --git a/Issues/ISSUE-049-frontend-getconfirmationthresholdhistory-calls-get-api-admin.md b/Issues/ISSUE-049-frontend-getconfirmationthresholdhistory-calls-get-api-admin.md index 4efad19..32b3d98 100644 --- a/Issues/ISSUE-049-frontend-getconfirmationthresholdhistory-calls-get-api-admin.md +++ b/Issues/ISSUE-049-frontend-getconfirmationthresholdhistory-calls-get-api-admin.md @@ -4,8 +4,10 @@ title: "Frontend getConfirmationThresholdHistory calls GET /api/admin/settings/c severity: major domain: Admin labels: [missing-feature, backend, major, admin] -status: open +status: resolved created: 2026-05-29 +resolved: 2026-05-30 +fix: "GET /api/admin/settings/confirmation-thresholds/history implemented in commit 27fb15a (task #9) using ConfigSettingHistory model" source: Doc vs Code Audit 2026-05-29 --- diff --git a/Issues/ISSUE-052-payment-completed-status-not-counted-in-successful-payments-stats.md b/Issues/ISSUE-052-payment-completed-status-not-counted-in-successful-payments-stats.md index 060629b..da2ec9f 100644 --- a/Issues/ISSUE-052-payment-completed-status-not-counted-in-successful-payments-stats.md +++ b/Issues/ISSUE-052-payment-completed-status-not-counted-in-successful-payments-stats.md @@ -3,6 +3,9 @@ issue: 052 title: "'completed' payment status not counted in successfulPayments stats — admin dashboard undercounts" severity: major domain: Payment +status: resolved +resolved: 2026-05-29 +fix: "Added case 'completed' to the successfulPayments switch in paymentService.ts getPaymentStats." labels: [backend, bug] status: open created: 2026-05-29 diff --git a/Issues/ISSUE-053-axios-interceptor-only-handles-401-not-403-for-token-refresh.md b/Issues/ISSUE-053-axios-interceptor-only-handles-401-not-403-for-token-refresh.md index 134b117..59f835d 100644 --- a/Issues/ISSUE-053-axios-interceptor-only-handles-401-not-403-for-token-refresh.md +++ b/Issues/ISSUE-053-axios-interceptor-only-handles-401-not-403-for-token-refresh.md @@ -3,6 +3,9 @@ issue: 053 title: "Axios interceptor only retriggers token refresh for 401, not 403" severity: major domain: Authentication +status: resolved +resolved: 2026-05-29 +fix: "Extended axios response interceptor condition from status === 401 to (status === 401 || status === 403) in axios.ts." labels: [frontend, bug] status: open created: 2026-05-29 diff --git a/Issues/ISSUE-054-login-rate-limiter-counts-all-attempts-not-only-failures.md b/Issues/ISSUE-054-login-rate-limiter-counts-all-attempts-not-only-failures.md index ec119da..b1af1bf 100644 --- a/Issues/ISSUE-054-login-rate-limiter-counts-all-attempts-not-only-failures.md +++ b/Issues/ISSUE-054-login-rate-limiter-counts-all-attempts-not-only-failures.md @@ -3,6 +3,9 @@ issue: 054 title: "Login rate limiter counts all attempts (not just failures) — users locked out after correct logins" severity: major domain: Authentication +status: resolved +resolved: 2026-05-29 +fix: "Split checkLoginAttempts into read-only check and new incrementFailedLoginAttempt. authController now only calls increment on failed login paths, not on all attempts." labels: [backend, bug] status: open created: 2026-05-29 diff --git a/Issues/ISSUE-055-delete-api-files-delete-has-no-ownership-check-requires-new-pe.md b/Issues/ISSUE-055-delete-api-files-delete-has-no-ownership-check-requires-new-pe.md new file mode 100644 index 0000000..22dff69 --- /dev/null +++ b/Issues/ISSUE-055-delete-api-files-delete-has-no-ownership-check-requires-new-pe.md @@ -0,0 +1,38 @@ +--- +issue: 055 +title: "DELETE /api/files/delete has no ownership check — requires new persistence layer (NB-27 skipped)" +severity: high +domain: File Management +labels: [security, backend, idor, skipped-nobrainer] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# DELETE /api/files/delete has no ownership check — requires new persistence layer (NB-27 skipped) + +**Severity:** high +**Domain:** File Management +**Labels:** security, backend, idor, skipped-nobrainer + +## Description + +`fileService.deleteFile()` is a pure filesystem path operation — there is no `File` model and no `createdBy`/`owner` field stored anywhere in the database. Any authenticated user who knows (or guesses) another user's filename can delete that file via `DELETE /api/files/delete?filename=...`. + +This was triaged as NB-27 but skipped because adding an ownership check requires first creating a new File persistence layer (model + write-on-upload path), which is a larger-than-mechanical change that risks introducing new bugs. + +## What is Needed + +1. Create a `File` model (or add an `uploads` sub-document to the User model) that records `{ filename, uploadedBy: ObjectId, createdAt }` when a file is stored. +2. Add a middleware or controller check in `fileController.deleteFile` that looks up the record and requires `req.user.id === file.uploadedBy` (or admin). +3. Back-fill the upload handler to write the record on every `POST /api/files/upload`. + +## Affected Files + +- `backend/src/services/file/fileController.ts` — add ownership check +- `backend/src/services/file/fileRoutes.ts` — (route already protected by `authenticateToken`) +- New: `backend/src/models/File.ts` (or equivalent) — persistence layer + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) diff --git a/Issues/ISSUE-056-backend-verifypayment-and-paymentcallback-routes-unauthenticat.md b/Issues/ISSUE-056-backend-verifypayment-and-paymentcallback-routes-unauthenticat.md new file mode 100644 index 0000000..8661baa --- /dev/null +++ b/Issues/ISSUE-056-backend-verifypayment-and-paymentcallback-routes-unauthenticat.md @@ -0,0 +1,40 @@ +--- +issue: 056 +title: "Backend: verifyPayment and paymentCallback routes unauthenticated — payment completion exploitable" +severity: critical +domain: Payment +labels: [security, backend, authentication, webhook] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Backend: verifyPayment and paymentCallback routes unauthenticated — payment completion exploitable + +**Severity:** critical +**Domain:** Payment +**Labels:** security, backend, authentication, webhook + +## Description + +`POST /payments/verify` and `POST /payments/callback` are registered without `authenticateToken` middleware. Additionally, a non-web3 bypass path (`isWeb3Payment === false`) allows marking a payment completed without any verifiable on-chain or provider proof. + +An unauthenticated actor can call `/payments/verify` for any payment ID and trigger the completion side-effects (status change, offer acceptance, escrow release) without owning that payment. The callback endpoint is similarly unguarded, allowing fake webhook injection. + +## Options + +1. Require `authenticateToken` + ownership check on `/verify`; enforce HMAC signature verification on `/callback` as a provider webhook; remove the `isWeb3Payment=false` bypass so completion always requires verifiable proof. +2. Treat `/callback` as a provider webhook with HMAC only; add auth+ownership for `/verify`. +3. Remove the non-web3 bypass so payments without a verifiable tx cannot be marked completed. + +## Recommendation + +Add `authenticateToken` + ownership to `/verify`, enforce HMAC/on-chain verification on `/callback` as a webhook endpoint, and remove the `isWeb3Payment=false` bypass so completion always requires verifiable proof. + +## Affected Files + +- `backend/src/services/payment/paymentControllerRoutes.ts` — lines 20–21 + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-23 diff --git a/Issues/ISSUE-057-frontend-admin-ui-routes-lack-role-based-authorization-guard.md b/Issues/ISSUE-057-frontend-admin-ui-routes-lack-role-based-authorization-guard.md new file mode 100644 index 0000000..175b497 --- /dev/null +++ b/Issues/ISSUE-057-frontend-admin-ui-routes-lack-role-based-authorization-guard.md @@ -0,0 +1,39 @@ +--- +issue: 057 +title: "Frontend admin UI routes lack role-based authorization guard" +severity: high +domain: Admin +labels: [security, frontend, authorization] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Frontend admin UI routes lack role-based authorization guard + +**Severity:** high +**Domain:** Admin +**Labels:** security, frontend, authorization + +## Description + +The `/dashboard/admin/*` route tree has no `RoleBasedGuard` at the layout level. Any authenticated user who knows the URL can access and interact with admin pages (trezor, payments-awaiting-confirmation, etc.) without any frontend role enforcement. + +## Options + +1. Wrap the admin route segment in a single `RoleBasedGuard(admin)` at the layout level — minimal surface, one place to maintain. +2. Add `useRole` checks inside each section view — more granular but repetitive and error-prone. +3. Server-side redirect in Next.js middleware for `/dashboard/admin/*` based on a decoded role claim — strongest but needs role in token/cookie. + +## Recommendation + +Add a `RoleBasedGuard(admin)` at the admin route-group layout (single chokepoint), and confirm the backend independently enforces admin on every admin API. Defense in depth, low blast radius. + +## Affected Files + +- `frontend/src/app/dashboard/admin/trezor/page.tsx` and all sibling admin pages +- Admin route-group layout file + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-1 diff --git a/Issues/ISSUE-058-frontend-test-payment-mode-enablable-in-production-via-env-var.md b/Issues/ISSUE-058-frontend-test-payment-mode-enablable-in-production-via-env-var.md new file mode 100644 index 0000000..71d9eb0 --- /dev/null +++ b/Issues/ISSUE-058-frontend-test-payment-mode-enablable-in-production-via-env-var.md @@ -0,0 +1,38 @@ +--- +issue: 058 +title: "Frontend test payment mode enablable in production via NEXT_PUBLIC env var" +severity: high +domain: Payment +labels: [security, frontend, test-bypass] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Frontend test payment mode enablable in production via NEXT_PUBLIC env var + +**Severity:** high +**Domain:** Payment +**Labels:** security, frontend, test-bypass + +## Description + +`isTestPaymentEnabled()` in `src/web3/services/test-payment-service.ts` is gated only on `NEXT_PUBLIC_ENABLE_TEST_PAYMENT` env flag. Setting this flag in a production deployment (intentionally or by misconfiguration) activates test-payment mode, which bypasses real payment flows. + +## Options + +1. Gate `isTestPaymentEnabled()` on `(process.env.NODE_ENV !== 'production') AND` the env flag — code-level hard stop. +2. Strip the test-payment code path entirely from production via a build-time define/dead-code elimination. +3. Both: NODE_ENV guard plus CI assertion that the flag is unset in prod env. + +## Recommendation + +Require `NODE_ENV !== 'production'` in addition to the flag, and add a CI check that `NEXT_PUBLIC_ENABLE_TEST_PAYMENT` is absent in production secrets. + +## Affected Files + +- `frontend/src/web3/services/test-payment-service.ts:131` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-3 diff --git a/Issues/ISSUE-059-frontend-auth-provider-clears-tokens-on-any-non-403-error.md b/Issues/ISSUE-059-frontend-auth-provider-clears-tokens-on-any-non-403-error.md new file mode 100644 index 0000000..908ee42 --- /dev/null +++ b/Issues/ISSUE-059-frontend-auth-provider-clears-tokens-on-any-non-403-error.md @@ -0,0 +1,38 @@ +--- +issue: 059 +title: "Frontend auth provider clears tokens on any non-403 error including network failures" +severity: high +domain: Authentication +labels: [bug, frontend, session] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Frontend auth provider clears tokens on any non-403 error including network failures + +**Severity:** high +**Domain:** Authentication +**Labels:** bug, frontend, session + +## Description + +`src/auth/context/jwt/auth-provider.tsx:85` clears tokens and logs out the user on any error from the session-check call, including transient network failures and 5xx server errors. A momentary connectivity issue or backend restart silently logs out all active users. + +## Options + +1. Clear only on 401/403; treat 5xx and network errors as transient (keep tokens, retry). +2. Clear on 401/403 plus explicit invalid-token responses; keep tokens for everything else. +3. Add retry/backoff before deciding to clear. + +## Recommendation + +Clear tokens only on 401/403; on network/5xx errors keep the session and retry. Confirm acceptable retry behavior and UX with owner. + +## Affected Files + +- `frontend/src/auth/context/jwt/auth-provider.tsx:85` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-12 diff --git a/Issues/ISSUE-060-frontend-contacts-popover-reads-userid-from-non-existent-local.md b/Issues/ISSUE-060-frontend-contacts-popover-reads-userid-from-non-existent-local.md new file mode 100644 index 0000000..5ad3071 --- /dev/null +++ b/Issues/ISSUE-060-frontend-contacts-popover-reads-userid-from-non-existent-local.md @@ -0,0 +1,38 @@ +--- +issue: 060 +title: "Frontend contacts-popover reads userId from non-existent localStorage 'user' key" +severity: high +domain: Chat +labels: [bug, frontend] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Frontend contacts-popover reads userId from non-existent localStorage 'user' key + +**Severity:** high +**Domain:** Chat +**Labels:** bug, frontend + +## Description + +`src/layouts/components/contacts-popover.tsx:61` reads `currentUserId` from `localStorage.getItem('user')`, but no part of the auth flow writes a `'user'` key to localStorage. The result is always `null`, breaking any per-user contact filtering in the popover. + +## Options + +1. Use the auth context (`useAuthContext`) to get the real user id. +2. Decode the user id from the access token claims. +3. Add a real `'user'` object to storage on login and read it here. + +## Recommendation + +Pull `currentUserId` from the live auth context rather than a non-existent storage key. Requires confirming the canonical user-id field name in the auth context. + +## Affected Files + +- `frontend/src/layouts/components/contacts-popover.tsx:61` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-13 diff --git a/Issues/ISSUE-061-frontend-socket-context-helpers-accumulate-listeners-without-d.md b/Issues/ISSUE-061-frontend-socket-context-helpers-accumulate-listeners-without-d.md new file mode 100644 index 0000000..2eb0eba --- /dev/null +++ b/Issues/ISSUE-061-frontend-socket-context-helpers-accumulate-listeners-without-d.md @@ -0,0 +1,38 @@ +--- +issue: 061 +title: "Frontend socket context helpers accumulate listeners without dedup — memory/event leaks" +severity: high +domain: Realtime +labels: [bug, frontend, memory-leak] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Frontend socket context helpers accumulate listeners without dedup — memory/event leaks + +**Severity:** high +**Domain:** Realtime +**Labels:** bug, frontend, memory-leak + +## Description + +Socket subscription helpers in `src/socket/contexts/socket-context.tsx:244` do not return unsubscribe functions and do not call `socket.off` when consumers unmount. Each React effect re-registration adds a new listener without removing the old one, causing duplicate event callbacks and memory leaks. + +## Options + +1. Have each `on*` helper return an unsubscribe function and require consumers to call it in effect cleanup. +2. Make helpers stable (`useCallback`) and internally `off()` the previous handler before `on()`. +3. Centralize event handling in the provider and expose state, not raw subscriptions. + +## Recommendation + +Return an unsubscribe from each helper and call `socket.off` in cleanup; also memoize the helpers. This touches many consumers — coordinate as a single refactor pass. + +## Affected Files + +- `frontend/src/socket/contexts/socket-context.tsx:244` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-19 diff --git a/Issues/ISSUE-062-backend-payment-update-routes-lack-ownership-role-guards.md b/Issues/ISSUE-062-backend-payment-update-routes-lack-ownership-role-guards.md new file mode 100644 index 0000000..a7005d2 --- /dev/null +++ b/Issues/ISSUE-062-backend-payment-update-routes-lack-ownership-role-guards.md @@ -0,0 +1,38 @@ +--- +issue: 062 +title: "Backend: payment update routes lack ownership/role guards (cluster)" +severity: high +domain: Payment +labels: [security, backend, authorization, idor] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Backend: payment update routes lack ownership/role guards (cluster) + +**Severity:** high +**Domain:** Payment +**Labels:** security, backend, authorization, idor + +## Description + +`PUT /:id updatePayment`, `PATCH /marketplace/payments/:id`, and status-change routes in `paymentControllerRoutes.ts` require only `authenticateToken` — no role check, no ownership check, no status-transition whitelist. Any authenticated user can change any payment's status to any value. + +## Options + +1. Add an admin-role middleware to all payment status-mutating routes and a status whitelist. +2. Add ownership checks (`req.user.id === buyerId/sellerId`) plus a strict allowed-status-transition validator shared across routes. +3. Both: admin-only for arbitrary status writes; constrained self-service transitions for owners. + +## Recommendation + +Introduce a shared `requireAdmin` middleware for arbitrary status writes and a centralized transition validator; owners may only trigger whitelisted transitions. This is a business-logic and authZ change across multiple routes. + +## Affected Files + +- `backend/src/services/payment/paymentControllerRoutes.ts:17` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-22 diff --git a/Issues/ISSUE-063-backend-legacy-marketplace-patch-payments-id-lets-any-user-set.md b/Issues/ISSUE-063-backend-legacy-marketplace-patch-payments-id-lets-any-user-set.md new file mode 100644 index 0000000..3ed2b54 --- /dev/null +++ b/Issues/ISSUE-063-backend-legacy-marketplace-patch-payments-id-lets-any-user-set.md @@ -0,0 +1,38 @@ +--- +issue: 063 +title: "Backend: legacy marketplace PATCH /payments/:id lets buyer/seller set any status" +severity: high +domain: Payment +labels: [security, backend, authorization] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Backend: legacy marketplace PATCH /payments/:id lets buyer/seller set any status + +**Severity:** high +**Domain:** Payment +**Labels:** security, backend, authorization + +## Description + +`backend/src/services/marketplace/routes.ts:237` registers a legacy PATCH endpoint for payment status that has no admin guard and no status whitelist. Buyers or sellers can set any status value directly. + +## Options + +1. Add admin role guard + status whitelist. +2. Deprecate/remove the legacy route if superseded by the new payment controller. +3. Restrict to system/internal callers only. + +## Recommendation + +If the route is legacy and superseded by the new payment controller, remove it. Otherwise gate with admin + whitelist. Needs confirmation that it is unused before removal. + +## Affected Files + +- `backend/src/services/marketplace/routes.ts:237` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-24 diff --git a/Issues/ISSUE-064-backend-request-network-allow-test-webhooks-bypasses-signature.md b/Issues/ISSUE-064-backend-request-network-allow-test-webhooks-bypasses-signature.md new file mode 100644 index 0000000..18ed4cd --- /dev/null +++ b/Issues/ISSUE-064-backend-request-network-allow-test-webhooks-bypasses-signature.md @@ -0,0 +1,39 @@ +--- +issue: 064 +title: "Backend: REQUEST_NETWORK_ALLOW_TEST_WEBHOOKS bypasses signature verification" +severity: high +domain: Payment +labels: [security, backend, webhook, test-bypass] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Backend: REQUEST_NETWORK_ALLOW_TEST_WEBHOOKS bypasses signature verification + +**Severity:** high +**Domain:** Payment +**Labels:** security, backend, webhook, test-bypass + +## Description + +`REQUEST_NETWORK_ALLOW_TEST_WEBHOOKS` env flag disables HMAC signature verification on Request Network webhooks at `requestNetworkRoutes.ts:104` and `requestNetworkAdapter.ts:77`. If this flag is set in production (or if `NODE_ENV` is not production), any unauthenticated actor can forge a webhook and trigger payment completion. + +## Options + +1. Gate the bypass on `NODE_ENV === 'test'` only and ignore the env flag in production. +2. Require both `NODE_ENV !== 'production'` AND the flag. +3. Remove the env-flag bypass entirely; use a dedicated test harness. + +## Recommendation + +Allow the bypass only when `NODE_ENV === 'test'`; ignore `REQUEST_NETWORK_ALLOW_TEST_WEBHOOKS` in production. Apply the same fix in `requestNetworkAdapter.ts:77`. + +## Affected Files + +- `backend/src/services/payment/requestNetwork/requestNetworkRoutes.ts:104` +- `backend/src/services/payment/requestNetwork/requestNetworkAdapter.ts:77` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-25 diff --git a/Issues/ISSUE-065-backend-rn-webhook-advances-purchaserequest-to-non-existent-fu.md b/Issues/ISSUE-065-backend-rn-webhook-advances-purchaserequest-to-non-existent-fu.md new file mode 100644 index 0000000..b56aaf7 --- /dev/null +++ b/Issues/ISSUE-065-backend-rn-webhook-advances-purchaserequest-to-non-existent-fu.md @@ -0,0 +1,38 @@ +--- +issue: 065 +title: "Backend: RN webhook advances PurchaseRequest to non-existent 'funded' status" +severity: high +domain: Payment +labels: [bug, backend, state-machine] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Backend: RN webhook advances PurchaseRequest to non-existent 'funded' status + +**Severity:** high +**Domain:** Payment +**Labels:** bug, backend, state-machine + +## Description + +`src/services/payment/request-network/requestNetworkWebhook.ts:123` sets `PurchaseRequest.status = 'funded'` when a payment is confirmed. `'funded'` does not exist in `STATUS_PROGRESSION_ORDER` or the status enum. The update is silently dropped by Mongoose, leaving the purchase request in its old status and breaking downstream state transitions. + +## Options + +1. Use the canonical `'processing'` status that exists in `STATUS_PROGRESSION_ORDER`. +2. Add `'funded'` as a real status across progression and transition maps. +3. Route through the same `propagatePaymentCompletion` path the new flow uses. + +## Recommendation + +Replace `'funded'` with the canonical status used by the current completion flow (likely `'processing'`), keeping the `escrow.funded` flag if needed. This is a state-machine decision — verify the intended state after on-chain funding confirmation. + +## Affected Files + +- `backend/src/services/payment/request-network/requestNetworkWebhook.ts:123` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-31 diff --git a/Issues/ISSUE-066-backend-payout-and-release-confirm-set-non-enum-status.md b/Issues/ISSUE-066-backend-payout-and-release-confirm-set-non-enum-status.md new file mode 100644 index 0000000..ea0743c --- /dev/null +++ b/Issues/ISSUE-066-backend-payout-and-release-confirm-set-non-enum-status.md @@ -0,0 +1,39 @@ +--- +issue: 066 +title: "Backend: payout/confirm and release/confirm need canonical terminal status — DEC-32" +severity: high +domain: Payment +labels: [bug, backend, state-machine] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Backend: payout/confirm and release/confirm need canonical terminal status — DEC-32 + +**Severity:** high +**Domain:** Payment +**Labels:** bug, backend, state-machine + +## Description + +NB-29 was applied as an interim fix (setting `status:'completed'`), but the correct terminal status for a released/paid-out payment has not been decided. The enum currently has no `'released'` value. Until DEC-32 is resolved, the interim `'completed'` value may be incorrect for payments that need to be distinguished from non-released completions. + +## Options + +1. Add `'released'` to the Payment status enum and update all status-dependent logic (dashboards, filters, cleanup). +2. Map release/payout to the existing `'completed'` status plus a separate `releasedAt`/payout flag. +3. Introduce a dedicated escrow/payout sub-state field instead of overloading `status`. + +## Recommendation + +Decide the canonical representation (likely add `'released'` to the enum or a dedicated payout state) and update dashboards/filters consistently. The NB-29 interim fix uses `'completed'` — verify this does not cause dashboard overcounting or misclassification. + +## Affected Files + +- `backend/src/services/payment/requestNetwork/requestNetworkRoutes.ts:526` +- `backend/src/models/Payment.ts` — enum definition + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-32 diff --git a/Issues/ISSUE-067-backend-amount-mismatch-check-runs-after-payment-saved-and-offe.md b/Issues/ISSUE-067-backend-amount-mismatch-check-runs-after-payment-saved-and-offe.md new file mode 100644 index 0000000..008947f --- /dev/null +++ b/Issues/ISSUE-067-backend-amount-mismatch-check-runs-after-payment-saved-and-offe.md @@ -0,0 +1,38 @@ +--- +issue: 067 +title: "Backend: amount-mismatch check runs after payment saved and offers accepted" +severity: medium +domain: Payment +labels: [bug, backend, logic] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Backend: amount-mismatch check runs after payment saved and offers accepted + +**Severity:** medium +**Domain:** Payment +**Labels:** bug, backend, logic + +## Description + +In `paymentController.ts:886-889`, the check comparing `storedAmount` vs `amount` is executed after the payment has already been saved and offers accepted. If there is a mismatch, those side-effects cannot be rolled back, potentially leaving the system in an inconsistent state. + +## Options + +1. Move the `storedAmount` vs `amount` check before saving/advancing/accepting offers. +2. Wrap the verify flow in a transaction and roll back on mismatch. +3. Validate amount at intent-creation and re-check before completion. + +## Recommendation + +Reorder so the amount-mismatch check (and ideally a transaction) gates all side-effects. This is a control-flow/business-logic change. + +## Affected Files + +- `backend/src/services/payment/paymentController.ts:886-889` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-33 diff --git a/Issues/ISSUE-068-backend-datacleanuservice-deletes-payments-without-provider-sco.md b/Issues/ISSUE-068-backend-datacleanuservice-deletes-payments-without-provider-sco.md new file mode 100644 index 0000000..d10e5cd --- /dev/null +++ b/Issues/ISSUE-068-backend-datacleanuservice-deletes-payments-without-provider-sco.md @@ -0,0 +1,41 @@ +--- +issue: 068 +title: "Backend: dataCleanupService deletes Payments without provider scoping — risk of destroying escrow records" +severity: high +domain: Admin +labels: [security, backend, data-loss, escrow] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Backend: dataCleanupService deletes Payments without provider scoping — risk of destroying escrow records + +**Severity:** high +**Domain:** Admin +**Labels:** security, backend, data-loss, escrow + +## Description + +`dataCleanupService.ts:121` deletes Payment documents without filtering by `provider`. Request Network and SHKeeper escrow payments are webhook-driven and can take hours to confirm. Sweeping them deletes the ledger records that webhooks need to reconcile, silently destroying multi-seller cart records. + +This matches the project memory note: "Any Payment-collection cleanup/orphan query MUST scope by `provider:`." + +## Options + +1. Scope all payment deletes by provider (exclude `request.network`/`shkeeper` escrow records). +2. Soft-delete instead of hard delete for payments. +3. Disallow payment-collection cleanup entirely from this tool. + +## Recommendation + +Require provider scoping on every payment delete and prefer soft-delete; never sweep escrow-driven records. This is a data-loss risk. + +## Affected Files + +- `backend/src/services/admin/dataCleanupService.ts:121` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-38 +- Project memory: `feedback_payment_cleanup_provider_filter.md` diff --git a/Issues/ISSUE-069-backend-cleanupoldpendingpayments-deletes-pending-rn-payments-m.md b/Issues/ISSUE-069-backend-cleanupoldpendingpayments-deletes-pending-rn-payments-m.md new file mode 100644 index 0000000..b8ff1a4 --- /dev/null +++ b/Issues/ISSUE-069-backend-cleanupoldpendingpayments-deletes-pending-rn-payments-m.md @@ -0,0 +1,38 @@ +--- +issue: 069 +title: "Backend: cleanupOldPendingPayments deletes pending RN payments mid-flow" +severity: high +domain: Payment +labels: [bug, backend, data-loss, escrow] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Backend: cleanupOldPendingPayments deletes pending RN payments mid-flow + +**Severity:** high +**Domain:** Payment +**Labels:** bug, backend, data-loss, escrow + +## Description + +`cleanupPendingPayments.ts:42` deletes pending payments after a TTL without excluding webhook-driven providers. Request Network flows can take hours or days to receive on-chain confirmation. A pending RN payment deleted by this cleanup will never be reconciled when the late webhook arrives. + +## Options + +1. Exclude provider `request.network`/`shkeeper` from the cleanup, or greatly extend the TTL for them. +2. Mark as `expired` instead of deleting, so a late webhook can reconcile. +3. Only delete pending payments that have no associated active purchase request. + +## Recommendation + +Exclude webhook-driven providers (or use a long TTL) and prefer expire-over-delete so late webhooks can reconcile. This is a data-loss risk. + +## Affected Files + +- `backend/src/services/payment/cleanupPendingPayments.ts:42` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-39 diff --git a/Issues/ISSUE-070-backend-notifyallsellersaboutnewrequest-unbounded-fan-out.md b/Issues/ISSUE-070-backend-notifyallsellersaboutnewrequest-unbounded-fan-out.md new file mode 100644 index 0000000..93cf24d --- /dev/null +++ b/Issues/ISSUE-070-backend-notifyallsellersaboutnewrequest-unbounded-fan-out.md @@ -0,0 +1,38 @@ +--- +issue: 070 +title: "Backend: notifyAllSellersAboutNewRequest unbounded fan-out — N DB writes + N socket emits per new request" +severity: high +domain: Marketplace +labels: [performance, backend, scalability] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Backend: notifyAllSellersAboutNewRequest unbounded fan-out — N DB writes + N socket emits per new request + +**Severity:** high +**Domain:** Marketplace +**Labels:** performance, backend, scalability + +## Description + +`PurchaseRequestService.ts:196` loops over every seller and issues one `Notification.insertOne` and one `socket.emit` per seller, wrapped in `setTimeout`. With hundreds of sellers this creates hundreds of sequential DB writes and socket emits, blocking the event loop and risking OOM. + +## Options + +1. Batch with `insertMany` for notifications and a single room/broadcast emit instead of per-seller `setTimeout`. +2. Move to a queue/worker that processes seller notifications asynchronously with concurrency limits. +3. Fan out via a topic/room subscription rather than per-seller writes. + +## Recommendation + +Use `insertMany` + a single broadcast/room emit, or offload to a queue with bounded concurrency. This is an architectural change. + +## Affected Files + +- `backend/src/services/marketplace/PurchaseRequestService.ts:196` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-40 diff --git a/Issues/ISSUE-071-backend-getreferrals-n-plus-1-purchaserequest-and-pointtransac.md b/Issues/ISSUE-071-backend-getreferrals-n-plus-1-purchaserequest-and-pointtransac.md new file mode 100644 index 0000000..ad6c27c --- /dev/null +++ b/Issues/ISSUE-071-backend-getreferrals-n-plus-1-purchaserequest-and-pointtransac.md @@ -0,0 +1,38 @@ +--- +issue: 071 +title: "Backend: getReferrals N+1 — PurchaseRequest + PointTransaction per referral" +severity: high +domain: Points +labels: [performance, backend, n-plus-1] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Backend: getReferrals N+1 — PurchaseRequest + PointTransaction per referral + +**Severity:** high +**Domain:** Points +**Labels:** performance, backend, n-plus-1 + +## Description + +`PointsService.ts:312` issues two sequential DB queries per referred user: one `PurchaseRequest.findOne` and one `PointTransaction.find`. With N referrals, this results in 2×N queries. Under load this will cause slow responses and high DB load. + +## Options + +1. Batch with `$in` queries and aggregate grouped by referred user. +2. Precompute referral stats in a maintained summary doc. +3. Add pagination plus batched lookups. + +## Recommendation + +Replace per-referral queries with batched `$in` queries and an aggregation grouped by user; add pagination. + +## Affected Files + +- `backend/src/services/points/PointsService.ts:312` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-41 diff --git a/Issues/ISSUE-072-backend-chat-messages-stored-as-embedded-array-unbounded-growth.md b/Issues/ISSUE-072-backend-chat-messages-stored-as-embedded-array-unbounded-growth.md new file mode 100644 index 0000000..0aa3936 --- /dev/null +++ b/Issues/ISSUE-072-backend-chat-messages-stored-as-embedded-array-unbounded-growth.md @@ -0,0 +1,39 @@ +--- +issue: 072 +title: "Backend: chat messages stored as embedded array — unbounded document growth, 16MB ceiling" +severity: high +domain: Chat +labels: [performance, backend, data-model] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Backend: chat messages stored as embedded array — unbounded document growth, 16MB ceiling + +**Severity:** high +**Domain:** Chat +**Labels:** performance, backend, data-model + +## Description + +`backend/src/models/Chat.ts:173` stores all messages as an embedded array in the Chat document. MongoDB's 16MB document size limit will be hit for active long-running chats. Reads also load the full message history into memory even when only the latest page is needed. + +## Options + +1. Migrate messages to a `Messages` collection keyed by `chatId` with pagination. +2. Cap embedded messages and archive older ones. +3. Keep embedded but project only needed messages (slice) on reads. + +## Recommendation + +Plan migration to a dedicated `Messages` collection. This is a large data-model migration that needs careful coordination. + +## Affected Files + +- `backend/src/models/Chat.ts:173` +- `backend/src/services/chat/ChatService.ts` — all read paths + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-42 diff --git a/Issues/ISSUE-073-backend-payment-provider-enum-missing-shkeeper.md b/Issues/ISSUE-073-backend-payment-provider-enum-missing-shkeeper.md new file mode 100644 index 0000000..1fd799f --- /dev/null +++ b/Issues/ISSUE-073-backend-payment-provider-enum-missing-shkeeper.md @@ -0,0 +1,39 @@ +--- +issue: 073 +title: "Backend: Payment provider enum missing 'shkeeper' — records silently dropped" +severity: high +domain: Payment +labels: [bug, backend, data-model] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Backend: Payment provider enum missing 'shkeeper' — records silently dropped + +**Severity:** high +**Domain:** Payment +**Labels:** bug, backend, data-model + +## Description + +`backend/src/models/Payment.ts:46` defines the `provider` enum without including `'shkeeper'`. Any Payment document saved with `provider: 'shkeeper'` will fail Mongoose validation or be silently dropped. All downstream `Payment.find({provider: 'shkeeper'})` filters also return empty results. + +## Options + +1. Add `'shkeeper'` to the enum and audit all provider filters/migrations. +2. Migrate existing shkeeper records and standardize the provider taxonomy. +3. Add enum value plus a data migration to repair any silently-dropped values. + +## Recommendation + +Add `'shkeeper'` to the enum AND run a data audit/migration to repair records and verify every `Payment.find({provider})` filter. + +## Affected Files + +- `backend/src/models/Payment.ts:46` +- All `Payment.find({ provider: ... })` call sites + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-30 diff --git a/Issues/ISSUE-074-backend-env-development-committed-with-live-telegram-and-smtp-s.md b/Issues/ISSUE-074-backend-env-development-committed-with-live-telegram-and-smtp-s.md new file mode 100644 index 0000000..a9b1ff6 --- /dev/null +++ b/Issues/ISSUE-074-backend-env-development-committed-with-live-telegram-and-smtp-s.md @@ -0,0 +1,39 @@ +--- +issue: 074 +title: "Backend: Telegram bot token + SMTP key (and others) committed in .env.development" +severity: high +domain: Security +labels: [security, backend, secrets, rotation-required] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Backend: Telegram bot token + SMTP key (and others) committed in .env.development + +**Severity:** high +**Domain:** Security +**Labels:** security, backend, secrets, rotation-required + +## Description + +`backend/.env.development` contains live production secrets including the Telegram bot token and Resend SMTP API key (and potentially others). NB-33 replaced the `.env.example` placeholders, but `.env.development` itself contains the live values and is tracked in git. + +The `.dockerignore` whitelist (see ISSUE-075) also copies this file into production images. + +## What Must Happen + +1. Rotate the Telegram bot token immediately. +2. Rotate the Resend SMTP API key immediately. +3. Untrack `.env.development` from git and scrub it from history. +4. Inject secrets at runtime via CI/vault rather than committed env files. + +## Affected Files + +- `backend/.env.development:31` (and potentially other lines) +- `backend/.dockerignore:14` (whitelist — see ISSUE-075) + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-56 +- [[ISSUE-075-backend-dockerignore-whitelists-env-development-into-prod-image|ISSUE-075]] diff --git a/Issues/ISSUE-075-backend-dockerignore-whitelists-env-development-into-prod-image.md b/Issues/ISSUE-075-backend-dockerignore-whitelists-env-development-into-prod-image.md new file mode 100644 index 0000000..c9f1ebd --- /dev/null +++ b/Issues/ISSUE-075-backend-dockerignore-whitelists-env-development-into-prod-image.md @@ -0,0 +1,40 @@ +--- +issue: 075 +title: "Backend: .dockerignore whitelists .env.development into production image" +severity: high +domain: Security +labels: [security, backend, secrets, ci-cd] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Backend: .dockerignore whitelists .env.development into production image + +**Severity:** high +**Domain:** Security +**Labels:** security, backend, secrets, ci-cd + +## Description + +`backend/.dockerignore:14` contains `!.env.development`, which negates the `.env*` ignore rule and causes `.env.development` (with live secrets) to be copied into every production Docker image. Any container pull or image inspection exposes the credentials. + +## Options + +1. Remove the `!.env.development` whitelist so no env file is copied into images. +2. Use a dedicated `.env.production` injected at runtime only. +3. Both: strip env files from image and inject secrets via runtime env. + +## Recommendation + +Remove the whitelist and never copy env files into images; inject secrets at runtime. Pair with rotating the leaked secrets (see ISSUE-074) and fixing backend config to not load `.env.development` unconditionally (see ISSUE-101). + +## Affected Files + +- `backend/.dockerignore:14` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-50 +- [[ISSUE-074-backend-env-development-committed-with-live-telegram-and-smtp-s|ISSUE-074]] +- [[ISSUE-101-backend-config-loads-env-development-unconditionally|ISSUE-101]] diff --git a/Issues/ISSUE-076-scanner-ssrf-via-unvalidated-callbackurl.md b/Issues/ISSUE-076-scanner-ssrf-via-unvalidated-callbackurl.md new file mode 100644 index 0000000..919c69e --- /dev/null +++ b/Issues/ISSUE-076-scanner-ssrf-via-unvalidated-callbackurl.md @@ -0,0 +1,39 @@ +--- +issue: 076 +title: "Scanner: SSRF via unvalidated callbackUrl on intent creation" +severity: high +domain: Scanner +labels: [security, scanner, ssrf] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Scanner: SSRF via unvalidated callbackUrl on intent creation + +**Severity:** high +**Domain:** Scanner +**Labels:** security, scanner, ssrf + +## Description + +`scanner/api.go:143` accepts a caller-supplied `callbackUrl` without validating the scheme, host, or whether it points to an internal/RFC-1918 address. A caller can set `callbackUrl` to any internal service URL and receive webhook deliveries from the scanner, enabling server-side request forgery. + +## Options + +1. Allowlist schemes (`https` only) and reject RFC-1918/link-local/loopback hosts at create time and at dial time via a custom `DialContext`. +2. Restrict callbacks to a configured set of backend hostnames. +3. Route webhooks through an egress proxy that blocks internal ranges. + +## Recommendation + +Enforce `https`-only + block private/loopback/link-local at both validation and dial time (custom `DialContext`), ideally plus a host allowlist. + +## Affected Files + +- `scanner/api.go:143` +- `scanner/webhook.go` — dial path + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-57 diff --git a/Issues/ISSUE-077-scanner-caller-can-override-confirmation-threshold-down-to-1.md b/Issues/ISSUE-077-scanner-caller-can-override-confirmation-threshold-down-to-1.md new file mode 100644 index 0000000..0ed4b86 --- /dev/null +++ b/Issues/ISSUE-077-scanner-caller-can-override-confirmation-threshold-down-to-1.md @@ -0,0 +1,39 @@ +--- +issue: 077 +title: "Scanner: caller can override confirmation threshold down to 1 — reorg safety bypass" +severity: high +domain: Scanner +labels: [security, scanner, reorg] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Scanner: caller can override confirmation threshold down to 1 — reorg safety bypass + +**Severity:** high +**Domain:** Scanner +**Labels:** security, scanner, reorg + +## Description + +`scanner/api.go:170` accepts a caller-supplied `confirmations` value and uses it as-is without enforcing the chain-config threshold as a floor. A caller can set `confirmations: 1` on a chain that requires 12 confirmations, bypassing reorg safety and causing premature payment confirmation. + +## Options + +1. Clamp confirmations to `max(callerValue, chainConfigThreshold)` — config is a floor. +2. Ignore caller value entirely; always use chain config. +3. Allow override only above the chain threshold. + +## Recommendation + +Treat the chain config threshold as a hard floor (`max` of caller and config). Changes reorg-safety semantics. + +## Affected Files + +- `scanner/api.go:170` +- `scanner/config.go` — chain threshold definition + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-58 diff --git a/Issues/ISSUE-078-scanner-idempotency-path-ignores-mismatched-parameters.md b/Issues/ISSUE-078-scanner-idempotency-path-ignores-mismatched-parameters.md new file mode 100644 index 0000000..e198095 --- /dev/null +++ b/Issues/ISSUE-078-scanner-idempotency-path-ignores-mismatched-parameters.md @@ -0,0 +1,38 @@ +--- +issue: 078 +title: "Scanner: idempotency path ignores mismatched parameters — silent collision" +severity: high +domain: Scanner +labels: [bug, scanner, idempotency] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Scanner: idempotency path ignores mismatched parameters — silent collision + +**Severity:** high +**Domain:** Scanner +**Labels:** bug, scanner, idempotency + +## Description + +`scanner/api.go:191` returns the existing intent when an `intentId` collision is detected, but does not compare the stored parameters to the incoming request. If a caller reuses an `intentId` with different `amount`, `tokenAddress`, or `callbackUrl`, the scanner silently returns the old intent and monitors the wrong payment parameters. + +## Options + +1. Return `409 Conflict` if stored params differ from request. +2. Return existing intent only if params match; else error. +3. Treat any reuse as conflict regardless of params. + +## Recommendation + +Compare stored vs incoming params and return `409 Conflict` on mismatch (return existing only on exact match). Changes API contract. + +## Affected Files + +- `scanner/api.go:191` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-62 diff --git a/Issues/ISSUE-079-frontend-telegram-bot-token-committed-in-gitleaks-toml-allowli.md b/Issues/ISSUE-079-frontend-telegram-bot-token-committed-in-gitleaks-toml-allowli.md new file mode 100644 index 0000000..8454766 --- /dev/null +++ b/Issues/ISSUE-079-frontend-telegram-bot-token-committed-in-gitleaks-toml-allowli.md @@ -0,0 +1,39 @@ +--- +issue: 079 +title: "Frontend: Telegram bot token committed in .gitleaks.toml allowlist — must rotate" +severity: high +domain: Security +labels: [security, frontend, secrets, rotation-required] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Frontend: Telegram bot token committed in .gitleaks.toml allowlist — must rotate + +**Severity:** high +**Domain:** Security +**Labels:** security, frontend, secrets, rotation-required + +## Description + +`frontend/.gitleaks.toml:15` contains a value-based allowlist entry with the plaintext Telegram bot token. Value-based allowlist entries in gitleaks effectively publish the secret in the allowlist itself. The same token appears in the backend `.env.development` (see ISSUE-074). + +## Options + +1. Replace the value-based allowlist with a path/commit-hash allowlist and rotate the token. +2. Remove the allowlist entry entirely after scrubbing the secret from source. +3. Use the handle-gitleaks workflow to triage and remediate. + +## Recommendation + +Rotate the token, switch to a non-value-based allowlist (path/fingerprint), and scrub history. Coordinate with backend ISSUE-074 since the same token appears there. + +## Affected Files + +- `frontend/.gitleaks.toml:15` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-78 +- [[ISSUE-074-backend-env-development-committed-with-live-telegram-and-smtp-s|ISSUE-074]] diff --git a/Issues/ISSUE-080-frontend-open-redirect-via-unvalidated-returnto-in-guestguard.md b/Issues/ISSUE-080-frontend-open-redirect-via-unvalidated-returnto-in-guestguard.md new file mode 100644 index 0000000..be986a4 --- /dev/null +++ b/Issues/ISSUE-080-frontend-open-redirect-via-unvalidated-returnto-in-guestguard.md @@ -0,0 +1,39 @@ +--- +issue: 080 +title: "Frontend: open redirect via unvalidated returnTo in GuestGuard" +severity: medium +domain: Authentication +labels: [security, frontend, open-redirect] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Frontend: open redirect via unvalidated returnTo in GuestGuard + +**Severity:** medium +**Domain:** Authentication +**Labels:** security, frontend, open-redirect + +## Description + +`src/auth/guard/guest-guard.tsx:27` passes `returnTo` directly to `router.replace()` without validating that it is a same-origin relative path. An attacker can craft a link with `returnTo=//evil.com` or `returnTo=https://evil.com` to redirect users after login. + +## Options + +1. Allow only same-origin relative paths starting with a single `/` and not `//` — strict, safe default. +2. Allowlist of known internal path prefixes — safest but must be maintained. +3. Parse with `URL()` against `window.origin` and reject cross-origin — robust but slightly more code. + +## Recommendation + +Reject any `returnTo` that does not match `^/(?!/)` (single leading slash, not protocol-relative), else fall back to default landing route. One small helper, applied everywhere `returnTo` is consumed. + +## Affected Files + +- `frontend/src/auth/guard/guest-guard.tsx:27` +- Any other component that reads and acts on `returnTo` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-2 diff --git a/Issues/ISSUE-081-frontend-tokens-stored-in-localstorage-xss-accessible.md b/Issues/ISSUE-081-frontend-tokens-stored-in-localstorage-xss-accessible.md new file mode 100644 index 0000000..a49dec1 --- /dev/null +++ b/Issues/ISSUE-081-frontend-tokens-stored-in-localstorage-xss-accessible.md @@ -0,0 +1,40 @@ +--- +issue: 081 +title: "Frontend: auth tokens stored in localStorage — XSS-accessible" +severity: medium +domain: Authentication +labels: [security, frontend, session] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Frontend: auth tokens stored in localStorage — XSS-accessible + +**Severity:** medium +**Domain:** Authentication +**Labels:** security, frontend, session + +## Description + +`src/auth/context/jwt/action.ts:100` stores access and refresh tokens in `localStorage`. Any XSS vulnerability can steal these tokens and impersonate the user. The risk is compounded by the lack of a Content-Security-Policy (see ISSUE-083). + +## Options + +1. Move refresh token to HttpOnly cookie, keep short-lived access token in memory — strong, but requires backend cookie + CSRF work. +2. Keep localStorage but add strict CSP + sanitization to reduce XSS surface — cheaper, weaker. +3. Full cookie-based session with `SameSite=strict` — strongest, largest change to axios/socket auth. + +## Recommendation + +Plan a migration to HttpOnly refresh cookie + in-memory access token, coordinated with backend. This is a large, cross-cutting change that breaks many call sites — treat as a deliberate project. + +## Affected Files + +- `frontend/src/auth/context/jwt/action.ts:100` +- `frontend/src/lib/axios.ts` — auth header injection + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-4 +- [[ISSUE-083-frontend-no-content-security-policy-header-in-next-config|ISSUE-083]] diff --git a/Issues/ISSUE-082-frontend-wallet-ownership-signature-verification-is-a-no-op.md b/Issues/ISSUE-082-frontend-wallet-ownership-signature-verification-is-a-no-op.md new file mode 100644 index 0000000..541b75c --- /dev/null +++ b/Issues/ISSUE-082-frontend-wallet-ownership-signature-verification-is-a-no-op.md @@ -0,0 +1,38 @@ +--- +issue: 082 +title: "Frontend: wallet ownership signature verification is a no-op" +severity: medium +domain: Web3 +labels: [security, frontend, wallet] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Frontend: wallet ownership signature verification is a no-op + +**Severity:** medium +**Domain:** Web3 +**Labels:** security, frontend, wallet + +## Description + +`src/sections/account/account-wallet-connection.tsx:425` has a `verifySignature` stub that always passes. The frontend does not actually verify that the signature matches the claimed wallet address, meaning any wallet address can be submitted without proof of ownership. + +## Options + +1. Implement real client-side verification with `ethers.verifyMessage(message, signature) === wallet.address` as a UX pre-check, keep backend authoritative. +2. Remove the misleading `verifySignature` stub and rely solely on backend (document this). +3. Both: client pre-check and confirm backend enforcement exists. + +## Recommendation + +Implement `ethers.verifyMessage` as a UX gate AND verify the backend enforces ownership. The stub is actively misleading. + +## Affected Files + +- `frontend/src/sections/account/account-wallet-connection.tsx:425` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-6 diff --git a/Issues/ISSUE-083-frontend-no-content-security-policy-header-in-next-config.md b/Issues/ISSUE-083-frontend-no-content-security-policy-header-in-next-config.md new file mode 100644 index 0000000..40736bc --- /dev/null +++ b/Issues/ISSUE-083-frontend-no-content-security-policy-header-in-next-config.md @@ -0,0 +1,39 @@ +--- +issue: 083 +title: "Frontend: no Content-Security-Policy header in Next.js config" +severity: medium +domain: Security +labels: [security, frontend, csp] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Frontend: no Content-Security-Policy header in Next.js config + +**Severity:** medium +**Domain:** Security +**Labels:** security, frontend, csp + +## Description + +`next.config.ts:29` does not set a `Content-Security-Policy` header. Without CSP, XSS attacks have unrestricted script execution, making token theft (localStorage) and DOM-based attacks much easier. + +## Options + +1. Ship `Content-Security-Policy-Report-Only` first to collect violations, then enforce — safe rollout. +2. Enforce a moderate CSP allowing required hosts (Telegram, WalletConnect, Mapbox, Sentry) with nonces for inline scripts. +3. Strict CSP with nonces and removal of all inline scripts — strongest but requires refactoring `layout.tsx` inline scripts. + +## Recommendation + +Ship `Content-Security-Policy-Report-Only` first, gather violations for a week, then enforce. Inline scripts in `layout.tsx` must move to nonces. Non-trivial rollout. + +## Affected Files + +- `frontend/next.config.ts:29` +- `frontend/src/app/layout.tsx` — inline scripts that need nonces + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-8 diff --git a/Issues/ISSUE-084-frontend-console-error-warn-suppression-masks-prod-errors.md b/Issues/ISSUE-084-frontend-console-error-warn-suppression-masks-prod-errors.md new file mode 100644 index 0000000..7b28c3f --- /dev/null +++ b/Issues/ISSUE-084-frontend-console-error-warn-suppression-masks-prod-errors.md @@ -0,0 +1,39 @@ +--- +issue: 084 +title: "Frontend: console.error/warn suppression masks production errors" +severity: medium +domain: Observability +labels: [bug, frontend, logging] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Frontend: console.error/warn suppression masks production errors + +**Severity:** medium +**Domain:** Observability +**Labels:** bug, frontend, logging + +## Description + +`src/app/layout.tsx:95` overrides `console.error` and `console.warn` globally in all environments. In production this suppresses real errors from reaching Sentry or developer tools, making production issues invisible. See also ISSUE-120 (the polling suppression interval that triggers this). + +## Options + +1. Remove the global override in production entirely (allow real errors through to Sentry). +2. Scope suppression to the specific known Emotion/MUI warning string only. +3. Keep dev-only suppression, none in production. + +## Recommendation + +Remove global suppression in production and, at most, filter the one known benign warning by message substring in development. Coordinate with ISSUE-120 since it is the same script. + +## Affected Files + +- `frontend/src/app/layout.tsx:95` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-10 +- [[ISSUE-120-frontend-50ms-setinterval-console-suppression-script-in-root-l|ISSUE-120]] diff --git a/Issues/ISSUE-085-frontend-token-refresh-queue-dispatches-with-undefined-authori.md b/Issues/ISSUE-085-frontend-token-refresh-queue-dispatches-with-undefined-authori.md new file mode 100644 index 0000000..d3e3805 --- /dev/null +++ b/Issues/ISSUE-085-frontend-token-refresh-queue-dispatches-with-undefined-authori.md @@ -0,0 +1,38 @@ +--- +issue: 085 +title: "Frontend: token refresh queue dispatches with undefined Authorization header" +severity: medium +domain: Authentication +labels: [bug, frontend, session] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Frontend: token refresh queue dispatches with undefined Authorization header + +**Severity:** medium +**Domain:** Authentication +**Labels:** bug, frontend, session + +## Description + +`src/lib/axios.ts:136` flushes queued requests after a refresh attempt unconditionally. When the refresh yields no token (expired session, network error), queued requests are dispatched with `Authorization: Bearer undefined`, which backend middleware treats as an invalid token, causing all queued requests to fail with 401 — but no logout or error surfacing occurs. + +## Options + +1. On no token: reject queued requests (fail fast) and trigger logout/redirect. +2. Skip the `forEach` when `newAccessToken` is falsy and let requests retry later. +3. Move the `forEach` inside the `if(newAccessToken)` guard and reject the queue in the `else` branch. + +## Recommendation + +Move flush inside the token guard and explicitly reject queued callbacks so they error rather than retry with `'Bearer undefined'`. + +## Affected Files + +- `frontend/src/lib/axios.ts:136` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-11 diff --git a/Issues/ISSUE-086-frontend-paymentdetailsview-status-dropdown-exposed-to-all-use.md b/Issues/ISSUE-086-frontend-paymentdetailsview-status-dropdown-exposed-to-all-use.md new file mode 100644 index 0000000..464c421 --- /dev/null +++ b/Issues/ISSUE-086-frontend-paymentdetailsview-status-dropdown-exposed-to-all-use.md @@ -0,0 +1,39 @@ +--- +issue: 086 +title: "Frontend: PaymentDetailsView status dropdown exposed to all users" +severity: medium +domain: Payment +labels: [security, frontend, authorization] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Frontend: PaymentDetailsView status dropdown exposed to all users + +**Severity:** medium +**Domain:** Payment +**Labels:** security, frontend, authorization + +## Description + +`src/sections/payment/view/payment-details-view.tsx:312` renders a status-change dropdown without an `isAdmin` check. `PaymentDetailsCard` already gates this correctly with `isAdmin`, but the view-level dropdown bypasses that check, allowing any authenticated user to attempt a status change from the UI. + +## Options + +1. Wrap the status `TextField` in an `isAdmin` check mirroring `PaymentDetailsCard`. +2. Hide the control for non-admins and rely on backend role enforcement too. +3. Move status changes to an admin-only view. + +## Recommendation + +Gate the control behind `isAdmin` (as `PaymentDetailsCard` already does) AND ensure backend enforces admin for the underlying route (see ISSUE-062). UI gating alone is insufficient. + +## Affected Files + +- `frontend/src/sections/payment/view/payment-details-view.tsx:312` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-15 +- [[ISSUE-062-backend-payment-update-routes-lack-ownership-role-guards|ISSUE-062]] diff --git a/Issues/ISSUE-087-frontend-getpaymentstatus-and-checkpaymentstatus-hit-different.md b/Issues/ISSUE-087-frontend-getpaymentstatus-and-checkpaymentstatus-hit-different.md new file mode 100644 index 0000000..d2e3b14 --- /dev/null +++ b/Issues/ISSUE-087-frontend-getpaymentstatus-and-checkpaymentstatus-hit-different.md @@ -0,0 +1,38 @@ +--- +issue: 087 +title: "Frontend: getPaymentStatus and checkPaymentStatus hit different endpoints" +severity: medium +domain: Payment +labels: [bug, frontend] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Frontend: getPaymentStatus and checkPaymentStatus hit different endpoints + +**Severity:** medium +**Domain:** Payment +**Labels:** bug, frontend + +## Description + +`src/actions/payment.ts:62` has two functions — `getPaymentStatus` and `checkPaymentStatus` — that appear to serve the same purpose but call different endpoints (`/payment/:id/status` vs `/payment/payments/:id/status`). Only one of these can be the correct backend path. + +## Options + +1. Point `getPaymentStatus` at the registry-defined `/payment/:id/status` and deduplicate with `checkPaymentStatus`. +2. Add `/payment/payments/:id/status` to the endpoints registry if backend truly serves it. +3. Remove the redundant `getPaymentStatus` and migrate callers to `checkPaymentStatus`. + +## Recommendation + +Verify the real backend route, then collapse to a single function using the registry path. Could break callers, so verify before removing. + +## Affected Files + +- `frontend/src/actions/payment.ts:62` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-16 diff --git a/Issues/ISSUE-088-frontend-adminwalletpayout-falls-back-to-literal-admin-string.md b/Issues/ISSUE-088-frontend-adminwalletpayout-falls-back-to-literal-admin-string.md new file mode 100644 index 0000000..399bdfc --- /dev/null +++ b/Issues/ISSUE-088-frontend-adminwalletpayout-falls-back-to-literal-admin-string.md @@ -0,0 +1,38 @@ +--- +issue: 088 +title: "Frontend: adminWalletPayout falls back to literal 'admin' adminUserId" +severity: medium +domain: Payment +labels: [bug, frontend, authorization] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Frontend: adminWalletPayout falls back to literal 'admin' adminUserId + +**Severity:** medium +**Domain:** Payment +**Labels:** bug, frontend, authorization + +## Description + +`src/actions/payment.ts:663` sends `adminUserId: adminUserId || 'admin'`. When the admin user ID is not available (e.g. context not loaded), the string literal `'admin'` is sent to the backend. This may match a user record named 'admin' unintentionally or corrupt audit trails. + +## Options + +1. Require `adminUserId` and throw/abort if absent (no fallback). +2. Source `adminUserId` from the authenticated admin context automatically. +3. Keep a fallback but use the real admin id from token rather than the string `'admin'`. + +## Recommendation + +Remove the `'admin'` literal and require a real admin id from the auth context; abort if unavailable. This affects audit/authorization semantics. + +## Affected Files + +- `frontend/src/actions/payment.ts:663` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-17 diff --git a/Issues/ISSUE-089-frontend-admin-payments-awaiting-confirmation-polls-every-12s.md b/Issues/ISSUE-089-frontend-admin-payments-awaiting-confirmation-polls-every-12s.md new file mode 100644 index 0000000..fafe4b5 --- /dev/null +++ b/Issues/ISSUE-089-frontend-admin-payments-awaiting-confirmation-polls-every-12s.md @@ -0,0 +1,38 @@ +--- +issue: 089 +title: "Frontend: admin payments-awaiting-confirmation polls every 12s unconditionally" +severity: medium +domain: Admin +labels: [performance, frontend, polling] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Frontend: admin payments-awaiting-confirmation polls every 12s unconditionally + +**Severity:** medium +**Domain:** Admin +**Labels:** performance, frontend, polling + +## Description + +`payments-awaiting-confirmation-list-view.tsx:95` polls the backend every 12 seconds regardless of tab visibility or socket connectivity. NB-49 added visibility-gating as a no-brainer, but the longer-term question of whether to replace polling with socket subscriptions remains. + +## Options + +1. Pause polling when `document.visibilityState === 'hidden'` and increase interval (applied via NB-49). +2. Replace polling with a socket subscription for awaiting-confirmation events — best but needs backend events. +3. Both: visibility-gated polling now, socket later. + +## Recommendation + +NB-49 applied the visibility gate. Plan a socket subscription for awaiting-confirmation events to eliminate polling entirely. Confirm acceptable notification latency with owner. + +## Affected Files + +- `frontend/src/sections/admin/payments-awaiting-confirmation/payments-awaiting-confirmation-list-view.tsx:95` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-18 diff --git a/Issues/ISSUE-090-frontend-chat-views-re-fetch-full-conversation-on-every-new-me.md b/Issues/ISSUE-090-frontend-chat-views-re-fetch-full-conversation-on-every-new-me.md new file mode 100644 index 0000000..2d85e32 --- /dev/null +++ b/Issues/ISSUE-090-frontend-chat-views-re-fetch-full-conversation-on-every-new-me.md @@ -0,0 +1,40 @@ +--- +issue: 090 +title: "Frontend: chat views re-fetch full conversation on every new-message socket event" +severity: medium +domain: Chat +labels: [performance, frontend] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Frontend: chat views re-fetch full conversation on every new-message socket event + +**Severity:** medium +**Domain:** Chat +**Labels:** performance, frontend + +## Description + +`src/sections/chat/view/buyer-chat-view.tsx:157` calls the full conversation fetch whenever a `new-message` socket event fires. With the chat messages stored as an embedded array (see ISSUE-072), this re-fetches the entire conversation history on every incoming message, causing high network and backend load in active chats. + +## Options + +1. Append the message from the socket payload to local state; only re-fetch on gaps/errors. +2. Keep re-fetch but debounce it. +3. Hybrid: optimistic append plus periodic reconciliation. + +## Recommendation + +Append the payload message directly and reconcile only on inconsistency. This changes data-flow correctness assumptions. + +## Affected Files + +- `frontend/src/sections/chat/view/buyer-chat-view.tsx:157` +- `frontend/src/sections/chat/view/seller-chat-view.tsx` (similar pattern) + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-20 +- [[ISSUE-072-backend-chat-messages-stored-as-embedded-array-unbounded-growth|ISSUE-072]] diff --git a/Issues/ISSUE-091-frontend-dual-socket-connections-socketprovider-and-socketserv.md b/Issues/ISSUE-091-frontend-dual-socket-connections-socketprovider-and-socketserv.md new file mode 100644 index 0000000..02c5c9f --- /dev/null +++ b/Issues/ISSUE-091-frontend-dual-socket-connections-socketprovider-and-socketserv.md @@ -0,0 +1,39 @@ +--- +issue: 091 +title: "Frontend: dual socket connections (SocketProvider + socketService singleton)" +severity: medium +domain: Realtime +labels: [bug, frontend, performance] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Frontend: dual socket connections (SocketProvider + socketService singleton) + +**Severity:** medium +**Domain:** Realtime +**Labels:** bug, frontend, performance + +## Description + +`src/socket/lib/socket-service.ts:217` creates a standalone socket.io connection separate from the `SocketProvider` context. Both may connect simultaneously, resulting in duplicate connections to the backend, doubled event delivery, and doubled auth overhead. + +## Options + +1. Make `socketService` delegate to the `SocketProvider` connection (single source of truth). +2. Migrate all `actions/chat.ts` usages to the context provider and delete `socketService`. +3. Keep both but ensure only one actually connects. + +## Recommendation + +Consolidate onto `SocketProvider` and refactor `socketService` callers; remove the duplicate connection. This is a large refactor. + +## Affected Files + +- `frontend/src/socket/lib/socket-service.ts:217` +- `frontend/src/actions/chat.ts` — socketService callers + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-21 diff --git a/Issues/ISSUE-092-backend-jwt-refresh-and-access-tokens-share-same-secret.md b/Issues/ISSUE-092-backend-jwt-refresh-and-access-tokens-share-same-secret.md new file mode 100644 index 0000000..4833eab --- /dev/null +++ b/Issues/ISSUE-092-backend-jwt-refresh-and-access-tokens-share-same-secret.md @@ -0,0 +1,39 @@ +--- +issue: 092 +title: "Backend: JWT refresh and access tokens share the same secret; middleware skips token type check" +severity: medium +domain: Authentication +labels: [security, backend, jwt] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Backend: JWT refresh and access tokens share the same secret; middleware skips token type check + +**Severity:** medium +**Domain:** Authentication +**Labels:** security, backend, jwt + +## Description + +`src/services/auth/authService.ts:44` signs both access and refresh tokens with the same secret. `authenticateToken` middleware does not check `token.type`, so a refresh token can be presented as an access token and accepted by protected routes. + +## Options + +1. Add a `type:'access'` claim check in `authenticateToken` middleware (reject `type:'refresh'`). +2. Use separate secrets for access vs refresh tokens. +3. Add audience/issuer claims and verify them in middleware. + +## Recommendation + +Enforce a token-type check in the middleware (reject refresh tokens) and ideally split secrets. Both changes touch core auth verification. + +## Affected Files + +- `backend/src/services/auth/authService.ts:44` +- `backend/src/middleware/authenticateToken.ts` (or equivalent) + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-26 diff --git a/Issues/ISSUE-093-backend-addevidence-no-participant-ownership-check-on-disputes.md b/Issues/ISSUE-093-backend-addevidence-no-participant-ownership-check-on-disputes.md new file mode 100644 index 0000000..e319884 --- /dev/null +++ b/Issues/ISSUE-093-backend-addevidence-no-participant-ownership-check-on-disputes.md @@ -0,0 +1,39 @@ +--- +issue: 093 +title: "Backend: addEvidence has no participant ownership check on disputes" +severity: medium +domain: Dispute +labels: [security, backend, authorization] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Backend: addEvidence has no participant ownership check on disputes + +**Severity:** medium +**Domain:** Dispute +**Labels:** security, backend, authorization + +## Description + +`src/routes/disputeRoutes.ts:32` registers the `addEvidence` route with only `authenticateToken`. Any authenticated user can submit evidence to any dispute, not just the buyer/seller/admin who are participants. + +## Options + +1. Verify `req.user.id` is buyer or seller of the dispute before accepting evidence. +2. Allow admins plus participants only. +3. Add participant check in controller and reject otherwise. + +## Recommendation + +Add a participant (buyer/seller/admin) check in `addEvidence` before persisting. This is an authorization-logic change. + +## Affected Files + +- `backend/src/routes/disputeRoutes.ts:32` +- `backend/src/controllers/disputeController.ts` — `addEvidence` handler + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-27 diff --git a/Issues/ISSUE-094-backend-selectoffer-does-not-verify-buyer-owns-purchase-request.md b/Issues/ISSUE-094-backend-selectoffer-does-not-verify-buyer-owns-purchase-request.md new file mode 100644 index 0000000..cb0eddf --- /dev/null +++ b/Issues/ISSUE-094-backend-selectoffer-does-not-verify-buyer-owns-purchase-request.md @@ -0,0 +1,38 @@ +--- +issue: 094 +title: "Backend: selectOffer does not verify buyer owns the purchase request" +severity: medium +domain: Marketplace +labels: [security, backend, idor] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Backend: selectOffer does not verify buyer owns the purchase request + +**Severity:** medium +**Domain:** Marketplace +**Labels:** security, backend, idor + +## Description + +`src/services/marketplace/marketplaceController.ts:1029` handles `selectOffer` without checking that `req.user.id` matches the `purchaseRequest.buyerId`. Any authenticated user who knows the purchase request ID can select an offer on someone else's request. + +## Options + +1. Reject when `req.user.id !== purchaseRequest.buyerId`. +2. Allow buyer-owner or admin only. +3. Atomic `findOneAndUpdate` scoped by `buyerId`. + +## Recommendation + +Enforce `req.user.id === purchaseRequest.buyerId` (admin override allowed). This changes who can accept offers. + +## Affected Files + +- `backend/src/services/marketplace/marketplaceController.ts:1029` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-28 diff --git a/Issues/ISSUE-095-backend-getuserstats-no-ownership-admin-check-idor.md b/Issues/ISSUE-095-backend-getuserstats-no-ownership-admin-check-idor.md new file mode 100644 index 0000000..de137b1 --- /dev/null +++ b/Issues/ISSUE-095-backend-getuserstats-no-ownership-admin-check-idor.md @@ -0,0 +1,38 @@ +--- +issue: 095 +title: "Backend: getUserStats has no ownership/admin check (IDOR)" +severity: medium +domain: Payment +labels: [security, backend, idor] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Backend: getUserStats has no ownership/admin check (IDOR) + +**Severity:** medium +**Domain:** Payment +**Labels:** security, backend, idor + +## Description + +`paymentControllerRoutes.ts:13` serves `GET /api/payment/stats/:userId` without checking that `req.user.id === req.params.userId` or that the caller is an admin. Any authenticated user can retrieve payment statistics for any other user ID. + +## Options + +1. Require `req.user.id === req.params.userId`, or admin. +2. Admin-only endpoint. +3. Scope query to the authenticated user, ignore param. + +## Recommendation + +Require self-or-admin (`req.user.id === userId || isAdmin`). + +## Affected Files + +- `backend/src/services/payment/paymentControllerRoutes.ts:13` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-29 diff --git a/Issues/ISSUE-096-backend-validatestatustransition-requires-escrowstate-funded-n.md b/Issues/ISSUE-096-backend-validatestatustransition-requires-escrowstate-funded-n.md new file mode 100644 index 0000000..49db75a --- /dev/null +++ b/Issues/ISSUE-096-backend-validatestatustransition-requires-escrowstate-funded-n.md @@ -0,0 +1,38 @@ +--- +issue: 096 +title: "Backend: validateStatusTransition requires escrowState 'funded' never set on completed payments" +severity: medium +domain: Payment +labels: [bug, backend, state-machine] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Backend: validateStatusTransition requires escrowState 'funded' never set on completed payments + +**Severity:** medium +**Domain:** Payment +**Labels:** bug, backend, state-machine + +## Description + +`marketplaceController.ts:570-583` guards a status transition by querying for `{ status:'completed', escrowState:'funded' }`. The completion flow never sets `escrowState:'funded'`; it is set earlier (at funding time). A genuinely completed payment may not match this query, causing the guard to reject valid transitions. + +## Options + +1. Query by `status:'completed'` only (drop `escrowState:'funded'`). +2. Ensure the completion flow sets `escrowState:'funded'` consistently and keep the guard. +3. Match on a documented completed-payment predicate aligned with the actual write path. + +## Recommendation + +Align the guard with what the completion flow actually writes — most safely query `status:'completed'` without the `escrowState` constraint, after confirming no false positives. + +## Affected Files + +- `backend/src/services/marketplace/marketplaceController.ts:570-583` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-34 diff --git a/Issues/ISSUE-097-backend-validtransitions-map-missing-in-negotiation-key.md b/Issues/ISSUE-097-backend-validtransitions-map-missing-in-negotiation-key.md new file mode 100644 index 0000000..161a0f9 --- /dev/null +++ b/Issues/ISSUE-097-backend-validtransitions-map-missing-in-negotiation-key.md @@ -0,0 +1,38 @@ +--- +issue: 097 +title: "Backend: validTransitions map missing 'in_negotiation' key" +severity: medium +domain: Marketplace +labels: [bug, backend, state-machine] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Backend: validTransitions map missing 'in_negotiation' key + +**Severity:** medium +**Domain:** Marketplace +**Labels:** bug, backend, state-machine + +## Description + +`marketplaceController.ts:544-555` defines a `validTransitions` map for PurchaseRequest status transitions but has no entry for `'in_negotiation'`. A PurchaseRequest in the `in_negotiation` state cannot transition to any other state via this validator. + +## Options + +1. Add `'in_negotiation'` with its allowed next statuses (e.g. `payment`, `cancelled`). +2. Treat missing key as 'allow same-tier transitions' default. +3. Derive transitions from `STATUS_PROGRESSION_ORDER` instead of a hand-maintained map. + +## Recommendation + +Add an explicit `'in_negotiation'` entry with the correct next statuses. Requires product/state-machine confirmation of valid transitions. + +## Affected Files + +- `backend/src/services/marketplace/marketplaceController.ts:544-555` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-35 diff --git a/Issues/ISSUE-098-backend-in-memory-seendeliveryids-resets-on-restart.md b/Issues/ISSUE-098-backend-in-memory-seendeliveryids-resets-on-restart.md new file mode 100644 index 0000000..201b5ef --- /dev/null +++ b/Issues/ISSUE-098-backend-in-memory-seendeliveryids-resets-on-restart.md @@ -0,0 +1,38 @@ +--- +issue: 098 +title: "Backend: in-memory seenDeliveryIds resets on restart — webhook dedup lost" +severity: medium +domain: Payment +labels: [bug, backend, idempotency] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Backend: in-memory seenDeliveryIds resets on restart — webhook dedup lost + +**Severity:** medium +**Domain:** Payment +**Labels:** bug, backend, idempotency + +## Description + +`requestNetworkRoutes.ts:16` maintains webhook deduplication via an in-memory `Set` of delivery IDs. This Set is lost on every server restart or pod restart. A redelivered webhook that arrived before the restart will be processed twice, potentially triggering double payment completion. + +## Options + +1. Persist processed delivery IDs in MongoDB (unique index) with TTL. +2. Use Redis SET with TTL for delivery-id dedup. +3. Make webhook handlers idempotent by keying state transitions on payment status guards. + +## Recommendation + +Persist delivery IDs (Mongo unique index or Redis) AND make handlers idempotent via status guards. This is an infra/state decision. + +## Affected Files + +- `backend/src/services/payment/requestNetwork/requestNetworkRoutes.ts:16` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-36 diff --git a/Issues/ISSUE-099-backend-on-demand-rn-reconciliation-in-getpaymentbyid-can-race.md b/Issues/ISSUE-099-backend-on-demand-rn-reconciliation-in-getpaymentbyid-can-race.md new file mode 100644 index 0000000..c6e84e8 --- /dev/null +++ b/Issues/ISSUE-099-backend-on-demand-rn-reconciliation-in-getpaymentbyid-can-race.md @@ -0,0 +1,38 @@ +--- +issue: 099 +title: "Backend: on-demand RN reconciliation in getPaymentById can race — double-processing risk" +severity: medium +domain: Payment +labels: [bug, backend, concurrency] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Backend: on-demand RN reconciliation in getPaymentById can race — double-processing risk + +**Severity:** medium +**Domain:** Payment +**Labels:** bug, backend, concurrency + +## Description + +`paymentController.ts:407-466` triggers RN reconciliation on every `GET /payment/:id` call. If two browser tabs or requests call this concurrently on a pending payment, both can read `status:'pending'` and both trigger the completion side-effects before either write commits. + +## Options + +1. Use an atomic `findOneAndUpdate` guarded on `status:'pending'` so only one writer wins. +2. Add a distributed lock (Redis) around reconciliation per payment. +3. Move reconciliation off the read path into a single-writer background job. + +## Recommendation + +Make the status transition atomic (`findOneAndUpdate` filtering on current status) so only the first concurrent caller advances it; ideally move reconciliation off the GET path. + +## Affected Files + +- `backend/src/services/payment/paymentController.ts:407-466` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-37 diff --git a/Issues/ISSUE-100-backend-updatepurchaserequest-does-findbyid-then-findbyidandupd.md b/Issues/ISSUE-100-backend-updatepurchaserequest-does-findbyid-then-findbyidandupd.md new file mode 100644 index 0000000..3702bb9 --- /dev/null +++ b/Issues/ISSUE-100-backend-updatepurchaserequest-does-findbyid-then-findbyidandupd.md @@ -0,0 +1,38 @@ +--- +issue: 100 +title: "Backend: updatePurchaseRequest does findById then findByIdAndUpdate — non-atomic race" +severity: medium +domain: Marketplace +labels: [bug, backend, concurrency] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Backend: updatePurchaseRequest does findById then findByIdAndUpdate — non-atomic race + +**Severity:** medium +**Domain:** Marketplace +**Labels:** bug, backend, concurrency + +## Description + +`PurchaseRequestService.ts:413` reads the document first (`findById`) to check allowed status transitions, then writes it (`findByIdAndUpdate`). Between the read and the write, another request can change the status, defeating the transition guard. + +## Options + +1. Use `findOneAndUpdate` with `status:{$in:allowedCurrentStatuses}` condition — atomic. +2. Keep two queries but wrap in a transaction. +3. Leave as-is. + +## Recommendation + +Use a single conditional `findOneAndUpdate` to make the transition atomic and halve round-trips. + +## Affected Files + +- `backend/src/services/marketplace/PurchaseRequestService.ts:413` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-46 diff --git a/Issues/ISSUE-101-backend-config-loads-env-development-unconditionally.md b/Issues/ISSUE-101-backend-config-loads-env-development-unconditionally.md new file mode 100644 index 0000000..71c4a80 --- /dev/null +++ b/Issues/ISSUE-101-backend-config-loads-env-development-unconditionally.md @@ -0,0 +1,39 @@ +--- +issue: 101 +title: "Backend: config loads .env.development unconditionally regardless of NODE_ENV" +severity: medium +domain: Security +labels: [security, backend, configuration] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Backend: config loads .env.development unconditionally regardless of NODE_ENV + +**Severity:** medium +**Domain:** Security +**Labels:** security, backend, configuration + +## Description + +`backend/src/shared/config/index.ts:4` loads `.env.development` unconditionally. In a production environment where `NODE_ENV=production`, this still reads and applies `.env.development` values, overriding injected production secrets with development values. Paired with `.dockerignore` whitelisting this file (ISSUE-075), it means dev secrets are active in prod images. + +## Options + +1. Load `.env.` conditionally, never fall back to dev file in production. +2. Only load dotenv when not in production (rely on injected env in prod). +3. Load env-specific file and fail fast if required vars are missing. + +## Recommendation + +Load the env-file matching `NODE_ENV` (or none in production) and never default to `.env.development`. Pair with ISSUE-075. + +## Affected Files + +- `backend/src/shared/config/index.ts:4` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-49 +- [[ISSUE-075-backend-dockerignore-whitelists-env-development-into-prod-image|ISSUE-075]] diff --git a/Issues/ISSUE-102-backend-14-high-severity-npm-vulns-no-audit-step-in-ci.md b/Issues/ISSUE-102-backend-14-high-severity-npm-vulns-no-audit-step-in-ci.md new file mode 100644 index 0000000..91fb329 --- /dev/null +++ b/Issues/ISSUE-102-backend-14-high-severity-npm-vulns-no-audit-step-in-ci.md @@ -0,0 +1,39 @@ +--- +issue: 102 +title: "Backend: 14 high-severity npm vulnerabilities, no audit step in CI" +severity: medium +domain: Dependencies +labels: [security, backend, dependencies, ci-cd] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Backend: 14 high-severity npm vulnerabilities, no audit step in CI + +**Severity:** medium +**Domain:** Dependencies +**Labels:** security, backend, dependencies, ci-cd + +## Description + +`npm audit` reports 14 high-severity vulnerabilities in backend production dependencies (packages include mongoose, multer, axios, and others). No CI pipeline step runs `npm audit`, so new vulnerabilities silently accumulate. + +## Options + +1. Add `npm audit` (or `audit-ci`) as a non-blocking report step first, then make blocking. +2. Upgrade the flagged packages and add a blocking audit gate. +3. Adopt Renovate/Dependabot plus a CI audit step. + +## Recommendation + +Add an audit step (start as report), prioritize upgrading the 14 highs, then make the gate blocking. Package upgrades risk breakage — test before making the gate mandatory. + +## Affected Files + +- `backend/package.json` +- `backend/.woodpecker/development.yml` — add audit step + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-51 diff --git a/Issues/ISSUE-103-backend-react-react-dom-in-backend-production-dependencies.md b/Issues/ISSUE-103-backend-react-react-dom-in-backend-production-dependencies.md new file mode 100644 index 0000000..a6a30b6 --- /dev/null +++ b/Issues/ISSUE-103-backend-react-react-dom-in-backend-production-dependencies.md @@ -0,0 +1,38 @@ +--- +issue: 103 +title: "Backend: react/react-dom in backend production dependencies" +severity: medium +domain: Dependencies +labels: [backend, dependencies, cleanup] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Backend: react/react-dom in backend production dependencies + +**Severity:** medium +**Domain:** Dependencies +**Labels:** backend, dependencies, cleanup + +## Description + +`backend/package.json:83` lists `react` and `react-dom` as production dependencies. These are large packages with no apparent usage in the backend (no SSR email templates confirmed). They inflate the production bundle and increase the attack surface. + +## Options + +1. Remove both after confirming zero imports. +2. Move to `devDependencies` if only used in tooling. +3. Keep if some build step requires them. + +## Recommendation + +Confirm no runtime/SSR usage, then remove. Because removal could break an unseen template render, verify all imports before removing. + +## Affected Files + +- `backend/package.json:83` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-52 diff --git a/Issues/ISSUE-104-backend-bcrypt-native-addon-alongside-used-bcryptjs.md b/Issues/ISSUE-104-backend-bcrypt-native-addon-alongside-used-bcryptjs.md new file mode 100644 index 0000000..474d4c6 --- /dev/null +++ b/Issues/ISSUE-104-backend-bcrypt-native-addon-alongside-used-bcryptjs.md @@ -0,0 +1,38 @@ +--- +issue: 104 +title: "Backend: native bcrypt addon present alongside bcryptjs — unnecessary build toolchain dependency" +severity: medium +domain: Dependencies +labels: [backend, dependencies, cleanup] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Backend: native bcrypt addon present alongside bcryptjs — unnecessary build toolchain dependency + +**Severity:** medium +**Domain:** Dependencies +**Labels:** backend, dependencies, cleanup + +## Description + +`backend/package.json:67` includes `bcrypt` (native C++ addon, requires build toolchain) alongside `bcryptjs` (pure JS). Code uses `bcryptjs`. The native addon adds unnecessary native build complexity and is an unused dependency. + +## Options + +1. Remove `bcrypt` (keep `bcryptjs`) after confirming no imports and no migration need. +2. Standardize on native `bcrypt` instead (faster) and migrate hashes-compatible. +3. Leave both. + +## Recommendation + +Confirm `bcryptjs` is the sole hasher and remove native `bcrypt` to drop the build toolchain requirement. Hashing libs are sensitive — verify before removing. + +## Affected Files + +- `backend/package.json:67` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-53 diff --git a/Issues/ISSUE-105-backend-no-startup-validation-of-required-env-vars.md b/Issues/ISSUE-105-backend-no-startup-validation-of-required-env-vars.md new file mode 100644 index 0000000..c0ba5a3 --- /dev/null +++ b/Issues/ISSUE-105-backend-no-startup-validation-of-required-env-vars.md @@ -0,0 +1,38 @@ +--- +issue: 105 +title: "Backend: no startup validation of required env vars — silent misconfiguration" +severity: medium +domain: Configuration +labels: [backend, reliability, configuration] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Backend: no startup validation of required env vars — silent misconfiguration + +**Severity:** medium +**Domain:** Configuration +**Labels:** backend, reliability, configuration + +## Description + +`backend/src/shared/config/index.ts:32` reads env vars without validating they are present or have correct types. A misconfigured deployment (missing `JWT_SECRET`, `MONGODB_URI`, or `SMTP_PORT`) starts silently and fails only at runtime when those vars are first used, making misconfiguration hard to diagnose. + +## Options + +1. Validate required vars with a schema (zod/envalid) and exit on missing/NaN. +2. Manual assertions for the critical few (`PORT`, `JWT_SECRET`, `MONGODB_URI`, `SMTP_PORT`). +3. Log-and-continue warnings only. + +## Recommendation + +Add schema-based validation that fails fast on missing/invalid required vars. Changes startup behavior. + +## Affected Files + +- `backend/src/shared/config/index.ts:32` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-54 diff --git a/Issues/ISSUE-106-backend-dual-lockfiles-yarn-lock-and-package-lock-json-diverge.md b/Issues/ISSUE-106-backend-dual-lockfiles-yarn-lock-and-package-lock-json-diverge.md new file mode 100644 index 0000000..ae2171d --- /dev/null +++ b/Issues/ISSUE-106-backend-dual-lockfiles-yarn-lock-and-package-lock-json-diverge.md @@ -0,0 +1,41 @@ +--- +issue: 106 +title: "Backend: dual lockfiles (yarn.lock + package-lock.json) diverge" +severity: medium +domain: Dependencies +labels: [backend, ci-cd, dependencies] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Backend: dual lockfiles (yarn.lock + package-lock.json) diverge + +**Severity:** medium +**Domain:** Dependencies +**Labels:** backend, ci-cd, dependencies + +## Description + +`backend/package.json:117` has both `yarn.lock` and `package-lock.json` in the repo, and they are not kept in sync. CI and production use npm; the `packageManager` field references yarn. The two lockfiles represent different resolved dependency trees, so local yarn installs and CI npm installs can diverge. + +## Options + +1. Standardize on npm + `package-lock.json` (matches CI/prod), delete `yarn.lock`, fix `Dockerfile.dev`. +2. Standardize on yarn (matches `packageManager` field), make CI use yarn. +3. Keep both but regenerate and pin. + +## Recommendation + +Pick one (npm matches prod/CI), delete the other lockfile, align Dockerfiles, and regenerate. + +## Affected Files + +- `backend/package.json` +- `backend/yarn.lock` +- `backend/package-lock.json` +- `backend/Dockerfile.dev` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-55 diff --git a/Issues/ISSUE-107-scanner-tronGrid-pagination-next-url-used-unvalidated.md b/Issues/ISSUE-107-scanner-tronGrid-pagination-next-url-used-unvalidated.md new file mode 100644 index 0000000..52ee87f --- /dev/null +++ b/Issues/ISSUE-107-scanner-tronGrid-pagination-next-url-used-unvalidated.md @@ -0,0 +1,38 @@ +--- +issue: 107 +title: "Scanner: TronGrid pagination next-URL used unvalidated — SSRF via API response" +severity: medium +domain: Scanner +labels: [security, scanner, ssrf] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Scanner: TronGrid pagination next-URL used unvalidated — SSRF via API response + +**Severity:** medium +**Domain:** Scanner +**Labels:** security, scanner, ssrf + +## Description + +`scanner/tron_chain.go:180` follows the `Links.Next` URL from a TronGrid API response without validating that it has the same scheme and host as the configured RPC URL. A compromised or malicious TronGrid response can redirect the scanner to arbitrary internal endpoints. + +## Options + +1. Require next URL to share scheme+host with `chain.RpcURL`. +2. Reconstruct pagination params ourselves instead of trusting `Links.Next`. +3. Allowlist the TronGrid host. + +## Recommendation + +Validate scheme+host equals the configured RPC URL before following `Links.Next`. + +## Affected Files + +- `scanner/tron_chain.go:180` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-61 diff --git a/Issues/ISSUE-108-scanner-unauthenticated-startup-when-scanner-api-key-unset.md b/Issues/ISSUE-108-scanner-unauthenticated-startup-when-scanner-api-key-unset.md new file mode 100644 index 0000000..22fbba8 --- /dev/null +++ b/Issues/ISSUE-108-scanner-unauthenticated-startup-when-scanner-api-key-unset.md @@ -0,0 +1,38 @@ +--- +issue: 108 +title: "Scanner: unauthenticated startup when SCANNER_API_KEY unset" +severity: medium +domain: Scanner +labels: [security, scanner, configuration] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Scanner: unauthenticated startup when SCANNER_API_KEY unset + +**Severity:** medium +**Domain:** Scanner +**Labels:** security, scanner, configuration + +## Description + +`scanner/config.go:111` logs a warning when `SCANNER_API_KEY` is empty but allows the server to start and accept unauthenticated requests. An operator mistake or CI misconfiguration can deploy a scanner that accepts any intent without an API key. + +## Options + +1. Fail fast in non-dev when `SCANNER_API_KEY` is empty. +2. Allow empty key only when bound to localhost; refuse otherwise. +3. Keep warning but add a required-in-prod env flag. + +## Recommendation + +Refuse to start (or restrict to loopback) when no API key is set outside local dev. + +## Affected Files + +- `scanner/config.go:111` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-60 diff --git a/Issues/ISSUE-109-scanner-tron-lag-metric-reported-in-ms-not-blocks.md b/Issues/ISSUE-109-scanner-tron-lag-metric-reported-in-ms-not-blocks.md new file mode 100644 index 0000000..564b84a --- /dev/null +++ b/Issues/ISSUE-109-scanner-tron-lag-metric-reported-in-ms-not-blocks.md @@ -0,0 +1,39 @@ +--- +issue: 109 +title: "Scanner: Tron lag metric reported in ms, not blocks — inconsistent with EVM chains" +severity: medium +domain: Scanner +labels: [scanner, observability] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Scanner: Tron lag metric reported in ms, not blocks — inconsistent with EVM chains + +**Severity:** medium +**Domain:** Scanner +**Labels:** scanner, observability + +## Description + +`scanner/api.go:55` reports Tron lag in milliseconds while EVM chains report lag in blocks. Monitoring dashboards and alerts that compare lag across chains will produce incorrect comparisons. + +## Options + +1. Convert Tron lag to blocks (divide by ~3s block time) to match EVM semantics. +2. Keep ms but relabel the field/units and fix the comment and alerts. +3. Report a normalized seconds value across all chains. + +## Recommendation + +Pick a consistent unit (blocks for EVM/Tron, or seconds everywhere), update the struct comment and any alerts. Affects monitoring contracts. + +## Affected Files + +- `scanner/api.go:55` +- Status struct and any Prometheus/monitoring config + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-64 diff --git a/Issues/ISSUE-110-scanner-ton-worker-on-http-fan-out-per-scan-cycle.md b/Issues/ISSUE-110-scanner-ton-worker-on-http-fan-out-per-scan-cycle.md new file mode 100644 index 0000000..eb586f7 --- /dev/null +++ b/Issues/ISSUE-110-scanner-ton-worker-on-http-fan-out-per-scan-cycle.md @@ -0,0 +1,38 @@ +--- +issue: 110 +title: "Scanner: TON worker O(N) HTTP fan-out per scan cycle — one TonCenter call per intent" +severity: medium +domain: Scanner +labels: [performance, scanner, scalability] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Scanner: TON worker O(N) HTTP fan-out per scan cycle — one TonCenter call per intent + +**Severity:** medium +**Domain:** Scanner +**Labels:** performance, scanner, scalability + +## Description + +`scanner/ton_chain.go:131` issues one TonCenter API call per pending intent per scan cycle. With N pending intents, this creates N outbound HTTP calls per cycle. Under load or with many intents, this exhausts outbound connection capacity and hits TonCenter rate limits. + +## Options + +1. Batch intents by destination/jetton and query once per group. +2. Bounded-concurrency worker pool for per-intent calls. +3. Subscribe to TonCenter streaming/index instead of polling. + +## Recommendation + +Batch queries by jetton/destination where the API allows; otherwise bound concurrency. A TODO is already noted in the code. + +## Affected Files + +- `scanner/ton_chain.go:131` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-66 diff --git a/Issues/ISSUE-111-scanner-deliverwebhook-goroutines-use-blocking-time-sleep.md b/Issues/ISSUE-111-scanner-deliverwebhook-goroutines-use-blocking-time-sleep.md new file mode 100644 index 0000000..2dd9b8c --- /dev/null +++ b/Issues/ISSUE-111-scanner-deliverwebhook-goroutines-use-blocking-time-sleep.md @@ -0,0 +1,39 @@ +--- +issue: 111 +title: "Scanner: deliverWebhook goroutines use blocking time.Sleep — goroutine leak under sustained failure" +severity: medium +domain: Scanner +labels: [bug, scanner, goroutine-leak] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Scanner: deliverWebhook goroutines use blocking time.Sleep — goroutine leak under sustained failure + +**Severity:** medium +**Domain:** Scanner +**Labels:** bug, scanner, goroutine-leak + +## Description + +`scanner/webhook.go:90` spawns a goroutine per webhook delivery that uses `time.Sleep` for retry backoff. Under sustained backend failure, many goroutines accumulate blocking on sleep with no upper bound on their count or total memory usage. + +## Options + +1. Replace per-delivery sleeping goroutines with a persisted retry queue + ticker (already partially present). +2. Use a bounded worker pool + context cancellation instead of `time.Sleep`. +3. Cap concurrent in-flight deliveries with a semaphore. + +## Recommendation + +Move retries to the persisted queue/ticker model with a bounded worker pool and context-aware delays. Coordinate with ISSUE-112. + +## Affected Files + +- `scanner/webhook.go:90` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-67 +- [[ISSUE-112-scanner-unbounded-goroutine-fan-out-for-webhook-retries|ISSUE-112]] diff --git a/Issues/ISSUE-112-scanner-unbounded-goroutine-fan-out-for-webhook-retries.md b/Issues/ISSUE-112-scanner-unbounded-goroutine-fan-out-for-webhook-retries.md new file mode 100644 index 0000000..7de3c2c --- /dev/null +++ b/Issues/ISSUE-112-scanner-unbounded-goroutine-fan-out-for-webhook-retries.md @@ -0,0 +1,40 @@ +--- +issue: 112 +title: "Scanner: unbounded goroutine fan-out for webhook retries and reconciliation" +severity: medium +domain: Scanner +labels: [bug, scanner, goroutine-leak] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Scanner: unbounded goroutine fan-out for webhook retries and reconciliation + +**Severity:** medium +**Domain:** Scanner +**Labels:** bug, scanner, goroutine-leak + +## Description + +`scanner/main.go:130` spawns goroutines for retry and reconciliation fan-out without any concurrency bound. Under high load or many failed deliveries, the number of live goroutines is unbounded, risking OOM. + +## Options + +1. Bound with a semaphore/worker pool (e.g. `errgroup` with limit). +2. Process retries in batches sequentially. +3. Rate-limit outbound webhook calls globally. + +## Recommendation + +Introduce a bounded worker pool (`errgroup.SetLimit` or semaphore) for all retry fan-out paths. Coordinate with ISSUE-111. + +## Affected Files + +- `scanner/main.go:130` +- `scanner/webhook.go` — retry fan-out + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-68 +- [[ISSUE-111-scanner-deliverwebhook-goroutines-use-blocking-time-sleep|ISSUE-111]] diff --git a/Issues/ISSUE-113-scanner-rpc-response-bodies-read-without-size-limit-oom.md b/Issues/ISSUE-113-scanner-rpc-response-bodies-read-without-size-limit-oom.md new file mode 100644 index 0000000..64d99d7 --- /dev/null +++ b/Issues/ISSUE-113-scanner-rpc-response-bodies-read-without-size-limit-oom.md @@ -0,0 +1,40 @@ +--- +issue: 113 +title: "Scanner/backend: RPC response bodies read without size limit — OOM risk" +severity: medium +domain: Scanner +labels: [security, scanner, oom] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Scanner/backend: RPC response bodies read without size limit — OOM risk + +**Severity:** medium +**Domain:** Scanner +**Labels:** security, scanner, oom + +## Description + +NB-42 applied a `LimitReader` as a mechanical guard with a default cap, but the exact byte limit per endpoint was not decided. Choosing the wrong cap (too small) breaks legitimate large responses; too large offers little protection. A malicious RPC node can still exhaust memory if the cap is too generous. + +## Options + +1. Wrap `resp.Body` in `io.LimitReader(resp.Body, maxBytes)` with a generous per-endpoint cap (applied as NB-42). +2. Use `http.MaxBytesReader`-style enforcement and error on exceed. +3. Stream-parse JSON with a bounded decoder. + +## Recommendation + +Review the default cap applied by NB-42 against actual maximum RPC response sizes for each chain (EVM batch, Tron page, TON jetton response). Adjust per-endpoint caps and error explicitly when the limit is exceeded rather than silently truncating. + +## Affected Files + +- `scanner/chain.go:96` +- `scanner/tron_chain.go:116` +- `scanner/ton_chain.go:106` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-72 diff --git a/Issues/ISSUE-114-frontend-walletconnect-google-client-ids-hardcoded-dockerfile.md b/Issues/ISSUE-114-frontend-walletconnect-google-client-ids-hardcoded-dockerfile.md new file mode 100644 index 0000000..40dec81 --- /dev/null +++ b/Issues/ISSUE-114-frontend-walletconnect-google-client-ids-hardcoded-dockerfile.md @@ -0,0 +1,38 @@ +--- +issue: 114 +title: "Frontend: WalletConnect/Google client IDs hardcoded as Dockerfile ARG defaults" +severity: low +domain: Security +labels: [frontend, configuration, ci-cd] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Frontend: WalletConnect/Google client IDs hardcoded as Dockerfile ARG defaults + +**Severity:** low +**Domain:** Security +**Labels:** frontend, configuration, ci-cd + +## Description + +`frontend/Dockerfile:14` has hardcoded default values for `NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID` and `NEXT_PUBLIC_GOOGLE_CLIENT_ID` in `ARG` defaults. Forks or copies of this repo will silently use production IDs without being aware. + +## Options + +1. Remove defaults; require build-args/CI to supply them. +2. Keep defaults since values are public-by-design but document them. +3. Move to runtime env only. + +## Recommendation + +Remove the baked defaults and supply via CI build-args to avoid forks reusing prod IDs. These values are public but should be explicit. + +## Affected Files + +- `frontend/Dockerfile:14` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-74 diff --git a/Issues/ISSUE-115-frontend-real-plaintext-credentials-in-committed-scripts.md b/Issues/ISSUE-115-frontend-real-plaintext-credentials-in-committed-scripts.md new file mode 100644 index 0000000..ec18bc2 --- /dev/null +++ b/Issues/ISSUE-115-frontend-real-plaintext-credentials-in-committed-scripts.md @@ -0,0 +1,38 @@ +--- +issue: 115 +title: "Frontend: real plaintext credentials in committed scripts/show-credentials.sh" +severity: low +domain: Security +labels: [security, frontend, secrets, rotation-required] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Frontend: real plaintext credentials in committed scripts/show-credentials.sh + +**Severity:** low +**Domain:** Security +**Labels:** security, frontend, secrets, rotation-required + +## Description + +`frontend/scripts/show-credentials.sh:8` contains hardcoded credentials including the password `Moji6364`. If this account exists in any real environment, the password must be rotated. + +## Options + +1. Delete the scripts and rotate the password if the account is real. +2. Replace hardcoded creds with env-var prompts. +3. Keep scripts but move creds out and rotate. + +## Recommendation + +Remove the hardcoded credentials (use env-var prompts instead) and rotate the account password if it exists in any real environment. + +## Affected Files + +- `frontend/scripts/show-credentials.sh:8` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-75 diff --git a/Issues/ISSUE-116-frontend-backend-scanner-ci-images-not-pinned-to-digests.md b/Issues/ISSUE-116-frontend-backend-scanner-ci-images-not-pinned-to-digests.md new file mode 100644 index 0000000..f064176 --- /dev/null +++ b/Issues/ISSUE-116-frontend-backend-scanner-ci-images-not-pinned-to-digests.md @@ -0,0 +1,44 @@ +--- +issue: 116 +title: "Frontend/scanner/backend: CI pipeline images not pinned to digests — tag-hijack risk" +severity: medium +domain: CI/CD +labels: [security, ci-cd, supply-chain] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Frontend/scanner/backend: CI pipeline images not pinned to digests — tag-hijack risk + +**Severity:** medium +**Domain:** CI/CD +**Labels:** security, ci-cd, supply-chain + +## Description + +CI step images across all three repos use floating version tags or `latest` (node, buildx plugin, curl, alpine). A tag can be replaced with a malicious image that exfiltrates secrets or produces a compromised build artifact. + +NB-40 and NB-41 pinned the scanner `alpine:latest` and buildx plugin. The broader policy of pinning all CI images across all repos remains a decision. + +## Options + +1. Pin all CI images (node, buildx plugin, curl, alpine) to immutable digests — track updates via Renovate. +2. Pin to specific version tags only. +3. Use a vetted internal mirror with digests. + +## Recommendation + +Pin every CI step image to a digest across all pipelines; track updates via Renovate. Affects all CI files in frontend, backend, and scanner. + +## Affected Files + +- `frontend/.woodpecker/development.yml:8` +- `frontend/.woodpecker/production.yml` +- `backend/.woodpecker/development.yml` +- `scanner/.woodpecker/development.yml` +- (and all other pipeline files) + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-76 diff --git a/Issues/ISSUE-117-frontend-backend-scanner-production-manual-ci-pipelines-lack-g.md b/Issues/ISSUE-117-frontend-backend-scanner-production-manual-ci-pipelines-lack-g.md new file mode 100644 index 0000000..a51029c --- /dev/null +++ b/Issues/ISSUE-117-frontend-backend-scanner-production-manual-ci-pipelines-lack-g.md @@ -0,0 +1,41 @@ +--- +issue: 117 +title: "Frontend/scanner/backend: production/manual CI pipelines lack lint/type/test/audit gates" +severity: medium +domain: CI/CD +labels: [ci-cd, quality, supply-chain] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Frontend/scanner/backend: production/manual CI pipelines lack lint/type/test/audit gates + +**Severity:** medium +**Domain:** CI/CD +**Labels:** ci-cd, quality, supply-chain + +## Description + +Production and manual CI pipelines across all three repos push images without the same lint/type/test gates that development pipelines apply. A broken build can be pushed to production via a manual trigger. NB-37 added a typecheck to the backend manual pipeline; the broader question of enforcing gates on all production/manual pipelines remains. + +## Options + +1. Add tsc/lint/test (and `go vet`/`go test` for scanner) to production and manual pipelines. +2. Reuse the development pipeline's gate as a shared step. +3. Block manual pipeline pushes unless a gate flag is passed. + +## Recommendation + +Require the same lint/type/test gate on production and manual pipelines across all repos. This is a known project memory item ("verify before push"). + +## Affected Files + +- `frontend/.woodpecker/production.yml` +- `backend/.woodpecker/manual.yml` +- `scanner/.woodpecker/manual.yml` +- `scanner/.woodpecker/production.yml` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-77 diff --git a/Issues/ISSUE-118-frontend-notification-title-rendered-via-dangerouslysetinnerht.md b/Issues/ISSUE-118-frontend-notification-title-rendered-via-dangerouslysetinnerht.md new file mode 100644 index 0000000..d312233 --- /dev/null +++ b/Issues/ISSUE-118-frontend-notification-title-rendered-via-dangerouslysetinnerht.md @@ -0,0 +1,38 @@ +--- +issue: 118 +title: "Frontend: notification title rendered via dangerouslySetInnerHTML in .backup drawer" +severity: low +domain: Security +labels: [security, frontend, xss, dead-code] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Frontend: notification title rendered via dangerouslySetInnerHTML in .backup drawer + +**Severity:** low +**Domain:** Security +**Labels:** security, frontend, xss, dead-code + +## Description + +`src/layouts/components/notifications-drawer.backup/notification-item.tsx:32` renders a notification title via `dangerouslySetInnerHTML`, creating an XSS sink. The `.backup` directory is likely dead code but may be imported somewhere or re-enabled in the future. + +## Options + +1. Delete the entire `.backup` directory if unused — removes dead code and the XSS sink. +2. Replace `dangerouslySetInnerHTML` with plain text rendering. +3. Keep HTML but sanitize via DOMPurify. + +## Recommendation + +Confirm nothing imports the `.backup` directory and delete it. If any live notification rendering uses `dangerouslySetInnerHTML` elsewhere, switch to text or DOMPurify. + +## Affected Files + +- `frontend/src/layouts/components/notifications-drawer.backup/notification-item.tsx:32` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-5 diff --git a/Issues/ISSUE-119-frontend-telegramdebugpanel-exposed-in-production-via-url-flag.md b/Issues/ISSUE-119-frontend-telegramdebugpanel-exposed-in-production-via-url-flag.md new file mode 100644 index 0000000..5733e28 --- /dev/null +++ b/Issues/ISSUE-119-frontend-telegramdebugpanel-exposed-in-production-via-url-flag.md @@ -0,0 +1,38 @@ +--- +issue: 119 +title: "Frontend: TelegramDebugPanel exposed in production via URL/localStorage flag" +severity: low +domain: Security +labels: [security, frontend, debug-panel] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Frontend: TelegramDebugPanel exposed in production via URL/localStorage flag + +**Severity:** low +**Domain:** Security +**Labels:** security, frontend, debug-panel + +## Description + +`src/components/debug/telegram-debug-panel.tsx:50` is enabled by a URL param or localStorage flag. In production, any user who discovers this flag can activate the debug panel, which exposes internal state including email, wallet, userId, and Telegram session data. + +## Options + +1. Render the panel only when `NODE_ENV !== 'production'` (compile-time) — removes the enumeration surface. +2. Keep runtime flag but redact PII fields (email, wallet, userId). +3. Remove the component from account pages entirely. + +## Recommendation + +Guard rendering on `NODE_ENV !== 'production'` so the flag cannot reveal it in prod builds. + +## Affected Files + +- `frontend/src/components/debug/telegram-debug-panel.tsx:50` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-7 diff --git a/Issues/ISSUE-120-frontend-50ms-setinterval-console-suppression-script-in-root-l.md b/Issues/ISSUE-120-frontend-50ms-setinterval-console-suppression-script-in-root-l.md new file mode 100644 index 0000000..abd77df --- /dev/null +++ b/Issues/ISSUE-120-frontend-50ms-setinterval-console-suppression-script-in-root-l.md @@ -0,0 +1,39 @@ +--- +issue: 120 +title: "Frontend: 50ms setInterval console-suppression script in root layout" +severity: high +domain: Observability +labels: [bug, frontend, logging] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Frontend: 50ms setInterval console-suppression script in root layout + +**Severity:** high +**Domain:** Observability +**Labels:** bug, frontend, logging + +## Description + +`src/app/layout.tsx:139` contains a `setInterval` that repeatedly overrides `console.error`/`console.warn` every 50ms. This creates a recurring CPU microtask throughout the page lifecycle. The goal appears to be silencing an Emotion/MUI SSR warning, but the approach overrides the console globally on every tick. + +## Options + +1. Remove the suppression script entirely and address the underlying Emotion/MUI SSR warning properly. +2. Keep one-time suppression (no interval) gated to development only. +3. Replace with a single non-polling console override applied once at module load. + +## Recommendation + +Remove the polling entirely. If the SSR warning must be silenced, apply a single non-polling override and only in development. Coordinate with ISSUE-084 (console suppression masks prod errors). + +## Affected Files + +- `frontend/src/app/layout.tsx:139` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-9 +- [[ISSUE-084-frontend-console-error-warn-suppression-masks-prod-errors|ISSUE-084]] diff --git a/Issues/ISSUE-121-frontend-transferfunds-and-createpayment-post-to-same-endpoint.md b/Issues/ISSUE-121-frontend-transferfunds-and-createpayment-post-to-same-endpoint.md new file mode 100644 index 0000000..7dd93aa --- /dev/null +++ b/Issues/ISSUE-121-frontend-transferfunds-and-createpayment-post-to-same-endpoint.md @@ -0,0 +1,38 @@ +--- +issue: 121 +title: "Frontend: transferFunds and createPayment POST to the same endpoint" +severity: low +domain: Payment +labels: [bug, frontend] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Frontend: transferFunds and createPayment POST to the same endpoint + +**Severity:** low +**Domain:** Payment +**Labels:** bug, frontend + +## Description + +`src/actions/payment.ts:186` — `transferFunds` and `createPayment` both POST to the same backend endpoint. It is unclear whether this is intentional (payload-shape disambiguation on the backend) or an error where `transferFunds` should use a dedicated route. + +## Options + +1. Give `transferFunds` a dedicated backend route + frontend endpoint constant. +2. Keep shared endpoint but document the backend disambiguation contract. +3. Merge the two functions if they are truly the same operation. + +## Recommendation + +Confirm backend routing intent; if distinct operations, introduce a dedicated endpoint. + +## Affected Files + +- `frontend/src/actions/payment.ts:186` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-14 diff --git a/Issues/ISSUE-122-backend-missing-compound-index-for-seller-visibility-purchase-r.md b/Issues/ISSUE-122-backend-missing-compound-index-for-seller-visibility-purchase-r.md new file mode 100644 index 0000000..931d8d2 --- /dev/null +++ b/Issues/ISSUE-122-backend-missing-compound-index-for-seller-visibility-purchase-r.md @@ -0,0 +1,39 @@ +--- +issue: 122 +title: "Backend: missing compound index for seller-visibility purchase-request query" +severity: low +domain: Marketplace +labels: [performance, backend, database] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Backend: missing compound index for seller-visibility purchase-request query + +**Severity:** low +**Domain:** Marketplace +**Labels:** performance, backend, database + +## Description + +`PurchaseRequestService.ts:267` queries PurchaseRequests for the public marketplace feed filtering by `status` and `isPublic`. Without a compound index, this requires a collection scan on every marketplace page load. + +## Options + +1. Add `{ status:1, isPublic:1 }` (and possibly `createdAt:-1`) compound index. +2. Add a covering index including sort fields. +3. Profile actual query plans first, then index. + +## Recommendation + +Add a `{ status:1, isPublic:1, createdAt:-1 }` compound index after confirming the dominant query shape. Indexing has write-cost trade-offs. + +## Affected Files + +- `backend/src/services/marketplace/PurchaseRequestService.ts:267` +- `backend/src/models/PurchaseRequest.ts` — index definition + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-43 diff --git a/Issues/ISSUE-123-backend-notification-unread-count-chatty-db-access.md b/Issues/ISSUE-123-backend-notification-unread-count-chatty-db-access.md new file mode 100644 index 0000000..c923daf --- /dev/null +++ b/Issues/ISSUE-123-backend-notification-unread-count-chatty-db-access.md @@ -0,0 +1,38 @@ +--- +issue: 123 +title: "Backend: notification unread-count chatty DB access — 3 parallel countDocuments per event" +severity: low +domain: Notification +labels: [performance, backend, database] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Backend: notification unread-count chatty DB access — 3 parallel countDocuments per event + +**Severity:** low +**Domain:** Notification +**Labels:** performance, backend, database + +## Description + +`NotificationService.ts:145` runs 3 `countDocuments` calls per unread-count request. When notification events are frequent, this creates a high volume of DB count queries. Under heavy load, these compete with write operations. + +## Options + +1. Cache per-user unread count in Redis, increment/decrement on events. +2. Coalesce the 3 parallel `countDocuments` into fewer aggregations. +3. Leave as-is (covered by existing index) and only optimize if hot. + +## Recommendation + +Introduce a Redis-backed unread counter updated incrementally; until then, coalesce counts. + +## Affected Files + +- `backend/src/services/notification/NotificationService.ts:145` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-44 diff --git a/Issues/ISSUE-124-backend-per-seller-socket-emit-loop-in-updatepurchaserequeststatu.md b/Issues/ISSUE-124-backend-per-seller-socket-emit-loop-in-updatepurchaserequeststatu.md new file mode 100644 index 0000000..514686a --- /dev/null +++ b/Issues/ISSUE-124-backend-per-seller-socket-emit-loop-in-updatepurchaserequeststatu.md @@ -0,0 +1,38 @@ +--- +issue: 124 +title: "Backend: per-seller socket emit loop in updatePurchaseRequestStatus" +severity: low +domain: Marketplace +labels: [performance, backend, realtime] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Backend: per-seller socket emit loop in updatePurchaseRequestStatus + +**Severity:** low +**Domain:** Marketplace +**Labels:** performance, backend, realtime + +## Description + +`paymentCoordinator.ts:427` emits a socket event to each seller individually in a loop. With many sellers subscribed to a purchase request, this creates N socket emits per status change. A room-based broadcast would emit once. + +## Options + +1. Emit once to a shared request room that sellers join. +2. Keep per-seller but batch/await in parallel. +3. Leave as-is given small N. + +## Recommendation + +Move to a room-based broadcast when convenient; low urgency at current N. + +## Affected Files + +- `backend/src/services/payment/paymentCoordinator.ts:427` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-45 diff --git a/Issues/ISSUE-125-backend-getcategorypath-unbounded-sequential-findbyid-loop.md b/Issues/ISSUE-125-backend-getcategorypath-unbounded-sequential-findbyid-loop.md new file mode 100644 index 0000000..9c2a2f2 --- /dev/null +++ b/Issues/ISSUE-125-backend-getcategorypath-unbounded-sequential-findbyid-loop.md @@ -0,0 +1,38 @@ +--- +issue: 125 +title: "Backend: getCategoryPath unbounded sequential findById loop" +severity: low +domain: Marketplace +labels: [performance, backend] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Backend: getCategoryPath unbounded sequential findById loop + +**Severity:** low +**Domain:** Marketplace +**Labels:** performance, backend + +## Description + +`CategoryService.ts:142` builds a category breadcrumb path by looping and calling `findById` once per ancestor level. For a 5-level category tree, this is 5 sequential DB round-trips per request. + +## Options + +1. Load all categories once (already cached) and walk the tree in memory. +2. Maintain a denormalized `path`/`ancestors` field on each category. +3. Leave as-is for shallow trees. + +## Recommendation + +Build the path from the in-memory category cache; consider a denormalized `ancestors` field for deeper trees. + +## Affected Files + +- `backend/src/services/marketplace/CategoryService.ts:142` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-47 diff --git a/Issues/ISSUE-126-backend-getuserpoints-writes-full-user-document-on-read.md b/Issues/ISSUE-126-backend-getuserpoints-writes-full-user-document-on-read.md new file mode 100644 index 0000000..30043fd --- /dev/null +++ b/Issues/ISSUE-126-backend-getuserpoints-writes-full-user-document-on-read.md @@ -0,0 +1,39 @@ +--- +issue: 126 +title: "Backend: getUserPoints writes full User document on read when points fields missing" +severity: low +domain: Points +labels: [bug, backend, performance] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Backend: getUserPoints writes full User document on read when points fields missing + +**Severity:** low +**Domain:** Points +**Labels:** bug, backend, performance + +## Description + +`PointsService.ts:202` lazy-initializes `points`, `referralCode`, and `referralStats` fields by calling `user.save()` if they are missing. This means every `GET /points` call for a user without these fields triggers a full document write, risking concurrent update conflicts and inflating write load. + +## Options + +1. Initialize these fields at user creation (migration + signup default) so reads never write. +2. Use a targeted `$set` update instead of full `save` when missing. +3. Leave lazy init but guard against the referralCode generation loop. + +## Recommendation + +Initialize `points`/`referralCode`/`referralStats` at signup (plus a one-time backfill migration) so GETs never trigger writes. + +## Affected Files + +- `backend/src/services/points/PointsService.ts:202` +- `backend/src/services/auth/authService.ts` — user creation path + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-48 diff --git a/Issues/ISSUE-127-scanner-get-intents-id-exposes-salt-and-callbackurl.md b/Issues/ISSUE-127-scanner-get-intents-id-exposes-salt-and-callbackurl.md new file mode 100644 index 0000000..0bee0f0 --- /dev/null +++ b/Issues/ISSUE-127-scanner-get-intents-id-exposes-salt-and-callbackurl.md @@ -0,0 +1,38 @@ +--- +issue: 127 +title: "Scanner: GET /intents/:id exposes salt and callbackUrl in response" +severity: low +domain: Scanner +labels: [security, scanner, information-disclosure] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Scanner: GET /intents/:id exposes salt and callbackUrl in response + +**Severity:** low +**Domain:** Scanner +**Labels:** security, scanner, information-disclosure + +## Description + +`scanner/api.go:260` returns the full intent struct including `salt` (used in payment reference derivation) and `callbackUrl` (internal backend endpoint). Both are internal implementation details that should not be exposed to callers. + +## Options + +1. Tag `salt` and `callbackUrl` with `json:"-"` and return a sanitized DTO. +2. Return them only to admin/privileged callers. +3. Keep `callbackUrl` but always hide `salt`. + +## Recommendation + +Return a sanitized DTO that omits `salt` and `callbackUrl`; both are internal. Response-shape change may affect existing callers. + +## Affected Files + +- `scanner/api.go:260` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-59 diff --git a/Issues/ISSUE-128-scanner-post-intents-returns-200-instead-of-201.md b/Issues/ISSUE-128-scanner-post-intents-returns-200-instead-of-201.md new file mode 100644 index 0000000..542823a --- /dev/null +++ b/Issues/ISSUE-128-scanner-post-intents-returns-200-instead-of-201.md @@ -0,0 +1,38 @@ +--- +issue: 128 +title: "Scanner: POST /intents returns 200 instead of 201 for new resource creation" +severity: low +domain: Scanner +labels: [scanner, api-contract] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Scanner: POST /intents returns 200 instead of 201 for new resource creation + +**Severity:** low +**Domain:** Scanner +**Labels:** scanner, api-contract + +## Description + +`scanner/api.go:234` returns HTTP 200 for both new intent creation and idempotent replays. REST convention is 201 for new resource creation and 200 for idempotent replays. Clients that check status codes to distinguish creation from replay cannot do so currently. + +## Options + +1. Return 201 on new creation, 200 on idempotent replay. +2. Always 201. +3. Add a header/body flag indicating replay vs new. + +## Recommendation + +Return 201 for new resources and 200 for idempotent replays. Could affect clients keyed on status codes. + +## Affected Files + +- `scanner/api.go:234` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-63 diff --git a/Issues/ISSUE-129-scanner-ton-processTransfer-doesnt-verify-jettonmasteraddress.md b/Issues/ISSUE-129-scanner-ton-processTransfer-doesnt-verify-jettonmasteraddress.md new file mode 100644 index 0000000..07cb02b --- /dev/null +++ b/Issues/ISSUE-129-scanner-ton-processTransfer-doesnt-verify-jettonmasteraddress.md @@ -0,0 +1,38 @@ +--- +issue: 129 +title: "Scanner: TON processTransfer doesn't verify JettonMasterAddress vs intent.TokenAddress" +severity: low +domain: Scanner +labels: [bug, scanner, token-verification] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Scanner: TON processTransfer doesn't verify JettonMasterAddress vs intent.TokenAddress + +**Severity:** low +**Domain:** Scanner +**Labels:** bug, scanner, token-verification + +## Description + +`scanner/ton_chain.go:203` processes TON jetton transfers without explicitly verifying that `tr.JettonMasterAddress` equals `intent.TokenAddress`. It trusts the API filtering to return only the correct jetton, but a compromised API or a jetton with the same wallet address could pass silently. + +## Options + +1. Assert `tr.JettonMasterAddress === intent.TokenAddress` before confirming. +2. Trust API filtering but log mismatches. +3. Verify and reject on mismatch. + +## Recommendation + +Add an explicit equality check and reject mismatches rather than trusting API filtering. + +## Affected Files + +- `scanner/ton_chain.go:203` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-65 diff --git a/Issues/ISSUE-130-scanner-config-getchaingettokengetrpc-on-linear-scans.md b/Issues/ISSUE-130-scanner-config-getchaingettokengetrpc-on-linear-scans.md new file mode 100644 index 0000000..e078dcc --- /dev/null +++ b/Issues/ISSUE-130-scanner-config-getchaingettokengetrpc-on-linear-scans.md @@ -0,0 +1,38 @@ +--- +issue: 130 +title: "Scanner: Config.GetChain/GetToken/GetRPC O(N) linear scans — pre-index at load time" +severity: low +domain: Scanner +labels: [performance, scanner] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Scanner: Config.GetChain/GetToken/GetRPC O(N) linear scans — pre-index at load time + +**Severity:** low +**Domain:** Scanner +**Labels:** performance, scanner + +## Description + +`scanner/config.go:199` implements `GetChain`, `GetToken`, and `GetRPC` as linear scans over slices on every call. While chain count is small, these are called in hot paths (per-intent lookups). Pre-building maps at config load would eliminate all repeated scans. + +## Options + +1. Build maps (by chainId, token key) once at config load. +2. Leave as-is given small chain count. +3. Cache results per request. + +## Recommendation + +Build lookup maps at config load; trivial change but slightly alters config initialization. + +## Affected Files + +- `scanner/config.go:199` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-69 diff --git a/Issues/ISSUE-131-scanner-tron-ton-workers-dont-share-http-transport.md b/Issues/ISSUE-131-scanner-tron-ton-workers-dont-share-http-transport.md new file mode 100644 index 0000000..84b5305 --- /dev/null +++ b/Issues/ISSUE-131-scanner-tron-ton-workers-dont-share-http-transport.md @@ -0,0 +1,39 @@ +--- +issue: 131 +title: "Scanner: Tron/TON workers don't share HTTP transport — no connection reuse" +severity: low +domain: Scanner +labels: [performance, scanner] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Scanner: Tron/TON workers don't share HTTP transport — no connection reuse + +**Severity:** low +**Domain:** Scanner +**Labels:** performance, scanner + +## Description + +`scanner/tron_chain.go:38` creates separate `http.Client` instances per worker without a shared `http.Transport`. This means TCP connections to the same host are not reused across workers, increasing latency and connection overhead. + +## Options + +1. Use a shared `http.Client`/`Transport` with `MaxIdleConnsPerHost` set. +2. Per-worker clients but with explicit transport tuning. +3. Leave as-is. + +## Recommendation + +Share a single tuned transport across workers for connection reuse. + +## Affected Files + +- `scanner/tron_chain.go:38` +- `scanner/ton_chain.go` — similar pattern + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-70 diff --git a/Issues/ISSUE-132-scanner-evm-checkpoint-saved-every-2000-block-chunk.md b/Issues/ISSUE-132-scanner-evm-checkpoint-saved-every-2000-block-chunk.md new file mode 100644 index 0000000..2cfcac1 --- /dev/null +++ b/Issues/ISSUE-132-scanner-evm-checkpoint-saved-every-2000-block-chunk.md @@ -0,0 +1,38 @@ +--- +issue: 132 +title: "Scanner: EVM checkpoint saved every 2000-block chunk — write amplification during catch-up" +severity: low +domain: Scanner +labels: [performance, scanner] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Scanner: EVM checkpoint saved every 2000-block chunk — write amplification during catch-up + +**Severity:** low +**Domain:** Scanner +**Labels:** performance, scanner + +## Description + +`scanner/chain.go:260` saves the checkpoint to SQLite after every 2000-block chunk during catch-up. For a scanner catching up thousands of blocks, this means many small writes per cycle. Saving once per successful scan cycle (or every large N during deep catch-up) would reduce write load. + +## Options + +1. Save checkpoint only at end of a successful scan cycle. +2. Save every N chunks (larger N) during catch-up. +3. Leave as-is (SQLite WAL is efficient). + +## Recommendation + +Persist the checkpoint once per successful cycle (or every large N during deep catch-up). + +## Affected Files + +- `scanner/chain.go:260` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-71 diff --git a/Issues/ISSUE-133-scanner-ci-buildx-steps-run-privileged-true.md b/Issues/ISSUE-133-scanner-ci-buildx-steps-run-privileged-true.md new file mode 100644 index 0000000..77ff63c --- /dev/null +++ b/Issues/ISSUE-133-scanner-ci-buildx-steps-run-privileged-true.md @@ -0,0 +1,39 @@ +--- +issue: 133 +title: "Scanner: CI buildx steps run privileged: true — evaluate rootless alternative" +severity: low +domain: CI/CD +labels: [security, scanner, ci-cd] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Scanner: CI buildx steps run privileged: true — evaluate rootless alternative + +**Severity:** low +**Domain:** CI/CD +**Labels:** security, scanner, ci-cd + +## Description + +`scanner/.woodpecker/development.yml:23` runs buildx with `privileged: true` for Docker-in-Docker image builds. A privileged CI runner has full access to the host kernel. If a pipeline step is compromised, it can escape the container. + +## Options + +1. Switch to rootless/buildkit without privileged where the runner supports it. +2. Keep privileged but pin the plugin to a digest and restrict secret exposure. +3. Run builds on an isolated runner. + +## Recommendation + +Evaluate a rootless buildkit setup; if infeasible, at minimum pin the plugin digest (applied via NB-41) and isolate the runner. + +## Affected Files + +- `scanner/.woodpecker/development.yml:23` +- `scanner/.woodpecker/production.yml` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-73 diff --git a/Issues/ISSUE-134-frontend-sentry-source-map-upload-configured-but-no-auth-token.md b/Issues/ISSUE-134-frontend-sentry-source-map-upload-configured-but-no-auth-token.md new file mode 100644 index 0000000..9018f32 --- /dev/null +++ b/Issues/ISSUE-134-frontend-sentry-source-map-upload-configured-but-no-auth-token.md @@ -0,0 +1,39 @@ +--- +issue: 134 +title: "Frontend: Sentry source-map upload configured but no auth token injected" +severity: low +domain: Observability +labels: [frontend, configuration] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Frontend: Sentry source-map upload configured but no auth token injected + +**Severity:** low +**Domain:** Observability +**Labels:** frontend, configuration + +## Description + +`frontend/next.config.ts:83` uses `withSentryConfig` with source-map upload enabled, but `SENTRY_AUTH_TOKEN`, `SENTRY_ORG`, and `SENTRY_PROJECT` are not injected in CI. Source maps are not actually uploaded, making stack traces in Sentry unreadable. + +## Options + +1. Inject `SENTRY_AUTH_TOKEN`/`ORG`/`PROJECT` via CI so maps actually upload. +2. Disable `withSentryConfig` upload to save build time if Sentry is unused. +3. Keep config but document that uploads are intentionally off. + +## Recommendation + +Decide whether Sentry is in use: if yes, inject the secrets in CI; if no, disable the upload plugin. + +## Affected Files + +- `frontend/next.config.ts:83` +- `frontend/.woodpecker/production.yml` — CI secrets + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-79 diff --git a/Issues/ISSUE-135-backend-uploads-directory-served-without-authentication.md b/Issues/ISSUE-135-backend-uploads-directory-served-without-authentication.md new file mode 100644 index 0000000..6905b2b --- /dev/null +++ b/Issues/ISSUE-135-backend-uploads-directory-served-without-authentication.md @@ -0,0 +1,41 @@ +--- +issue: 135 +title: "Backend: uploads directory served without authentication — documents guessable by filename" +severity: low +domain: File Management +labels: [security, backend, authorization] +status: open +created: 2026-05-30 +source: Full Codebase Audit 2026-05-30 +--- + +# Backend: uploads directory served without authentication — documents guessable by filename + +**Severity:** low +**Domain:** File Management +**Labels:** security, backend, authorization + +## Description + +`backend/src/app.ts:465` serves the `uploads/` directory as static files without any authentication middleware. Anyone who knows or guesses a filename can download it directly. This affects sensitive documents (ID uploads, delivery evidence) as well as public assets (avatars). + +Additionally, `fileController.ts:146-148` copies files with verbatim original filenames, which are often predictable (e.g. `passport.jpg`). + +## Options + +1. Serve sensitive docs through an authenticated route with ownership checks; keep avatars public. +2. Require signed/expiring URLs for all uploads. +3. Move uploads to object storage with access policies. + +## Recommendation + +Route document downloads through an authenticated, ownership-checked handler (keep public assets like avatars open); consider signed URLs for sensitive files. + +## Affected Files + +- `backend/src/app.ts:465` +- `backend/src/services/file/fileController.ts:146-148` + +## References + +- [Full Codebase Audit 2026-05-30](../09%20-%20Audits/Full%20Codebase%20Audit%20-%202026-05-30.md) — DEC-80 diff --git a/Issues/Issues Index.md b/Issues/Issues Index.md index 73d634b..4b9cb67 100644 --- a/Issues/Issues Index.md +++ b/Issues/Issues Index.md @@ -1,7 +1,7 @@ # Issues Index > Generated from Doc vs Code Audit — 2026-05-29 · last reconciled 2026-05-29 -> **47 open issues** | 🔴 8 critical · 🟠 39 major · 🟡 0 minor · ⚪ 1 invalid (stale audit) · ✅ 6 resolved +> **0 open issues** | 🔴 0 critical · 🟠 0 major · 🟡 0 minor · ⚪ 1 invalid (stale audit) · ✅ 53 resolved ## 🔴 Critical diff --git a/PRD - AML Screening Provider Options.md b/PRD - AML Screening Provider Options.md new file mode 100644 index 0000000..c281a89 --- /dev/null +++ b/PRD - AML Screening Provider Options.md @@ -0,0 +1,222 @@ +# PRD — AML Screening Provider Options + +**Status:** Draft +**Date:** 2026-05-29 +**Context:** Task #10 added `ofacProvider` as the first working free AML provider. The Chainalysis free public API was ruled out (Cloudflare blocks all non-browser IPs). This document maps all options and recommends what to add next at zero cost. + +--- + +## Current State + +| What | Status | +|---|---| +| OFAC SDN provider | ✅ Live (`ofacProvider.ts`) | +| `amlScreeningService.ts` | ✅ Live — pluggable provider interface | +| `amlConfigRoutes.ts` | ✅ Admin can switch provider + force-refresh | +| Chainalysis KYT (paid) | Code ready, key required | +| Chainalysis free public API | ❌ Blocked by Cloudflare on all server IPs | + +`TRANSACTION_SAFETY_AML_PROVIDER=ofac` covers the US regulatory minimum: 97 sanctioned EVM addresses (Tornado Cash, Lazarus Group, sanctioned exchanges). + +--- + +## The Gap + +OFAC alone is narrow. A buyer could be: +- On the **EU**, **UN**, or **UK** sanctions lists but not OFAC +- Flagged by on-chain intelligence (stolen funds, mixer outputs, exploit proceeds) without being formally sanctioned anywhere +- A known scammer address catalogued by community databases + +For a marketplace doing cross-border escrow, the OFAC gap is meaningful and several free alternatives exist. + +--- + +## Option Map + +### Tier 1 — Free, Self-Hosted (Same Pattern as OFAC) + +These are official government-issued sanctions lists available as bulk XML/CSV downloads with no API key and no IP restrictions. Identical implementation pattern to `ofacProvider.ts`. + +#### 1A. EU Consolidated Sanctions List +- **Source:** European External Action Service (EEAS) +- **URL:** `https://webgate.ec.europa.eu/fsd/fsf/public/files/xmlFullSanctionsList_1_1/content` +- **Format:** XML, updated daily +- **EVM addresses:** ~40–80 (growing; includes Hydra Market operators, Garantex exchange) +- **Effort:** ~2h — copy ofacProvider, change regex to match EU XML schema +- **Overlap with OFAC:** ~60% (most major entities are dual-listed) +- **Why add it:** EU operators have a compliance obligation to the EU list independently of OFAC + +#### 1B. UK OFSI Consolidated List +- **Source:** UK Office of Financial Sanctions Implementation +- **URL:** `https://ofsistorage.blob.core.windows.net/publishlive/2022format/ConList.xml` +- **Format:** XML, updated daily +- **EVM addresses:** ~20–40 (post-Brexit separate list; includes Garantex, some Tornado Cash) +- **Effort:** ~2h — same pattern +- **Why add it:** required for any UK-person counterparty + +#### 1C. UN Security Council Consolidated List +- **Source:** UN Security Council +- **URL:** `https://scsanctions.un.org/resources/xml/en/consolidated.xml` +- **Format:** XML, updated ~weekly +- **EVM addresses:** low (~5–15), but includes DPRK entities (Lazarus Group basis) +- **Effort:** ~2h +- **Why add it:** many jurisdictions require UN list compliance as the baseline floor + +--- + +### Tier 2 — Free API Tier (Rate-Limited, No IP Block) + +These providers offer a free developer/community plan with meaningful rate limits. + +#### 2A. GoPlus Security API +- **Endpoint:** `https://api.gopluslabs.io/api/v1/address_security/{address}?chain_id={chainId}` +- **Auth:** None for basic tier (1000 req/day free); API key for higher volume +- **Returns:** `{ malicious_address, phishing_activities, blackmail_activities, stealing_attack, fake_token, honeypot_related_address, ... }` — 15+ risk categories +- **Coverage:** On-chain intelligence across BSC, ETH, Polygon, Arbitrum, Base — exactly our 5 chains +- **IP restrictions:** None — tested from SepehrHomeserverdk ✅ (community-maintained list served normally) +- **Latency:** ~150ms average +- **Effort:** ~3h (new provider class + admin toggle) +- **Why add it:** Catches non-sanctioned but known-bad addresses (drainers, phishing deployers, stolen-fund mixers) that no government list covers +- **Limitation:** Community-sourced, not legally authoritative; use as advisory block, not hard block + +#### 2B. Etherscan Address Tags (ETH only) +- **Endpoint:** `https://api.etherscan.io/api?module=account&action=txlist&address=...` + label check +- **Auth:** Free API key (no credit card, 5 req/s) +- **Returns:** Known scammer/phisher labels from Etherscan's community tag system +- **Coverage:** Ethereum mainnet only +- **Effort:** ~4h (needs scrape of label endpoint, which is unofficial) +- **Why skip for now:** Unofficial, ETH-only, limited EVM address coverage. GoPlus is strictly better. + +--- + +### Tier 3 — Paid / Enterprise + +| Provider | Model | Est. Cost | Notes | +|---|---|---|---| +| **Chainalysis KYT** | Per-request + annual | $10k+/yr | Gold standard; blocked on free tier | +| **TRM Labs** | Per-request | ~$0.05–0.20/check | Developer sandbox available; no IP block | +| **Elliptic** | Per-request | ~$0.10–0.50/check | Strong EU coverage | +| **Sardine** | Per-request | Custom | Adds fraud scoring beyond AML | +| **AMLBot** | Per-request | ~$0.10/check | Telegram-native AML bot; has API | +| **Scorechain** | Annual SaaS | ~$5k+/yr | Good EU/CIS coverage | + +**TRM Labs** is the strongest paid option after Chainalysis — has a developer sandbox, no Cloudflare blocking, and competitive pricing. If a paid provider is chosen later, TRM is the recommended starting point. + +--- + +### Tier 4 — On-Chain / Decentralized + +#### 4A. Forta Network +- Decentralized threat detection; bots emit alerts for known exploiter addresses +- **Free:** Yes, public alert feed via GraphQL +- **Latency:** 1–5 minutes (block-based, not real-time per request) +- **Fit:** Better for monitoring than pre-payment screening; not recommended for this use case + +#### 4B. Harpie Address Blocklist +- Blocklist maintained by Harpie (web3 security); some public exposure +- **Coverage:** Mostly phishing and drainer addresses on ETH +- **API:** Not public/stable +- **Verdict:** Skip — GoPlus covers the same space with a proper API + +--- + +## Recommended Free Tier Additions + +### Phase A — Sanctions breadth (1 day of work) + +Add EU + UN list providers, run all three (OFAC + EU + UN) in parallel, merge results: + +``` +OFAC SDN → 97 EVM addresses +EU List → ~60 EVM addresses +UN SC List → ~15 EVM addresses +───────────────────────────────── +Total unique → ~140–160 addresses (after dedup) +``` + +Implementation: add `euSanctionsProvider.ts` and `unSanctionsProvider.ts` following the exact same pattern as `ofacProvider.ts`. Add a `combinedSanctionsProvider.ts` that fans out to all three in parallel and merges results. Admin can enable `combined` as the provider. + +### Phase B — On-chain intelligence (½ day of work) + +Add `goplusProvider.ts`: +- Checks GoPlus free API for the 15 risk categories +- Returns `clean: false` for `malicious_address`, `phishing_activities`, `stealing_attack`, `blackmail_activities` +- Returns `clean: true` with a warning note for lower-severity flags +- Admin configurable: `advisory` (warn but allow) vs `blocking` mode + +### Composite provider (final step) + +A `compositeProvider.ts` that runs sanctions lists + GoPlus in parallel: +- Any sanctions hit → hard block +- GoPlus malicious hit → configurable (default: hard block) +- GoPlus advisory flag → pass with risk note in payment record + +--- + +## Provider Interface (no changes needed) + +```typescript +// Already in amlProvider.ts — all new providers just implement this +export interface AmlProvider { + name: string; + screenAddress(address: string, chainId?: number): Promise; +} + +export interface AmlScreenResult { + clean: boolean; + verdict: 'clean' | 'sanctions' | 'high-risk' | 'unknown'; + categories?: string[]; + raw?: any; + error?: string; + providerUnavailable?: boolean; + provider: string; +} +``` + +--- + +## Admin Config Changes + +New env vars (none required, all optional): + +```bash +# Provider options (expanded): +# 'none' — disabled +# 'ofac' — OFAC SDN only (current) +# 'combined' — OFAC + EU + UN (Phase A) +# 'goplus' — GoPlus on-chain intelligence only +# 'full' — combined + goplus (Phase A + B) +# 'chainalysis' — paid KYT API +TRANSACTION_SAFETY_AML_PROVIDER=ofac + +# GoPlus (Phase B) +GOPLUS_API_KEY= # optional; upgrades from 1k/day free to higher limit +GOPLUS_BLOCK_THRESHOLD=malicious # or: advisory (warns but allows) + +# EU / UN list URLs (leave blank for defaults) +EU_SANCTIONS_URL= +UN_SANCTIONS_URL= +``` + +--- + +## Priority Recommendation + +| Phase | Work | Benefit | Do it? | +|---|---|---|---| +| Phase A: EU + UN sanctions | ~1 day | Closes EU/UN legal gap | **Yes — do first** | +| Phase B: GoPlus | ~½ day | Catches non-sanctioned bad actors | **Yes — do second** | +| Composite provider | ~½ day | Single toggle for all | **Yes — do with Phase B** | +| TRM Labs paid | ~1 day | Full on-chain risk scoring | If/when compliance requires it | +| Chainalysis KYT | ~1 day | Gold standard + legal defensibility | If enterprise clients demand it | + +Total estimated effort for Phase A + B + Composite: **~2 days**. + +--- + +## Risk Notes + +- **GoPlus community data:** community-sourced, can have false positives. Recommended to run in advisory mode first, switch to blocking after monitoring. +- **Sanctions list lag:** all three government lists are updated daily/weekly; the 24h cache is appropriate. +- **Fail-open policy:** all providers fail-open by default (payment proceeds if provider unreachable). Seller's `amlBlockOnFailure` flag overrides this per-offer. +- **Not legal advice:** this document describes technical options. Jurisdiction-specific compliance obligations (EU's AMLD6, UK's MLR 2017, US BSA) should be reviewed with legal counsel. diff --git a/PRD - Telegram Mini App Bilingual (EN + FA).md b/PRD - Telegram Mini App Bilingual (EN + FA).md new file mode 100644 index 0000000..d29a906 --- /dev/null +++ b/PRD - Telegram Mini App Bilingual (EN + FA).md @@ -0,0 +1,172 @@ +# PRD — Telegram Mini App: Bilingual (EN + FA) + +**Status:** Ready to implement +**Scope:** `src/sections/telegram/telegram-mini-app-shell.tsx` only +**Depends on:** Telegram Mini App redesign (done — commit `integrate-main-into-development`) +**Estimated effort:** 1 day + +--- + +## 1. Goal + +The Telegram Mini App currently renders English only. Persian (Farsi) users — the primary audience — see the app in a language they don't use day-to-day in Telegram. This PRD specifies full EN ↔ FA bilingual support with automatic language detection and a manual toggle. + +--- + +## 2. Language Detection (priority order) + +1. **Telegram's `language_code`** — `shellContext.initDataUnsafe?.user?.language_code` — if `"fa"` or `"fa-IR"`, default to Persian +2. **localStorage key** `amn_tg_lang` — user's last manual selection, persists across sessions +3. **Fallback** — English + +Detection runs once on mount. Manual toggle overwrites and persists to localStorage. + +--- + +## 3. String Inventory (all strings to translate) + +### Loading state +| Key | EN | FA | +|---|---|---| +| `loading.session` | Loading session… | در حال بارگذاری… | +| `loading.signin` | Signing in… | در حال ورود… | + +### Unsupported state +| Key | EN | FA | +|---|---|---| +| `unsupported.badge` | MINI APP ONLY | فقط مینی‌اپ | +| `unsupported.title` | Open in Telegram | در تلگرام باز کنید | +| `unsupported.body` | This view is designed for the Telegram Mini App. Use the web dashboard from a browser. | این صفحه برای مینی‌اپ تلگرام طراحی شده است. از مرورگر به داشبورد وب دسترسی داشته باشید. | +| `unsupported.cta_web` | Open Web Dashboard | باز کردن داشبورد وب | +| `unsupported.cta_signin` | Sign In | ورود | + +### Unlinked / sign-in state +| Key | EN | FA | +|---|---|---| +| `unlinked.badge` | ESCROW · HELD IN TRUST | امانت · در امانت نگه‌داشته | +| `unlinked.body` | Link your Telegram session to access escrow requests, payments, and secure messaging. | حساب تلگرام خود را متصل کنید تا به درخواست‌های امانت، پرداخت‌ها و پیام‌رسانی امن دسترسی داشته باشید. | +| `unlinked.cta_telegram` | Continue with Telegram | ادامه با تلگرام | +| `unlinked.cta_signing_in` | Signing in… | در حال ورود… | +| `unlinked.cta_email` | Sign in with email | ورود با ایمیل | +| `unlinked.cta_create` | Create an account | ساخت حساب | + +### Header +| Key | EN | FA | +|---|---|---| +| `header.subtitle` | MINI APP | مینی‌اپ | + +### Home content +| Key | EN | FA | +|---|---|---| +| `home.banner.badge` | ESCROW ACCOUNT | حساب امانت | +| `home.banner.welcome` | Welcome back, {name} | خوش آمدید، {name} | +| `home.banner.cta` | New Escrow Request | درخواست امانت جدید | +| `home.actions.requests.label` | Open Requests | درخواست‌های باز | +| `home.actions.requests.desc` | Browse and negotiate active escrow requests | مرور و مذاکره درخواست‌های امانت فعال | +| `home.actions.payments.label` | Payments & Escrow | پرداخت‌ها و امانت | +| `home.actions.payments.desc` | Track payments, release funds, view history | پیگیری پرداخت‌ها، آزادسازی وجه، مشاهده تاریخچه | +| `home.actions.chat.label` | Request Chat | گفتگوی درخواست | +| `home.actions.chat.desc` | Continue conversations on active requests | ادامه مکالمات روی درخواست‌های فعال | +| `home.chips.badge` | ESCROW STATES | وضعیت‌های امانت | + +### Status chips (bilingual labels) +| State | EN | FA | +|---|---|---| +| Held | Held | نگهداری شده | +| Shipping | Shipping | در حال ارسال | +| Released | Released | آزاد شد | +| Pending | Pending | در انتظار | +| Disputed | Disputed | اختلاف | +| Cancelled | Cancelled | لغو شد | + +### Tab bar +| Key | EN | FA | +|---|---|---| +| `tabs.home` | Home | خانه | +| `tabs.requests` | Requests | درخواست‌ها | +| `tabs.chat` | Chat | گفتگو | +| `tabs.account` | Account | حساب | + +### MainButton (Telegram native) +| Key | EN | FA | +|---|---|---| +| `main.new_request` | New Request | درخواست جدید | +| `main.sign_in` | Sign In | ورود | + +### Onboarding sheet +| Key | EN | FA | +|---|---|---| +| `onboarding.title` | Account linked | حساب متصل شد | +| `onboarding.subtitle` | Your Telegram session is ready | حساب تلگرام شما آماده است | +| `onboarding.body` | Add your wallet, notification preferences, and payment details any time from Account Settings. | تنظیمات کیف پول، اعلان‌ها و جزئیات پرداخت را هر زمان از تنظیمات حساب اضافه کنید. | +| `onboarding.cta_settings` | Account Settings | تنظیمات حساب | +| `onboarding.cta_later` | Later | بعداً | + +--- + +## 4. Layout Changes for RTL (Persian) + +| Element | LTR | RTL | +|---|---|---| +| Root `dir` attribute | `ltr` | `rtl` | +| Font family | `var(--sans)` = IBM Plex Sans | `var(--sans-fa)` = Vazirmatn | +| Arrow icon in action list | `→` (arrowRight) | `←` (arrowLeft) | +| Tab bar order | Home · Requests · Chat · Account | خانه · درخواست‌ها · گفتگو · حساب (same order, text RTL) | +| Banner welcome text | left-aligned | right-aligned (inherits from dir=rtl) | +| Chip list | left-to-right wrap | right-to-left wrap (inherits) | + +**Font size bumps for Persian:** Body text 13px → 14px, labels 10px → 11px (Vazirmatn renders slightly smaller at the same size). + +--- + +## 5. Language Toggle UI + +A small toggle in the header, right side: + +``` +[ EN | فا ] +``` + +- Two buttons side by side, mono font +- Active language: ink-900 background, cream text +- Inactive: transparent, ink-600 text +- Width: auto (narrow) — doesn't displace the logo +- On tap: haptic light + switch language + persist to localStorage + +--- + +## 6. Translations Object Structure + +```ts +const TR = { + en: { loading: {...}, unsupported: {...}, unlinked: {...}, header: {...}, home: {...}, tabs: {...}, onboarding: {...}, main: {...} }, + fa: { /* same keys */ }, +}; +``` + +All access via `t.home.banner.cta` pattern — no template literals in JSX, only in the translations object. + +--- + +## 7. Files Changed + +| File | Change | +|---|---| +| `src/sections/telegram/telegram-mini-app-shell.tsx` | All string extraction + RTL layout + lang detection + toggle | + +No new files required. No routing changes. No backend changes. + +--- + +## 8. Definition of Done + +- [ ] Language auto-detected from `initDataUnsafe.user.language_code` on mount +- [ ] Manual toggle persists to `localStorage` key `amn_tg_lang` +- [ ] All 40+ strings translated (see table above) +- [ ] `dir="rtl"` applied to root `.tg-shell` when lang is `fa` +- [ ] Vazirmatn font active for Persian (already loaded via Google Fonts in `SHELL_CSS`) +- [ ] Arrow icons flip direction in RTL +- [ ] Tab labels translate correctly +- [ ] Telegram `MainButton` text updates when lang switches +- [ ] No TypeScript errors +- [ ] Tested in browser at `/telegram` with `?lang=fa` param override for dev preview diff --git a/PRD - UI UX Overhaul (Amaneh Design System).md b/PRD - UI UX Overhaul (Amaneh Design System).md new file mode 100644 index 0000000..5cc45e2 --- /dev/null +++ b/PRD - UI UX Overhaul (Amaneh Design System).md @@ -0,0 +1,344 @@ +# PRD — UI/UX Overhaul: Adopting the Amaneh Design System + +**Status:** Planning +**Author:** Claude / Design System by ClaudeDesign +**Date:** 2026-05-29 +**Scope:** Full frontend UI/UX replacement — web app + Telegram Mini App + +--- + +## 1. The Problem + +The current frontend was built on top of MUI v7 with a standard Material Design theme (teal primary `#00A76F`, flat white backgrounds, MUI's default elevation model). It works, but it reads as generic SaaS. The escrow context — trust, money, legal finality — demands a different visual language. Specific pain points: + +| Pain Point | Current State | Why It Matters | +|---|---|---| +| **Color feels cold and institutional** | Teal-green primary, grey surfaces | Escrow is about trust, not tech dashboards | +| **Typography is indistinct** | Almarai/Vazirmatn (UI only) | No hierarchy between headings, data, and UI chrome | +| **Status vocabulary is loose** | MUI semantic colors (`warning`, `info`, `success`) mapped inconsistently to states | Users can't reliably decode what "blue chip" vs "green chip" means | +| **Telegram UI ≈ Web UI** | TelegramMiniAppShell re-uses full MUI dashboard chrome | Telegram has native back button, bottom sheet patterns, safe area insets — these aren't used | +| **No brand identity** | Logo is text-only; no mark, no seal | Escrow platforms need institutional credibility signals | +| **Dark/Light toggle is cosmetic** | Both modes use the same palette logic | Cream paper + warm ink in light mode is more appropriate than flat white | + +--- + +## 2. The Proposed System (ClaudeDesign Brief) + +The `escrowPlatform.zip` design brief introduces **"Amaneh"** — a named, coherent design system with the following pillars: + +### 2.1 Color System + +Replace the current 6-preset MUI palette with a **5-ramp warm earth system**: + +| Ramp | Token | Hex (primary) | Semantic Role | +|---|---|---|---| +| **Cream / Ink** | `--cream-50` / `--ink-900` | `#FBF6EB` / `#1C1410` | Surface + text — warm not white/black | +| **Saffron** | `--saffron-600` | `#C2410C` | Single action color: buttons, links, focus rings | +| **Pistachio** | `--pistachio-700` | `#3D6B4F` | Good / Released / Success states | +| **Persian Blue** | `--persian-700` | `#1F4A8A` | Active / In-transit states | +| **Honey** | `--honey-700` | `#8A6314` | Pending / Warning states | +| **Pomegranate** | `--pomegranate-700` | `#8E2424` | Disputed / Error states | + +**What to drop:** The current 5 color presets (blue, purple, orange, red, green themes) and the settings drawer preset switcher. Single authoritative palette. + +### 2.2 Typography System + +Three-font stack replacing the current single Almarai/Vazirmatn stack: + +| Role | Font | Use | +|---|---|---| +| **Display / Headings** | Source Serif 4 (italic 500–600) | Page titles, section headers, pull quotes | +| **UI / Body** | IBM Plex Sans | All UI chrome, body text, labels | +| **Data / Mono** | IBM Plex Mono | Amounts, hashes, addresses, status codes, table data | +| **Persian headings** | Vazirmatn 700 | RTL headings | +| **Persian body** | Vazirmatn 400 | RTL body text | +| **Persian mono** | IBM Plex Mono | Numbers (works in both scripts) | + +### 2.3 State Vocabulary (Chip System) + +Fixed 1:1 mapping — no more loose MUI semantic color usage: + +| State EN | وضعیت FA | Chip Class | Color | +|---|---|---|---| +| Held | نگهداری شده | `chip--saffron` | Saffron | +| Shipping | در حال ارسال | `chip--active` | Persian Blue | +| Inspection | بازرسی | `chip--active` | Persian Blue | +| Released | آزاد شد | `chip--good` | Pistachio | +| Pending | در انتظار | `chip--warn` | Honey | +| Disputed | اختلاف | `chip--bad` | Pomegranate | +| Cancelled | لغو شد | *(neutral)* | Ink muted | + +### 2.4 Brand Mark + +Three logo options proposed (Seal, Bridge, Knot marks) + serif italic wordmark "amaneh·". Recommendation: **SealMark** (compact, works at 16px favicon and 64px logo) with wordmark for desktop nav. + +### 2.5 Surface Treatment + +- **Background:** `#E7DFCB` (warm parchment) instead of MUI's `#F4F6F8` grey +- **Paper:** `#FBF6EB` (cream) instead of `#FFFFFF` +- **Cards:** 1px `--border-hairline` border, `--shadow-1` (near-hairline) instead of MUI elevation shadows +- **Corner ticks:** optional `.doc-chrome` motif on key document cards (escrow agreements, receipts) +- **Paper grain:** subtle noise texture on dark panels + +--- + +## 3. Telegram UI vs. Web UI — The Differentiation Problem + +### Current State + +The Telegram Mini App (`/telegram` route, `TelegramMiniAppShell`) renders a list of action cards using MUI `Card`, `Button`, `Typography` — identical to the web dashboard. The only Telegram-specific code is safe area insets and theme color attachment. + +### What Telegram Mini Apps Actually Need + +Telegram has its own UX contract: +- **Back button** (hardware/header back) — not a browser back arrow +- **MainButton** — a full-width bottom CTA managed by `Telegram.WebApp.MainButton` +- **Bottom sheets** instead of dialogs +- **Haptic feedback** (`Telegram.WebApp.HapticFeedback`) +- **Native color tokens** (`--tg-theme-bg-color`, `--tg-theme-text-color`, etc.) +- **Touch targets ≥ 48px**, thumb-reachable primary actions +- **No sidebar navigation** — replace with bottom tab bar or header-only nav +- **Compressed information density** — single-column, card-per-item + +### Proposed Telegram-Specific Design Rules + +1. **Layout:** Full-width single column. No sidebar. Header = `←` back + title + optional right action. +2. **Navigation:** Bottom tab bar (4 items max): Dashboard · Requests · Chat · Account +3. **Primary actions:** Use `Telegram.WebApp.MainButton` for the primary CTA on each screen (pay, confirm, release) +4. **Modals → Bottom sheets:** Replace `Dialog` with a swipeable bottom sheet component +5. **Theme tokens:** Consume `--tg-theme-*` CSS vars as overrides on top of the Amaneh palette — so the app looks "at home" in both Telegram light and dark contexts +6. **Haptics:** Add feedback on state-changing taps (confirm, release, dispute) +7. **Font size:** Minimum 16px body (no 12px UI text in Telegram — pinch zoom is disabled) +8. **Status chips:** Same vocabulary as web, but rendered as full-width banners when the status is primary on a screen + +The goal is: the Telegram app should feel like a **Telegram app** that happens to be an escrow tool — not a dashboard stuffed into a WebView. + +--- + +## 4. What Is and Isn't Implemented in the Design Brief + +The ClaudeDesign zip contains: + +| File | What's Implemented | What's Missing | +|---|---|---| +| `styles.css` | Full CSS token layer, all color ramps, typography scale, buttons, chips, inputs, nav items, metric tiles | No responsive breakpoints, no dark mode tokens | +| `foundations.jsx` | Color palette artboard, type specimen, chip vocabulary table | No React component library — just reference renders | +| `dashboard.jsx` | Buyer dashboard mockup (sidebar + topbar + stat tiles + escrow card list) | Not wired to real data; not a component — a single monolith render | +| `auth.jsx` | Sign-in split-screen (form + dark side panel with pull quote) | Sign-up, reset password, verify pages not designed | +| `brand.jsx` | Logo variants (Seal, Bridge, Knot, Stamp), wordmark variants, bilingual lockups | No SVG exports, no favicon spec | +| `app.jsx` | Comparison artboard (before/after) | Not a deliverable component | +| `design-canvas.jsx` | Full design canvas with all artboards stitched together | Not a component — design review surface only | +| `uploads/escrow-design-ui/` | Partial Next.js structure (layouts, sections, nav configs) | Incomplete — missing most section components | + +**Bottom line:** The design system spec is complete and implementable. The React components are scaffolding only. We are building from scratch using the spec as the source of truth. + +--- + +## 5. Implementation Plan + +### Phase 0 — Foundation (Do First, Blocks Everything) + +**Goal:** Replace the token layer without touching any existing component logic. + +1. **Add Google Fonts** — Source Serif 4 + IBM Plex Sans + IBM Plex Mono to `layout.tsx` +2. **Create `src/theme/amaneh-tokens.css`** — Direct port of `styles.css` token layer (CSS vars only, no utility classes) +3. **Update `src/theme/theme-config.ts`** — Wire new color ramps into MUI palette: + - `primary.main` → `#C2410C` (saffron-600) + - `background.default` → `#E7DFCB` + - `background.paper` → `#FBF6EB` + - `text.primary` → `#1C1410` + - Success/Warning/Error/Info → pistachio/honey/pomegranate/persian ramps +4. **Update `typography.ts`** — Set font families to the three-font stack +5. **Remove 5 color presets from settings** — Delete preset switcher; remove from `SettingsDrawer` +6. **Update `components/theme/core/`** — Card, Button, Chip, Input MUI overrides to match Amaneh spec + +**Estimated effort:** 2–3 days. No page-level changes. Immediately visible everywhere. + +--- + +### Phase 1 — Core Components (High Impact, Low Risk) + +**Goal:** Replace the atomic UI elements that appear on every screen. + +| Component | Change | +|---|---| +| `Label` / status chips | Rewrite using Amaneh chip vocabulary (7 states, fixed color mapping) | +| `Logo` | Replace with `SealMark` + serif wordmark | +| Navigation items (sidebar) | Update active state to saffron-50 bg + saffron-700 text | +| Buttons | Saffron primary, ghost/outline variants | +| Card surfaces | Remove MUI elevation, add hairline border + shadow-1 | +| `EmptyContent` | Redesign with Amaneh illustration language | +| `LoadingScreen` / `SplashScreen` | Update to Amaneh brand colors + mark | + +**Estimated effort:** 3–4 days. + +--- + +### Phase 2 — Auth Pages + +**Goal:** Match the ClaudeDesign auth split-screen spec exactly. + +Pages to update: +- `sign-in` — Split screen: dark side panel (ink-900 bg, saffron mark, serif headline, pull-quote) + form panel +- `sign-up` — Same split-screen structure, different side copy +- `reset-password` — Centered form, simplified +- `update-password` — Centered form +- `verify` — Centered with code input + +**Key changes:** +- Dark side panel with paper-grain texture + saffron logo mark +- IBM Plex Mono for auth eyebrow text +- Saffron focus rings on inputs +- Telegram auth button styled distinctly (not identical to Google OAuth) + +**Estimated effort:** 3 days. + +--- + +### Phase 3 — Dashboard & Request Flow + +**Goal:** The most-used screens — buyer/seller dashboard, request list, request detail, payment flow. + +#### Dashboard Overview +- Metric tiles using `.metric` pattern (serif number, mono label) +- Escrow card list using new chip vocabulary +- Sidebar nav with Amaneh active states +- Topbar with IBM Plex Sans, compact search + +#### Request List + Detail +- Table rows with new chip states +- `doc-chrome` corner-tick treatment on the escrow agreement card +- Timeline/activity feed using mono for timestamps, serif for headings +- Action buttons (Release, Dispute, Hold) using saffron primary / pomegranate destructive + +#### Payment Flow +- Step indicators redesigned (saffron active step) +- Amount displays in IBM Plex Mono (tabular nums) +- Chain/network badges using Amaneh chip system + +**Estimated effort:** 5–7 days. + +--- + +### Phase 4 — Telegram Mini App Redesign + +**Goal:** Make the Telegram experience feel native, not a shrunken dashboard. + +#### New Telegram Layout System (`src/layouts/telegram/`) + +``` +TelegramLayout +├── TelegramHeader (title + ← back via WebApp.BackButton) +├── TelegramContent (full-width, scrollable, single-column) +├── TelegramBottomTabBar (4 tabs: Dashboard · Requests · Chat · Account) +└── TelegramMainButton (wrapper for WebApp.MainButton) +``` + +#### Component changes + +| Current | Telegram Replacement | +|---|---| +| `Dialog` | `TelegramBottomSheet` (swipe-down to close) | +| Sidebar nav | `TelegramBottomTabBar` | +| MUI `Card` with padding | Full-bleed touch card (48px+ tap target) | +| Small status chips | Full-width status banner at top of screen when active | +| MUI `Button` primary | `Telegram.WebApp.MainButton` (native Telegram CTA) | +| 14px UI text | 16px minimum everywhere | + +#### Theme token wiring + +```css +/* Telegram theme bridge */ +.telegram-context { + --cream-50: var(--tg-theme-bg-color, #FBF6EB); + --ink-900: var(--tg-theme-text-color, #1C1410); + --saffron-600: var(--tg-theme-button-color, #C2410C); +} +``` + +#### Pages to redesign in Telegram context +1. **Home** — 4-stat summary + active requests list (bottom tab: Dashboard) +2. **Requests** — Swipeable list with status chips (bottom tab: Requests) +3. **Request Detail** — Document-style layout + MainButton for primary action +4. **Chat** — Near-native feel (bottom sheet for attachments) +5. **Account** — Settings list (bottom tab: Account) + +**Estimated effort:** 5–6 days. + +--- + +### Phase 5 — Remaining Pages + +**Goal:** Apply the system uniformly to all remaining pages. + +- Shop pages (public + seller) — warmer e-commerce surface +- Admin pages (networks, trezor, confirmations) — keep dense table layout, apply chip system +- Error pages (403, 404, 500) — Amaneh brand language +- Post/blog — serif display typography prominent +- Checkout (Request Network) — high-trust surface treatment + +**Estimated effort:** 4–5 days. + +--- + +## 6. Scope Boundaries + +### In scope +- Full visual redesign of all pages +- New token layer (`amaneh-tokens.css`) +- MUI theme override updates +- New logo mark + wordmark +- Telegram Mini App layout system rebuild +- Status chip vocabulary standardization + +### Out of scope +- No changes to API contracts or data models +- No changes to auth logic, payment logic, or blockchain interactions +- No component library extraction to separate package (future) +- No Storybook / design token docs tooling (future) + +--- + +## 7. Risk Register + +| Risk | Likelihood | Impact | Mitigation | +|---|---|---|---| +| Font loading causes FOUT/CLS | Medium | High | `font-display: swap` + preconnect hints in `layout.tsx` | +| Saffron CTA conflicts with existing disabled state logic | Low | Medium | Audit all button disabled states in Phase 0 | +| `--tg-theme-*` vars unavailable outside Telegram | Low | Low | CSS var fallbacks already cover this | +| Persian serif (Gulzar) is heavy (3MB+) | High | Medium | Only load for display headings; fallback to Vazirmatn in body | +| Removing 5 color presets breaks user preferences in localStorage | Low | Low | Add migration: wipe `themeColorPreset` from settings on load | +| MUI DataGrid resists custom chip styling | Medium | Low | Use `renderCell` overrides for status columns | + +--- + +## 8. Order of Execution Summary + +``` +Week 1 │ Phase 0: Token layer + theme config + fonts + │ Phase 1: Core components (Logo, Chips, Buttons, Cards) + │ +Week 2 │ Phase 2: Auth pages + │ Phase 3: Dashboard + Request + Payment (start) + │ +Week 3 │ Phase 3: (finish) + │ Phase 4: Telegram Mini App rebuild + │ +Week 4 │ Phase 5: Remaining pages + │ QA pass: RTL, dark mode, Telegram dark theme, mobile responsive +``` + +--- + +## 9. Definition of Done + +- [ ] All 78 routes render with Amaneh palette (no teal `#00A76F` remaining) +- [ ] Status chips use the 7-state fixed vocabulary everywhere +- [ ] IBM Plex Mono is used for all monetary amounts and blockchain addresses +- [ ] Telegram Mini App has bottom tab bar, no sidebar, no MUI dialogs +- [ ] Telegram MainButton is wired for the primary action on each flow screen +- [ ] RTL (Persian) renders correctly with Vazirmatn for body and IBM Plex Mono for numbers +- [ ] No Google Fonts 5-preset switcher in Settings drawer +- [ ] Logo mark (SealMark) appears in nav, auth, and favicon +- [ ] Lighthouse CLS < 0.1 after font additions +- [ ] No accessibility regressions (color contrast AA minimum for all text/background pairs) diff --git a/UAT - Trezor Safekeeping (Task #11).md b/UAT - Trezor Safekeeping (Task #11).md new file mode 100644 index 0000000..14ecf0f --- /dev/null +++ b/UAT - Trezor Safekeeping (Task #11).md @@ -0,0 +1,387 @@ +# UAT — Trezor Safekeeping (Task #11) + +**Feature:** Hardware-protected admin release/refund actions via Trezor signing +**Branch:** `integrate-main-into-development` +**Backend version:** 2.6.59 +**Tester role required:** Admin +**Browser requirement:** Chromium-based only (Chrome, Edge, Brave) — WebUSB is not supported in Firefox or Safari + +--- + +## Overview + +Task #11 adds a hardware security layer over two critical admin actions: + +| Action | Without Trezor | With Trezor | +|---|---|---| +| Release payment to seller | Admin clicks → tx hash → confirm | Admin clicks → Trezor signs → tx hash → confirm | +| Refund payment to buyer | Admin clicks → tx hash → confirm | Admin clicks → Trezor signs → tx hash → confirm | + +The Trezor requirement is controlled by the backend env var `TREZOR_SAFEKEEPING_REQUIRED`. When `false`, the entire Trezor layer is transparent and the UI behaves exactly as before. + +An emergency **break-glass** mechanism lets an admin temporarily bypass Trezor for 1 hour with a mandatory Telegram alarm. + +--- + +## Flows Implemented + +### Flow A — Trezor Registration (one-time setup) + +**URL:** `/dashboard/admin/trezor` +**When:** First time, or when replacing the registered device. + +Steps: +1. Admin opens the Trezor Safekeeping page. +2. (Optional) Admin enters a device label in the text field. +3. Admin clicks **Connect Trezor**. +4. Browser shows a USB device picker — admin selects the Trezor. +5. Trezor device shows "Export public key?" — admin confirms on device. +6. Backend issues a registration message tied to the xpub + address. +7. UI moves to **Sign** step — admin clicks **Sign on Trezor**. +8. Trezor device shows the message to sign — admin confirms on device. +9. UI shows "Signature obtained — ready to register." +10. Admin clicks **Register Trezor** → backend stores xpub fingerprint + registration address. +11. Page refreshes and shows the registered account status (address, fingerprint, path, address count). + +If a Trezor is already registered, the page shows a **Re-register** section instead of the initial registration section. The flow is identical; it overwrites the old record. + +--- + +### Flow B — Release Payment (with Trezor) + +**URL:** `/dashboard/admin/payments-awaiting-confirmation` +**Precondition:** `TREZOR_SAFEKEEPING_REQUIRED=true`, break-glass inactive, Trezor registered. + +Steps: +1. Admin opens the Payments Awaiting Confirmation list. +2. Clicks the green **Release** button on a payment row. +3. Dialog opens at step 1 — **Build Instruction**. + - Shows payment ID and amount. + - Admin clicks **Build Instruction**. +4. Backend builds the release instruction (validates payment state). +5. Dialog advances to step 2 — **Sign with Trezor**. + - Shows provider and network info from the instruction. + - Admin connects Trezor (if not already connected) and clicks **Sign on Trezor**. +6. Trezor device shows the operation message — admin confirms on device. +7. Dialog advances to step 3 — **Enter tx hash**. + - Shows "Trezor approved — signer: 0x…" confirmation. + - Admin pastes the on-chain transaction hash of the actual transfer. + - Admin clicks **Next**. +8. Dialog advances to step 4 — **Confirm**. + - Admin clicks **Confirm Release** (green). +9. Backend verifies the Trezor signature against the registered xpub, records the tx hash, and marks the payment released. +10. Dialog closes, list refreshes. + +--- + +### Flow C — Refund Payment (with Trezor) + +**URL:** `/dashboard/admin/payments-awaiting-confirmation` +**Precondition:** Same as Flow B. + +Identical to Flow B except: +- Admin clicks the orange **Refund** button. +- Step 1 shows an optional **Refund reason** text field. +- Final button is **Confirm Refund** (orange). +- `reason` is passed through to the confirm API if provided. + +--- + +### Flow D — Release / Refund without Trezor (break-glass active or safekeeping disabled) + +**Precondition:** Either `TREZOR_SAFEKEEPING_REQUIRED=false` OR break-glass is active. + +The dialog runs a shorter 3-step flow: + +| Step | Label | Action | +|---|---|---| +| 1 | Build instruction | Same as above | +| 2 | Enter tx hash | No Trezor prompt; shows "Break-glass mode active" notice | +| 3 | Confirm | Submits without `trezor` field in body | + +The backend accepts the request because `isTrezorSafekeepingRequired()` returns `false` in both cases. + +--- + +### Flow E — Break-glass Activation + +**URL:** `/dashboard/admin/trezor` +**When:** Emergency — Trezor is unavailable, lost, or broken. + +Steps: +1. Admin opens the Trezor Safekeeping page. +2. The Break-glass section shows "Trezor safekeeping is active (normal mode)". +3. Admin clicks **Activate Break-glass**. +4. Confirmation dialog appears with warning text. +5. Admin clicks **Activate**. +6. Backend activates break-glass for 1 hour and fires a Telegram alarm to the monitoring bot (`TG_NOTIFY_BOT_TOKEN`). +7. Page shows red alert: "Break-glass is ACTIVE — Activated by: [email] — Expires: [time] — Remaining: ~60 min". +8. The payments-awaiting-confirmation list now passes `skipTrezor={true}` to the dialog. + +--- + +### Flow F — Break-glass Deactivation + +**URL:** `/dashboard/admin/trezor` + +Steps: +1. Admin opens the Trezor Safekeeping page while break-glass is active. +2. The section shows the red active alert with remaining time. +3. Admin clicks **Deactivate Break-glass**. +4. Confirmation dialog appears. +5. Admin clicks **Deactivate**. +6. Backend clears break-glass immediately and fires a Telegram "restored" notification. +7. Page shows green "Trezor safekeeping is active (normal mode)". + +--- + +### Flow G — Break-glass auto-expiry + +No UI action required. + +- After 1 hour, `isBreakGlassActive()` on the backend returns `false` automatically. +- On the next page load or dialog open, `getBreakGlassStatus()` returns `active: false`. +- `skipTrezor` reverts to `false` on the payments list. + +--- + +## API Endpoints + +| Method | Path | Purpose | +|---|---|---| +| `GET` | `/api/trezor/account` | Get registered Trezor status | +| `GET` | `/api/trezor/registration-message` | Get message to sign for registration | +| `POST` | `/api/trezor/register` | Submit registration (xpub + proof signature) | +| `POST` | `/api/trezor/operation-message` | Get message to sign for a release/refund operation | +| `POST` | `/api/payment/:id/release` | Build release instruction | +| `POST` | `/api/payment/:id/release/confirm` | Confirm release with tx hash (+ optional trezor sig) | +| `POST` | `/api/payment/:id/refund` | Build refund instruction | +| `POST` | `/api/payment/:id/refund/confirm` | Confirm refund with tx hash (+ optional trezor sig) | +| `GET` | `/api/admin/settings/break-glass` | Get break-glass status | +| `POST` | `/api/admin/settings/break-glass` | Activate break-glass | +| `DELETE` | `/api/admin/settings/break-glass` | Deactivate break-glass | + +--- + +## Acceptance Tests + +### AT-01 — Page loads and shows unregistered state + +**Precondition:** No Trezor registered in DB. +**Steps:** Navigate to `/dashboard/admin/trezor` as admin. +**Expected:** +- [ ] "No Trezor registered — admin actions are unprotected" warning is shown. +- [ ] "Register Trezor" card with device label field and **Connect Trezor** button is visible. +- [ ] Break-glass card shows "Trezor safekeeping is active (normal mode)" in green. + +--- + +### AT-02 — Registration flow completes (hardware path) + +**Precondition:** Trezor Model One or T connected via USB. Chromium browser. +**Steps:** Follow Flow A above. +**Expected:** +- [ ] USB device picker appears after clicking Connect Trezor. +- [ ] Trezor device shows public key export prompt. +- [ ] After approval, page moves to Sign step and shows the registration message. +- [ ] Trezor device shows the message to sign. +- [ ] After signing, Submit step shows "Signature obtained". +- [ ] After Submit, page shows account status card with green "Trezor registered" alert. +- [ ] Chips show correct registration address (matches what was shown on device), fingerprint, path `m/44'/60'/0'`, and address count `0`. + +--- + +### AT-03 — Registration rejected on device + +**Precondition:** Trezor connected. +**Steps:** Click Connect Trezor → reject the export on device. +**Expected:** +- [ ] Toast shows "Trezor getPublicKey failed: …" or similar error. +- [ ] Registration flow resets to idle (Connect button visible again). + +--- + +### AT-04 — Release without Trezor (safekeeping disabled) + +**Precondition:** Backend `TREZOR_SAFEKEEPING_REQUIRED=false`. +**Steps:** +1. Go to `/dashboard/admin/payments-awaiting-confirmation`. +2. Click Release on any payment with status awaiting confirmation. + +**Expected:** +- [ ] Dialog opens with 3-step stepper (Build instruction / Enter tx hash / Confirm). +- [ ] Build Instruction succeeds without Trezor prompt. +- [ ] "Break-glass mode active — Trezor signature not required." notice shown in step 1. +- [ ] Admin enters a dummy tx hash and clicks Next → Confirm. +- [ ] `POST /api/payment/:id/release/confirm` called with `txHash` but without `trezor` field. +- [ ] Success toast, dialog closes, list refreshes. + +--- + +### AT-05 — Release with Trezor (full hardware path) + +**Precondition:** `TREZOR_SAFEKEEPING_REQUIRED=true`, Trezor registered, break-glass inactive, Trezor device connected. +**Steps:** Follow Flow B above. +**Expected:** +- [ ] Dialog shows 4-step stepper. +- [ ] Sign with Trezor step shows provider/network info from instruction. +- [ ] Trezor device prompts to sign the operation message. +- [ ] After signing, tx hash step shows "Trezor approved — signer: 0x…". +- [ ] Confirm call includes `trezor: { message, signature }` in body. +- [ ] Backend returns 200. Toast: "Release confirmed". + +--- + +### AT-06 — Release blocked when Trezor required but not signed + +**Precondition:** `TREZOR_SAFEKEEPING_REQUIRED=true`, Trezor registered, break-glass inactive. +**Steps:** POST directly to `/api/payment/:id/release/confirm` with only `txHash`, no `trezor` field. +**Expected:** +- [ ] Backend returns 403 or 400 with message indicating Trezor signature is required. + +--- + +### AT-07 — Refund with reason + +**Precondition:** `TREZOR_SAFEKEEPING_REQUIRED=false` (for UI simplicity). +**Steps:** Follow Flow C. Enter "Item not received" in the reason field. +**Expected:** +- [ ] Reason field visible in Build Instruction step. +- [ ] `POST /api/payment/:id/refund/confirm` includes `reason: "Item not received"`. +- [ ] Toast: "Refund confirmed". + +--- + +### AT-08 — Break-glass activation fires Telegram alarm + +**Precondition:** Backend `TG_NOTIFY_BOT_TOKEN` and `TG_NOTIFY_CHAT_ID` set. +**Steps:** Follow Flow E. +**Expected:** +- [ ] Confirmation dialog warns about 1-hour bypass and Telegram alarm. +- [ ] After clicking Activate, page shows red break-glass active alert. +- [ ] Telegram monitoring chat receives alarm message from the notify bot (NOT the mini app bot). +- [ ] `expiresAt` shown is approximately 1 hour from now. +- [ ] `remainingMs` shown is approximately 60 min. + +--- + +### AT-09 — Break-glass makes Trezor step disappear in release dialog + +**Precondition:** `TREZOR_SAFEKEEPING_REQUIRED=true`, break-glass active. +**Steps:** +1. Confirm break-glass is active on Trezor page. +2. Go to payments-awaiting-confirmation. +3. Click Release on any payment. + +**Expected:** +- [ ] Dialog shows 3-step stepper (Build instruction / Enter tx hash / Confirm). +- [ ] No "Sign with Trezor" step visible. +- [ ] Step 1 shows "Break-glass mode active — Trezor signature not required." +- [ ] Release completes without Trezor device. + +--- + +### AT-10 — Break-glass deactivation restores Trezor step + +**Precondition:** Break-glass currently active. +**Steps:** +1. Go to Trezor page and click Deactivate Break-glass → confirm. +2. Go to payments-awaiting-confirmation and click Release. + +**Expected:** +- [ ] Trezor page shows green "Trezor safekeeping is active (normal mode)". +- [ ] Telegram monitoring chat receives "break-glass restored" notification. +- [ ] Release dialog shows 4-step stepper again. + +--- + +### AT-11 — Break-glass expires automatically + +**Precondition:** Break-glass activated. +**Steps:** Wait 1 hour (or manually set `BREAK_GLASS_DURATION_MS` to a short value in `breakGlassRoutes.ts` for testing). +**Expected:** +- [ ] After expiry, `GET /api/admin/settings/break-glass` returns `active: false`. +- [ ] Page refresh shows normal mode. +- [ ] Release dialog shows 4-step stepper. + +--- + +### AT-12 — Non-admin cannot access Trezor endpoints + +**Precondition:** Logged in as buyer or seller. +**Steps:** Call `GET /api/trezor/account` and `GET /api/admin/settings/break-glass` with non-admin JWT. +**Expected:** +- [ ] Both return 403. + +--- + +### AT-13 — Re-registration overwrites old device + +**Precondition:** Trezor already registered. +**Steps:** +1. Note the current registration address on the Trezor page. +2. Click **Register New Device**, complete registration flow with same or different Trezor. + +**Expected:** +- [ ] Page shows updated registration address after re-registration. +- [ ] If different device: new fingerprint, new address. +- [ ] Previous Trezor's signatures are no longer accepted on subsequent releases. + +--- + +### AT-14 — Cancel at any step resets dialog + +**Precondition:** Any. +**Steps:** Open release dialog, complete Build Instruction step, click Cancel. +**Expected:** +- [ ] Dialog closes with no action taken. +- [ ] Reopening the dialog starts at step 1 with empty state. + +--- + +## Test Environment Setup + +### Minimal (no hardware — UI flow only) + +```bash +# backend/.env +TREZOR_SAFEKEEPING_REQUIRED=false +``` + +Covers: AT-01, AT-04, AT-07, AT-08 to AT-10, AT-12, AT-14 + +### With safekeeping enforced (no hardware — break-glass path) + +```bash +# backend/.env +TREZOR_SAFEKEEPING_REQUIRED=true +``` + +1. Go to `/dashboard/admin/trezor` +2. Activate break-glass +3. Now release/refund works without Trezor + +Covers: AT-09, AT-10 + +### Full hardware path + +Requirements: +- Trezor Model One or Model T +- USB cable +- Chromium browser (Chrome / Edge / Brave) +- `TREZOR_SAFEKEEPING_REQUIRED=true` +- Frontend served on `https://` or `localhost` (WebUSB blocks non-secure production origins) + +Covers: AT-02, AT-03, AT-05, AT-06, AT-13 + +--- + +## Known Limitations / Out of Scope + +| Item | Status | +|---|---| +| Firefox / Safari WebUSB support | Not supported — Trezor registration and signing only works in Chromium | +| Multiple Trezor accounts | Only one active registration at a time; re-register to replace | +| Break-glass persistence across backend restarts | Intentionally not persisted — restart clears break-glass as a security property | +| Payout / sweep actions via Trezor | Backend `assertTrezorSignatureForOperation()` is wired; no UI yet | +| Audit log of Trezor operations | Not implemented in this task |