audit: 2026-05-30 full-codebase audit — report, issues, docs, runbooks
Full-codebase-audit 2026-05-30 outputs: - Audit report: 09 - Audits/Full Codebase Audit - 2026-05-30.md - 81 issue files ISSUE-055..135 (decisions + 1 skipped no-brainer). - Scanner docs from scratch (was zero): architecture, data model, API ref, payment flow, operations runbook + repo README. - Doc-sync updates across API reference, data models, flows, design system. - Secret Rotation Runbook (08 - Operations) for the exposed credentials. - Reusable workflow guide (07 - Development) + .claude/workflows/full-codebase-audit.js. Issues remain status:open intentionally — the code fixes are uncommitted-then-committed working-tree changes per repo and aren't "resolved" until merged/deployed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -631,7 +631,7 @@
|
|||||||
{
|
{
|
||||||
"id": 10,
|
"id": 10,
|
||||||
"title": "Implement Telegram as first-class authentication provider",
|
"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.",
|
"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",
|
"status": "done",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
@@ -650,7 +650,7 @@
|
|||||||
"id": "6",
|
"id": "6",
|
||||||
"title": "Request Network in-house checkout (Rabby-supporting)",
|
"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.",
|
"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": "",
|
"testStrategy": "",
|
||||||
"status": "done",
|
"status": "done",
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
@@ -674,7 +674,7 @@
|
|||||||
"id": "7",
|
"id": "7",
|
||||||
"title": "Per-(buyer, sellerOffer) ephemeral RN destination wallets",
|
"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.",
|
"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": "",
|
"testStrategy": "",
|
||||||
"status": "in-progress",
|
"status": "in-progress",
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
@@ -686,7 +686,7 @@
|
|||||||
"id": "8",
|
"id": "8",
|
||||||
"title": "Multichain RN proxy registry + USDC/USDT support",
|
"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.",
|
"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:<id>' 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:<id>' 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": "",
|
"testStrategy": "",
|
||||||
"status": "done",
|
"status": "done",
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
@@ -698,51 +698,55 @@
|
|||||||
"id": "9",
|
"id": "9",
|
||||||
"title": "Per-chain confirmation thresholds + admin UI",
|
"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.",
|
"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:<chainId>' 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:<chainId>' 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": "",
|
"testStrategy": "",
|
||||||
"status": "pending",
|
"status": "done",
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"priority": "medium",
|
"priority": "medium",
|
||||||
"subtasks": []
|
"subtasks": [],
|
||||||
|
"updatedAt": "2026-05-29T09:51:57.565Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "10",
|
"id": "10",
|
||||||
"title": "Optional AML screening on incoming payments (seller-paid)",
|
"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.",
|
"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": "",
|
"testStrategy": "",
|
||||||
"status": "pending",
|
"status": "done",
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"priority": "medium",
|
"priority": "medium",
|
||||||
"subtasks": []
|
"subtasks": [],
|
||||||
|
"updatedAt": "2026-05-29T10:00:28.716Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "11",
|
"id": "11",
|
||||||
"title": "Trezor signing for admin actions (release/refund/sweep)",
|
"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.",
|
"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": "",
|
"testStrategy": "",
|
||||||
"status": "pending",
|
"status": "done",
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
"subtasks": []
|
"subtasks": [],
|
||||||
|
"updatedAt": "2026-05-29T10:50:02.957Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "12",
|
"id": "12",
|
||||||
"title": "Replace auth rate limiter with CAPTCHA (Cloudflare Turnstile or reCAPTCHA v3)",
|
"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": "",
|
"details": "",
|
||||||
"testStrategy": "",
|
"testStrategy": "",
|
||||||
"status": "pending",
|
"status": "in-progress",
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"priority": "medium",
|
"priority": "medium",
|
||||||
"subtasks": []
|
"subtasks": [],
|
||||||
|
"updatedAt": "2026-05-29T11:23:30.368Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "13",
|
"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.",
|
"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.",
|
"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",
|
"priority": "high",
|
||||||
"status": "done",
|
"status": "done",
|
||||||
@@ -753,21 +757,22 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "14",
|
"id": "14",
|
||||||
"title": "Sweep service \u2014 PermitPull + GasTopUp (Kimi, backend@7688f57)",
|
"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 \u2014 Task #11 (Trezor) replaces this.",
|
"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.",
|
"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.",
|
"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",
|
"priority": "high",
|
||||||
"status": "in-progress",
|
"status": "done",
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"subtasks": []
|
"subtasks": [],
|
||||||
|
"updatedAt": "2026-05-29T11:56:24.674Z"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"lastModified": "2026-05-29T08:21:05.470Z",
|
"lastModified": "2026-05-29T11:56:24.675Z",
|
||||||
"taskCount": 12,
|
"taskCount": 14,
|
||||||
"completedCount": 6,
|
"completedCount": 11,
|
||||||
"tags": [
|
"tags": [
|
||||||
"master"
|
"master"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ created: 2026-05-23
|
|||||||
# Roles & Personas
|
# Roles & Personas
|
||||||
|
|
||||||
> [!info] Where roles live in code
|
> [!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
|
```mermaid
|
||||||
flowchart LR
|
flowchart LR
|
||||||
@@ -18,11 +18,13 @@ flowchart LR
|
|||||||
Seller["Seller<br/>(Owner)"]
|
Seller["Seller<br/>(Owner)"]
|
||||||
Support["Support<br/>(admin variant)"]
|
Support["Support<br/>(admin variant)"]
|
||||||
Admin["Admin"]
|
Admin["Admin"]
|
||||||
|
Resolver["Resolver<br/>(dispute specialist)"]
|
||||||
|
|
||||||
Visitor -->|signs up| Buyer
|
Visitor -->|signs up| Buyer
|
||||||
Buyer -->|requests seller mode<br/>+ admin approval| Seller
|
Buyer -->|requests seller mode<br/>+ admin approval| Seller
|
||||||
Buyer & Seller -->|opens ticket| Support
|
Buyer & Seller -->|opens ticket| Support
|
||||||
Support -->|escalates| Admin
|
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/`.
|
- **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-<id>` Socket.IO rooms (`backend/src/app.ts:101-112`).
|
- **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-<id>` 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.
|
- **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.
|
- **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.
|
- **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/request-template` — create / edit shop-scoped templates
|
||||||
- `/dashboard/payment` — receivables, payout history, pending releases
|
- `/dashboard/payment` — receivables, payout history, pending releases
|
||||||
- `/dashboard/disputes` — disputes where the seller is the respondent
|
- `/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
|
> [!tip] See also
|
||||||
> [[Seller Guide]] walks through onboarding, first listing, and payout setup end-to-end. [[Payments Overview]] explains the escrow + payout state machine.
|
> [[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
|
## Cross-cutting concerns
|
||||||
|
|
||||||
### Role transitions
|
### Role transitions
|
||||||
@@ -202,6 +229,7 @@ Support sees a stripped-down admin view focused on the inbox:
|
|||||||
| Anonymous | Buyer | Self-service signup | `User` created |
|
| Anonymous | Buyer | Self-service signup | `User` created |
|
||||||
| Buyer | Seller | Application → admin approval | `User.role` change |
|
| Buyer | Seller | Application → admin approval | `User.role` change |
|
||||||
| Buyer / Seller | Admin | Manual DB / boot-time seed | High-risk, manual |
|
| 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` |
|
| Admin | Support | Permission profile applied at middleware | Role stays `admin` |
|
||||||
|
|
||||||
### Permission model
|
### Permission model
|
||||||
|
|||||||
199
01 - Architecture/Scanner Architecture.md
Normal file
199
01 - Architecture/Scanner Architecture.md
Normal file
@@ -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 <SCANNER_API_KEY>` (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.
|
||||||
49
02 - Data Models/ConfigSettingHistory.md
Normal file
49
02 - Data Models/ConfigSettingHistory.md
Normal file
@@ -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:<chainId>`), 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:<chainId>` 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`
|
||||||
@@ -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.
|
- [[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.
|
- [[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.
|
- [[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:<chainId>`. Added in commit `27fb15a`.
|
||||||
|
|
||||||
## Relationship Diagram
|
## Relationship Diagram
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ aliases: [Purchase Request, Buy Request, IPurchaseRequest]
|
|||||||
|
|
||||||
# PurchaseRequest
|
# 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.
|
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. |
|
| `quantity` | Number | no | `1` | min 1 | — | Unit count. |
|
||||||
| `budget.min` | Number | no | — | min 0 | — | Lower bound. |
|
| `budget.min` | Number | no | — | min 0 | — | Lower bound. |
|
||||||
| `budget.max` | Number | no | — | min 0 | — | Upper 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. |
|
| `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. |
|
| `status` | String | no | `pending` | enum (13 values — see State Transitions below) | yes | Lifecycle state. |
|
||||||
| `isPublic` | Boolean | no | `true` | — | — | Public marketplace listing vs. private request. |
|
| `isPublic` | Boolean | no | `true` | — | — | Public marketplace listing vs. private request. |
|
||||||
|
|||||||
114
02 - Data Models/ScannerIntent.md
Normal file
114
02 - Data Models/ScannerIntent.md
Normal file
@@ -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
|
||||||
@@ -6,7 +6,7 @@ aliases: [Seller Offer, Bid, ISellerOffer]
|
|||||||
|
|
||||||
# SellerOffer
|
# 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`.
|
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. |
|
| `attachments[]` | String[] | no | — | — | — | URLs of supporting files. |
|
||||||
| `notes` | String | no | — | trim | — | Internal/private notes. |
|
| `notes` | String | no | — | trim | — | Internal/private notes. |
|
||||||
| `validUntil` | Date | no | — | — | — | Expiration. |
|
| `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. |
|
| `createdAt` | Date | auto | — | — | yes (desc) | Mongoose timestamp. |
|
||||||
| `updatedAt` | Date | auto | — | — | — | 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.
|
`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
|
## Relationships
|
||||||
|
|
||||||
|
|||||||
@@ -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. |
|
| `password` | String | no | — | minlength 6 | — | Hashed password. Optional to support passkey-only, Google, and Telegram accounts. |
|
||||||
| `firstName` | String | no | `"کاربر"` | trim | — | Persian default ("user"). |
|
| `firstName` | String | no | `"کاربر"` | trim | — | Persian default ("user"). |
|
||||||
| `lastName` | String | no | `"جدید"` | trim | — | Persian default ("new"). |
|
| `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). |
|
| `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`. |
|
| `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`. |
|
| `telegramVerified` | Boolean | no | `false` | — | — | Set when Telegram identity has been signature-verified and linked through `TelegramLink`. |
|
||||||
|
|||||||
@@ -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`).
|
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.
|
API discovery endpoint: `GET /api` → returns a map of available service prefixes.
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,16 @@ tags: [api, admin, reference]
|
|||||||
|
|
||||||
# Admin API
|
# 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:
|
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).
|
- 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).
|
- 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
|
## User management
|
||||||
|
|
||||||
See full descriptions in [[User API]].
|
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
|
### 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
|
## 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 |
|
| `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) |
|
| `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
|
### Confirmation thresholds
|
||||||
|
|
||||||
Frontend page exists. Endpoints require admin auth.
|
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 |
|
| `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 |
|
| `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:<chainId>`.
|
||||||
|
|
||||||
|
### 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
|
## Payments awaiting confirmation
|
||||||
|
|
||||||
@@ -200,6 +229,10 @@ Frontend page exists.
|
|||||||
| Endpoint | Auth | Action |
|
| Endpoint | Auth | Action |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `GET /api/admin/rn/networks` | admin | List all registered RN networks |
|
| `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
|
## Blog admin
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ tags: [api, auth, reference]
|
|||||||
|
|
||||||
# Authentication API
|
# 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).
|
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
|
- `403` email not verified
|
||||||
- `423` account locked (after repeated failures, tracked in Redis via `rateLimitService`)
|
- `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.
|
**⚠️ 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:**
|
**Side effects:**
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ tags: [api, chat, reference]
|
|||||||
|
|
||||||
# Chat API
|
# 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.
|
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-<chatId>`. 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.
|
Model: [[Chat]]. Real-time delivery happens over Socket.IO rooms named `chat-<chatId>`. 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
|
## Rate limits and constraints
|
||||||
|
|||||||
@@ -5,18 +5,21 @@ tags: [api, dispute, reference]
|
|||||||
|
|
||||||
# Dispute API
|
# 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
|
> [!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`
|
- `/api/disputes/*` — `backend/src/routes/disputeRoutes.ts` delegates to `DisputeController` (`backend/src/controllers/disputeController.ts`) for CRUD/triage. All routes apply `authenticateToken` globally.
|
||||||
> 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/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
|
> [!success] Route shadowing resolved (ISSUE-003)
|
||||||
> 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.
|
> 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
|
> [!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.
|
> 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).
|
- 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]]).
|
- 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)
|
**Auth required:** Bearer JWT (buyer who owns the request or admin)
|
||||||
**Request body:** `{ reason?: string }`
|
**Request body:** `{ reason?: string }`
|
||||||
**Response 200:** `{ success, message, data }`
|
**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)
|
**Auth required:** Bearer JWT (buyer, preferred seller, or admin)
|
||||||
|
|
||||||
## Read
|
## Read
|
||||||
@@ -79,7 +84,7 @@ Model: [[Dispute]]. A dispute references a [[PurchaseRequest]] plus optional [[P
|
|||||||
### GET /api/disputes/statistics
|
### GET /api/disputes/statistics
|
||||||
|
|
||||||
**Description:** Aggregated counts (open, by reason, average resolution time) for admin dashboards.
|
**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, ... } }`
|
**Response 200:** `{ success, data: { open, byReason, avgResolutionHours, ... } }`
|
||||||
|
|
||||||
### GET /api/disputes/:id
|
### GET /api/disputes/:id
|
||||||
@@ -92,10 +97,8 @@ Model: [[Dispute]]. A dispute references a [[PurchaseRequest]] plus optional [[P
|
|||||||
|
|
||||||
### POST /api/disputes/:id/assign
|
### POST /api/disputes/:id/assign
|
||||||
|
|
||||||
**Description:** Assign an admin moderator to the dispute. Sets `assignedAdminId` and transitions status to `in_progress`.
|
**Description:** Assign an admin or resolver moderator to the dispute. Sets `assignedAdminId` and transitions status to `in_progress`.
|
||||||
**Auth required:** Bearer JWT
|
**Auth required:** Bearer JWT (`admin` or `resolver`)
|
||||||
|
|
||||||
> ⚠️ **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.
|
|
||||||
|
|
||||||
**Request body:** `{ adminId: string }`
|
**Request body:** `{ adminId: string }`
|
||||||
**Side effects:** Notifies all participants.
|
**Side effects:** Notifies all participants.
|
||||||
@@ -103,18 +106,14 @@ Model: [[Dispute]]. A dispute references a [[PurchaseRequest]] plus optional [[P
|
|||||||
### PATCH /api/disputes/:id/status
|
### PATCH /api/disputes/:id/status
|
||||||
|
|
||||||
**Description:** Generic status update (e.g. close without resolution).
|
**Description:** Generic status update (e.g. close without resolution).
|
||||||
**Auth required:** Bearer JWT
|
**Auth required:** Bearer JWT (`admin` or `resolver`)
|
||||||
|
|
||||||
> ⚠️ **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.
|
|
||||||
|
|
||||||
**Request body:** `{ status: string; note?: string }`
|
**Request body:** `{ status: string; note?: string }`
|
||||||
|
|
||||||
### POST /api/disputes/:id/resolve
|
### POST /api/disputes/:id/resolve
|
||||||
|
|
||||||
**Description:** Final adjudication. Records the decision and triggers the appropriate escrow action.
|
**Description:** Final adjudication. Records the decision and triggers the appropriate escrow action.
|
||||||
**Auth required:** Bearer JWT
|
**Auth required:** Bearer JWT (`admin` or `resolver`)
|
||||||
|
|
||||||
> ⚠️ **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`.
|
|
||||||
|
|
||||||
> ⚠️ **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.
|
> ⚠️ **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 === "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.
|
- `action === "no_action"` or seller-favorable outcome → clear hold only after release checks pass.
|
||||||
- Notifies both participants and updates [[PurchaseRequest]] status to `disputed_resolved`.
|
- 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.
|
**Description:** Lightweight release-hold endpoint that clears the disputed hold flags on a purchase request and related payments.
|
||||||
**Auth required:** Bearer JWT (admin)
|
**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 }`
|
**Response 200:** `{ success, message, data }`
|
||||||
|
|
||||||
|
|||||||
@@ -71,8 +71,8 @@ The buyer-facing CRUD plus seller-side workflow endpoints. Model: [[PurchaseRequ
|
|||||||
size?: string;
|
size?: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
quantity?: number; // default 1
|
quantity?: number; // default 1
|
||||||
budget?: { min?: number; max?: number; currency: "USD" | "EUR" | "IRR" };
|
budget?: { min?: number; max?: number; currency: "USDT" | "USDC" }; // restricted to escrow-compatible stablecoins (commit d52feb7)
|
||||||
urgency?: "low" | "medium" | "high";
|
urgency?: "low" | "medium" | "high" | "urgent";
|
||||||
deliveryInfo?: {
|
deliveryInfo?: {
|
||||||
deliveryType: "physical" | "online";
|
deliveryType: "physical" | "online";
|
||||||
addressId?: string; // when physical
|
addressId?: string; // when physical
|
||||||
@@ -239,7 +239,7 @@ Valid `status` values: `pending | accepted | rejected | withdrawn`
|
|||||||
**Request body:**
|
**Request body:**
|
||||||
```ts
|
```ts
|
||||||
{
|
{
|
||||||
price: { amount: number; currency: "USD" | "EUR" | "IRR" };
|
price: { amount: number; currency: "USDT" }; // USDT only for escrow MVP
|
||||||
deliveryEstimate: { days: number; note?: string };
|
deliveryEstimate: { days: number; note?: string };
|
||||||
notes?: string;
|
notes?: string;
|
||||||
attachments?: string[];
|
attachments?: string[];
|
||||||
@@ -248,6 +248,8 @@ Valid `status` values: `pending | accepted | rejected | withdrawn`
|
|||||||
**Response 201:** `{ success, data: { offer } }`
|
**Response 201:** `{ success, data: { offer } }`
|
||||||
**Side effects:** Emits `new-offer` to `buyer-<buyerId>` and `seller-offer-update` to `seller-<sellerId>`.
|
**Side effects:** Emits `new-offer` to `buyer-<buyerId>` and `seller-offer-update` to `seller-<sellerId>`.
|
||||||
|
|
||||||
|
> **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)
|
### PUT /api/marketplace/purchase-requests/:id/offers (legacy)
|
||||||
|
|
||||||
**Description:** Older offer-update endpoint kept for compatibility.
|
**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.
|
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
|
### PATCH /api/marketplace/offers/:id
|
||||||
|
|
||||||
**Description:** Seller edits their pending offer (price, delivery estimate, notes).
|
**Description:** Seller edits their pending offer (price, delivery estimate, notes).
|
||||||
**Auth required:** Bearer JWT (offer owner)
|
**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
|
### DELETE /api/marketplace/offers/:id
|
||||||
|
|
||||||
@@ -293,9 +298,14 @@ This endpoint does not exist. `getOffersBySeller()` is an internal service metho
|
|||||||
**Auth required:** Bearer JWT
|
**Auth required:** Bearer JWT
|
||||||
**Request body:** `{ status: "pending" | "accepted" | "rejected" | "withdrawn" }`
|
**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
|
### 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)
|
**Auth required:** Bearer JWT (buyer)
|
||||||
**Request body:** `{ offerId: string }`
|
**Request body:** `{ offerId: string }`
|
||||||
**Side effects:**
|
**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.
|
- Emits `seller-offer-update` to all sellers for the request.
|
||||||
|
|
||||||
### POST /api/marketplace/offers/:id/accept (legacy)
|
### POST /api/marketplace/offers/:id/accept (legacy)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ tags: [api, payment, reference, request-network, escrow]
|
|||||||
|
|
||||||
# Payment API
|
# 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:
|
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/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/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/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/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 |
|
| `/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
|
### 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
|
**Auth required:** Bearer JWT
|
||||||
**Request body:**
|
**Request body:**
|
||||||
```ts
|
```ts
|
||||||
@@ -90,7 +91,7 @@ Core model: [[Payment]]. Coordination logic to avoid race conditions when multip
|
|||||||
|
|
||||||
### GET /api/payment/:id
|
### 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
|
**Auth required:** Bearer JWT
|
||||||
**Errors:** `404` not found.
|
**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
|
### POST /api/payment/payments/:id/fetch-tx
|
||||||
|
|
||||||
**Description:** Re-queries the blockchain to fetch the missing `transactionHash` for a completed payment.
|
**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 }`
|
**Response 200:** `{ success, transactionHash, network, source, message }`
|
||||||
|
|
||||||
### POST /api/payment/payments/auto-fetch-missing
|
### POST /api/payment/payments/auto-fetch-missing
|
||||||
|
|
||||||
**Description:** Batch tx-hash backfill across the database.
|
**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)
|
**Request body:** `{ limit?: number }` (default 10)
|
||||||
|
|
||||||
### GET /api/payment/payments/:id/debug
|
### GET /api/payment/payments/:id/debug
|
||||||
|
|
||||||
**Description:** Debug bundle including the raw payment, blockchain metadata, and wallet-monitor status. Intended for admin / development.
|
**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
|
### POST /api/payment/callback
|
||||||
|
|
||||||
@@ -153,9 +154,9 @@ Core model: [[Payment]]. Coordination logic to avoid race conditions when multip
|
|||||||
|
|
||||||
## Request Network - Pay-in
|
## 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)
|
**Auth required:** Bearer JWT (buyer)
|
||||||
**Request body:**
|
**Request body:**
|
||||||
```ts
|
```ts
|
||||||
@@ -182,7 +183,35 @@ Core model: [[Payment]]. Coordination logic to avoid race conditions when multip
|
|||||||
**Auth required:** No (signature-protected)
|
**Auth required:** No (signature-protected)
|
||||||
**Response:** `200` when processed or duplicate; `202` when accepted but safety checks are pending; `401` for invalid signature.
|
**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
|
## 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)
|
## 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
|
## Related
|
||||||
|
|
||||||
|
|||||||
249
03 - API Reference/Scanner API.md
Normal file
249
03 - API Reference/Scanner API.md
Normal file
@@ -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 <SCANNER_API_KEY>` 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 <SCANNER_API_KEY>
|
||||||
|
```
|
||||||
|
|
||||||
|
- 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.
|
||||||
@@ -3,7 +3,7 @@ title: Trezor API
|
|||||||
tags: [api, payments, trezor, safekeeping]
|
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
|
# 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.
|
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
|
## GET /api/trezor/registration-message
|
||||||
|
|
||||||
Builds the exact message the user must sign to register a Trezor xpub.
|
Builds the exact message the user must sign to register a Trezor xpub.
|
||||||
|
|||||||
@@ -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.
|
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.
|
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`.
|
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.
|
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.
|
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=...`.
|
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=...`.
|
||||||
|
|||||||
@@ -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.
|
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
|
> [!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.
|
> 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).
|
- **Seller** — party against whom the dispute is raised (or in rarer cases, initiator).
|
||||||
- **Admin / Mediator** — assigned to investigate.
|
- **Admin / Mediator** — assigned to investigate.
|
||||||
- **Frontend** — buyer/seller "Report issue" buttons in the request detail view; admin dispute dashboard.
|
- **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`.
|
- **MongoDB** — `disputes`, `chats`, `purchaserequests`, `payments`.
|
||||||
- **Socket.IO** — no events fire today; all emits are TODO stubs (see warning above).
|
- **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
|
**Fix:** `authorizeRoles('admin', 'resolver')` added in commit `fce8a19`. Only admins and resolvers can change dispute status.
|
||||||
router.patch('/:id/status', DisputeController.updateStatus);
|
|
||||||
```
|
|
||||||
|
|
||||||
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
|
### 3. `POST /api/disputes/:id/assign` — no role guard ✅ FIXED
|
||||||
router.post('/:id/resolve', DisputeController.resolveDispute);
|
|
||||||
```
|
|
||||||
|
|
||||||
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.
|
**Fix:** `authorizeRoles('admin', 'resolver')` added in commit `fce8a19`. Only admins and resolvers can assign mediators.
|
||||||
|
|
||||||
### 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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 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
|
```ts
|
||||||
// app.ts line 521 — mounted FIRST
|
// app.ts — current state
|
||||||
app.use("/api/disputes", dashboardDisputeRoutes); // src/routes/disputeRoutes.ts
|
app.use("/api/disputes", dashboardDisputeRoutes); // src/routes/disputeRoutes.ts
|
||||||
|
app.use("/api/disputes/pr", disputeRoutes); // src/services/dispute/disputeRoutes.ts — new prefix
|
||||||
// app.ts line 585 — mounted SECOND
|
|
||||||
app.use("/api/disputes", disputeRoutes); // src/services/dispute/disputeRoutes.ts
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Express evaluates routes in registration order. This creates two concrete hazards:
|
Release-hold endpoints now use the `/api/disputes/pr/` prefix:
|
||||||
|
- `POST /api/disputes/pr/:purchaseRequestId/raise`
|
||||||
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.
|
- `GET /api/disputes/pr/:purchaseRequestId/status`
|
||||||
|
- `POST /api/disputes/pr/:purchaseRequestId/resolve`
|
||||||
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).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -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.**
|
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.**
|
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
|
### Phase 4 — Resolution
|
||||||
|
|
||||||
@@ -190,10 +173,11 @@ Express evaluates routes in registration order. This creates two concrete hazard
|
|||||||
- `dispute.closedAt = now`
|
- `dispute.closedAt = now`
|
||||||
- Appends `timeline` entry `dispute_resolved`.
|
- Appends `timeline` entry `dispute_resolved`.
|
||||||
- Saves.
|
- 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`)
|
- **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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
179
04 - Flows/Payment Flow - Scanner.md
Normal file
179
04 - Flows/Payment Flow - Scanner.md
Normal file
@@ -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 <SCANNER_API_KEY>
|
||||||
|
|
||||||
|
{
|
||||||
|
"intentId": "<payment._id>",
|
||||||
|
"chainId": 56,
|
||||||
|
"tokenAddress": "0x55d398326f99059ff775485246999027b3197955",
|
||||||
|
"destination": "0xSellerWalletAddress",
|
||||||
|
"amount": "10000000000000000000",
|
||||||
|
"callbackUrl": "https://api.amn.gg/api/payment/scanner-callback",
|
||||||
|
"callbackSecret": "<per-intent HMAC secret stored in payment doc>",
|
||||||
|
"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 |
|
||||||
@@ -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"]
|
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
|
# 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).
|
- 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.
|
- 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' }`.
|
||||||
|
|
||||||
```
|
`canManageOffer` is only `true` when `requestDetails?.status === 'received_offers'`; once the buyer accepts and the status advances, both buttons are hidden.
|
||||||
PUT /api/marketplace/offers/:id
|
|
||||||
Body: { status: 'withdrawn' }
|
|
||||||
```
|
|
||||||
|
|
||||||
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
|
## Sequence diagram
|
||||||
|
|
||||||
@@ -157,10 +155,10 @@ sequenceDiagram
|
|||||||
| `POST` | `/api/marketplace/purchase-requests/:id/offers` | Create offer | `purchaseRequestId` is a path param |
|
| `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/purchase-requests/:id/offers` | Buyer view of offers on a request | |
|
||||||
| `GET` | `/api/marketplace/offers/:id` | Single offer details | |
|
| `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) | |
|
| `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 |
|
| `GET` | `/api/marketplace/offers/seller/:sellerId` | All offers by this seller (used by Offer Management page) | Implemented via `getSellerOffers` frontend action (commit 240a668) |
|
||||||
| ~~`POST /api/marketplace/offers/:id/withdraw`~~ | — | ~~Seller withdraws~~ | ⚠️ NOT IMPLEMENTED — use `PATCH /api/marketplace/offers/:id` with `{ status: 'withdrawn' }` instead |
|
| `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
|
## Database writes
|
||||||
|
|
||||||
@@ -211,6 +209,9 @@ sequenceDiagram
|
|||||||
- Backend: `backend/src/services/marketplace/marketplaceController.ts`
|
- Backend: `backend/src/services/marketplace/marketplaceController.ts`
|
||||||
- Backend: `backend/src/models/SellerOffer.ts`
|
- Backend: `backend/src/models/SellerOffer.ts`
|
||||||
- Backend: `backend/src/services/payment/paymentCoordinator.ts` (payment-state cascade)
|
- 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/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
|
||||||
|
|||||||
@@ -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.
|
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
|
## Safety Rules
|
||||||
|
|
||||||
- Never store Trezor seed words, private keys, or xprv/tprv values.
|
- Never store Trezor seed words, private keys, or xprv/tprv values.
|
||||||
|
|||||||
@@ -2,12 +2,26 @@
|
|||||||
title: Colors
|
title: Colors
|
||||||
tags: [design-system, colors, palette]
|
tags: [design-system, colors, palette]
|
||||||
created: 2026-05-23
|
created: 2026-05-23
|
||||||
|
updated: 2026-05-30
|
||||||
---
|
---
|
||||||
|
|
||||||
# Colors
|
# 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.**
|
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]
|
> [!warning]
|
||||||
> Hardcoded colors break dark mode and any future preset switch. Use `sx={{ color: 'primary.main' }}` or `theme.palette.primary.main`.
|
> Hardcoded colors break dark mode and any future preset switch. Use `sx={{ color: 'primary.main' }}` or `theme.palette.primary.main`.
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,14 @@
|
|||||||
title: Design System Overview
|
title: Design System Overview
|
||||||
tags: [design-system, ui, mui]
|
tags: [design-system, ui, mui]
|
||||||
created: 2026-05-23
|
created: 2026-05-23
|
||||||
|
updated: 2026-05-30
|
||||||
---
|
---
|
||||||
|
|
||||||
# Design System Overview
|
# 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).
|
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]
|
> [!info]
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ A drawer-based UI lets the end user toggle visual preferences. Settings persist
|
|||||||
| **Contrast** | `default` · `bold` | `default` | localStorage |
|
| **Contrast** | `default` · `bold` | `default` | localStorage |
|
||||||
| **Layout** | `vertical` · `mini` · `horizontal` | `vertical` | localStorage |
|
| **Layout** | `vertical` · `mini` · `horizontal` | `vertical` | localStorage |
|
||||||
| **Direction** | `ltr` · `rtl` | derived from locale | localStorage (overrides locale default) |
|
| **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 |
|
| **Font family** | `Public Sans Variable`, `DM Sans Variable`, `Inter Variable`, `Nunito Sans Variable` | `Public Sans Variable` | localStorage |
|
||||||
| **Compact navigation** | boolean | `false` | localStorage |
|
| **Compact navigation** | boolean | `false` | localStorage |
|
||||||
| **Border radius** | 0–24 | 8 | localStorage |
|
| **Border radius** | 0–24 | 8 | localStorage |
|
||||||
|
|||||||
@@ -2,10 +2,14 @@
|
|||||||
title: Theme Configuration
|
title: Theme Configuration
|
||||||
tags: [design-system, theme, mui]
|
tags: [design-system, theme, mui]
|
||||||
created: 2026-05-23
|
created: 2026-05-23
|
||||||
|
updated: 2026-05-30
|
||||||
---
|
---
|
||||||
|
|
||||||
# Theme Configuration
|
# 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.
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -2,39 +2,45 @@
|
|||||||
title: Typography
|
title: Typography
|
||||||
tags: [design-system, typography, fonts]
|
tags: [design-system, typography, fonts]
|
||||||
created: 2026-05-23
|
created: 2026-05-23
|
||||||
|
updated: 2026-05-30
|
||||||
---
|
---
|
||||||
|
|
||||||
# Typography
|
# 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
|
## 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
|
```jsonc
|
||||||
"@fontsource-variable/public-sans": "^5.2.5", // Primary
|
"@fontsource-variable/source-serif-4": "...", // Headings (italic)
|
||||||
"@fontsource-variable/dm-sans": "^5.2.5", // Optional preset
|
"@fontsource/ibm-plex-sans": "...", // UI / body
|
||||||
"@fontsource-variable/inter": "^5.2.5", // Optional preset
|
"@fontsource/ibm-plex-mono": "...", // Amounts, addresses, hashes
|
||||||
"@fontsource-variable/nunito-sans": "^5.2.5", // Optional preset
|
|
||||||
"@fontsource/barlow": "^5.2.5", // Secondary (display)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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:
|
Default font-family stack in the theme:
|
||||||
|
|
||||||
```css
|
```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:
|
Use `sx={{ fontFamily: 'IBMPlexMono' }}` (theme alias) for any USDT amounts, contract addresses, or transaction hashes.
|
||||||
|
|
||||||
```tsx
|
|
||||||
<Typography variant="h1" sx={{ fontFamily: '"Barlow", serif' }}>Welcome</Typography>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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 - <date>.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'`).
|
||||||
@@ -11,24 +11,21 @@ What's instrumented today and what to watch. Today's stack is intentionally lean
|
|||||||
|
|
||||||
## 1. Health endpoint
|
## 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
|
`GET /api/health` response shape (from `healthCheckService`):
|
||||||
app.get("/health", (req, res) => {
|
```json
|
||||||
res.json({
|
{
|
||||||
success: true,
|
"status": "ok",
|
||||||
message: "Marketplace Backend API is running",
|
"version": "2.6.xx",
|
||||||
timestamp: new Date().toISOString(),
|
"timestamp": "...",
|
||||||
environment: config.nodeEnv,
|
"checks": { "mongodb": "ok", "redis": "ok", "uptime": 3600, "memoryMB": 120 }
|
||||||
version: packageJson.version,
|
}
|
||||||
});
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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`.
|
Public URL behind Nginx: `https://amn.gg/api/health`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
220
08 - Operations/Scanner Operations.md
Normal file
220
08 - Operations/Scanner Operations.md
Normal file
@@ -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 <project-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
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-<VERSION>` (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.
|
||||||
105
08 - Operations/Secret Rotation Runbook - 2026-05-30.md
Normal file
105
08 - Operations/Secret Rotation Runbook - 2026-05-30.md
Normal file
@@ -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.<NODE_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.
|
||||||
268
09 - Audits/Full Codebase Audit - 2026-05-30.md
Normal file
268
09 - Audits/Full Codebase Audit - 2026-05-30.md
Normal file
@@ -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]]
|
||||||
@@ -3,6 +3,9 @@ issue: 007
|
|||||||
title: "Frontend deleteAccount action calls DELETE /user/profile which has no backend route — account deletion is broken"
|
title: "Frontend deleteAccount action calls DELETE /user/profile which has no backend route — account deletion is broken"
|
||||||
severity: critical
|
severity: critical
|
||||||
domain: Authentication
|
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]
|
labels: [bug, frontend, critical, broken-feature]
|
||||||
status: open
|
status: open
|
||||||
created: 2026-05-29
|
created: 2026-05-29
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ issue: 008
|
|||||||
title: "sendFileMessage posts to wrong endpoint — file uploads silently fail or corrupt text-message handler"
|
title: "sendFileMessage posts to wrong endpoint — file uploads silently fail or corrupt text-message handler"
|
||||||
severity: critical
|
severity: critical
|
||||||
domain: Chat
|
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]
|
labels: [bug, frontend, critical, broken-feature]
|
||||||
status: open
|
status: open
|
||||||
created: 2026-05-29
|
created: 2026-05-29
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ issue: 009
|
|||||||
title: "archiveConversation sends PUT but backend only accepts PATCH — all archive attempts fail"
|
title: "archiveConversation sends PUT but backend only accepts PATCH — all archive attempts fail"
|
||||||
severity: critical
|
severity: critical
|
||||||
domain: Chat
|
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]
|
labels: [bug, frontend, critical, broken-feature]
|
||||||
status: open
|
status: open
|
||||||
created: 2026-05-29
|
created: 2026-05-29
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ issue: 010
|
|||||||
title: "Frontend admin updateUserStatus and updateUserRole use PUT but backend only accepts PATCH"
|
title: "Frontend admin updateUserStatus and updateUserRole use PUT but backend only accepts PATCH"
|
||||||
severity: critical
|
severity: critical
|
||||||
domain: User Management
|
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]
|
labels: [bug, frontend, critical, admin, broken-feature]
|
||||||
status: open
|
status: open
|
||||||
created: 2026-05-29
|
created: 2026-05-29
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ issue: 011
|
|||||||
title: "Frontend updateUserStatus sends 'inactive'/'pending' status values that backend does not accept"
|
title: "Frontend updateUserStatus sends 'inactive'/'pending' status values that backend does not accept"
|
||||||
severity: critical
|
severity: critical
|
||||||
domain: User Management
|
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]
|
labels: [bug, frontend, critical, admin, type-mismatch]
|
||||||
status: open
|
status: open
|
||||||
created: 2026-05-29
|
created: 2026-05-29
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ issue: 013
|
|||||||
title: "createProviderPaymentIntent always routes to request-network/intents regardless of provider argument"
|
title: "createProviderPaymentIntent always routes to request-network/intents regardless of provider argument"
|
||||||
severity: critical
|
severity: critical
|
||||||
domain: Payment
|
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]
|
labels: [bug, frontend, critical, payment, routing]
|
||||||
status: open
|
status: open
|
||||||
created: 2026-05-29
|
created: 2026-05-29
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ issue: 014
|
|||||||
title: "PaymentProvider TypeScript type excludes 'shkeeper' and 'decentralized' causing UI fallthrough for main payment providers"
|
title: "PaymentProvider TypeScript type excludes 'shkeeper' and 'decentralized' causing UI fallthrough for main payment providers"
|
||||||
severity: critical
|
severity: critical
|
||||||
domain: Payment
|
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]
|
labels: [bug, frontend, critical, payment, type-mismatch]
|
||||||
status: open
|
status: open
|
||||||
created: 2026-05-29
|
created: 2026-05-29
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ issue: 015
|
|||||||
title: "Simulated transaction SIM_ bypass has no environment guard — can fire in production on wallet connection failure"
|
title: "Simulated transaction SIM_ bypass has no environment guard — can fire in production on wallet connection failure"
|
||||||
severity: critical
|
severity: critical
|
||||||
domain: Payment
|
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]
|
labels: [security, bug, critical, payment, frontend, bypass]
|
||||||
status: open
|
status: open
|
||||||
created: 2026-05-29
|
created: 2026-05-29
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ title: "updatePurchaseRequest uses PUT but backend only registers PATCH — all
|
|||||||
severity: major
|
severity: major
|
||||||
domain: Purchase Request
|
domain: Purchase Request
|
||||||
labels: [bug, frontend, major, broken-feature]
|
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
|
status: open
|
||||||
created: 2026-05-29
|
created: 2026-05-29
|
||||||
source: Doc vs Code Audit 2026-05-29
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ issue: 017
|
|||||||
title: "updateOffer uses PUT /marketplace/offers/:id but backend registers PATCH /offers/:id — offer edits fail"
|
title: "updateOffer uses PUT /marketplace/offers/:id but backend registers PATCH /offers/:id — offer edits fail"
|
||||||
severity: major
|
severity: major
|
||||||
domain: Seller Offer
|
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]
|
labels: [bug, frontend, major, broken-feature]
|
||||||
status: open
|
status: open
|
||||||
created: 2026-05-29
|
created: 2026-05-29
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ title: "select-offer updateMany has no status filter — overwrites withdrawn/re
|
|||||||
severity: major
|
severity: major
|
||||||
domain: Seller Offer
|
domain: Seller Offer
|
||||||
labels: [bug, backend, major, data-integrity]
|
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
|
status: open
|
||||||
created: 2026-05-29
|
created: 2026-05-29
|
||||||
source: Doc vs Code Audit 2026-05-29
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ title: "POST /api/marketplace/offers/:id/withdraw HTTP route does not exist —
|
|||||||
severity: major
|
severity: major
|
||||||
domain: Seller Offer
|
domain: Seller Offer
|
||||||
labels: [missing-feature, backend, frontend, major]
|
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
|
status: open
|
||||||
created: 2026-05-29
|
created: 2026-05-29
|
||||||
source: Doc vs Code Audit 2026-05-29
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ title: "GET /api/payment/payments/:id/debug has no authentication — full payme
|
|||||||
severity: major
|
severity: major
|
||||||
domain: Payment
|
domain: Payment
|
||||||
labels: [security, bug, backend, major, missing-auth]
|
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
|
created: 2026-05-29
|
||||||
source: Doc vs Code Audit 2026-05-29
|
source: Doc vs Code Audit 2026-05-29
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ title: "GET /api/payment/export has no admin role guard at route level — any a
|
|||||||
severity: major
|
severity: major
|
||||||
domain: Payment
|
domain: Payment
|
||||||
labels: [security, bug, backend, major, privilege-escalation]
|
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
|
status: open
|
||||||
created: 2026-05-29
|
created: 2026-05-29
|
||||||
source: Doc vs Code Audit 2026-05-29
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ issue: 024
|
|||||||
title: "GET /api/payment/stats has no admin role guard — any authenticated user can read aggregate payment stats"
|
title: "GET /api/payment/stats has no admin role guard — any authenticated user can read aggregate payment stats"
|
||||||
severity: major
|
severity: major
|
||||||
domain: Payment
|
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]
|
labels: [security, bug, backend, major, privilege-escalation]
|
||||||
status: open
|
status: open
|
||||||
created: 2026-05-29
|
created: 2026-05-29
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ issue: 025
|
|||||||
title: "GET /api/disputes/statistics has no admin role guard — any authenticated user can access aggregate dispute KPIs"
|
title: "GET /api/disputes/statistics has no admin role guard — any authenticated user can access aggregate dispute KPIs"
|
||||||
severity: major
|
severity: major
|
||||||
domain: Dispute
|
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]
|
labels: [security, bug, backend, major, privilege-escalation]
|
||||||
status: open
|
status: open
|
||||||
created: 2026-05-29
|
created: 2026-05-29
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ issue: 026
|
|||||||
title: "GET /notifications/:id only returns user's most-recent notification — all others return 404 erroneously"
|
title: "GET /notifications/:id only returns user's most-recent notification — all others return 404 erroneously"
|
||||||
severity: major
|
severity: major
|
||||||
domain: Notification
|
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]
|
labels: [bug, backend, major, broken-feature]
|
||||||
status: open
|
status: open
|
||||||
created: 2026-05-29
|
created: 2026-05-29
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ title: "confirm-delivery endpoint has no ownership check — any authenticated u
|
|||||||
severity: major
|
severity: major
|
||||||
domain: Delivery
|
domain: Delivery
|
||||||
labels: [security, bug, backend, major, authorization]
|
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
|
status: open
|
||||||
created: 2026-05-29
|
created: 2026-05-29
|
||||||
source: Doc vs Code Audit 2026-05-29
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ title: "delivery-code-generated socket event broadcasts raw 6-digit code to enti
|
|||||||
severity: major
|
severity: major
|
||||||
domain: Delivery
|
domain: Delivery
|
||||||
labels: [security, bug, backend, major, 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
|
status: open
|
||||||
created: 2026-05-29
|
created: 2026-05-29
|
||||||
source: Doc vs Code Audit 2026-05-29
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ title: "No brute-force protection on delivery code verification endpoint — 900
|
|||||||
severity: major
|
severity: major
|
||||||
domain: Delivery
|
domain: Delivery
|
||||||
labels: [security, bug, backend, major, brute-force]
|
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
|
status: open
|
||||||
created: 2026-05-29
|
created: 2026-05-29
|
||||||
source: Doc vs Code Audit 2026-05-29
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ issue: 030
|
|||||||
title: "POST /api/payment/payments/cleanup-pending admin check is inside handler only — no middleware-level enforcement"
|
title: "POST /api/payment/payments/cleanup-pending admin check is inside handler only — no middleware-level enforcement"
|
||||||
severity: major
|
severity: major
|
||||||
domain: Admin
|
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]
|
labels: [security, bug, backend, major, missing-auth]
|
||||||
status: open
|
status: open
|
||||||
created: 2026-05-29
|
created: 2026-05-29
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ issue: 031
|
|||||||
title: "POST /api/points/admin/add admin check is inside handler only — no middleware-level enforcement"
|
title: "POST /api/points/admin/add admin check is inside handler only — no middleware-level enforcement"
|
||||||
severity: major
|
severity: major
|
||||||
domain: Admin
|
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]
|
labels: [security, bug, backend, major, missing-auth]
|
||||||
status: open
|
status: open
|
||||||
created: 2026-05-29
|
created: 2026-05-29
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ issue: 032
|
|||||||
title: "Admin delete user via legacy endpoint performs hard delete (findByIdAndDelete) instead of soft delete"
|
title: "Admin delete user via legacy endpoint performs hard delete (findByIdAndDelete) instead of soft delete"
|
||||||
severity: major
|
severity: major
|
||||||
domain: User Management
|
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]
|
labels: [bug, frontend, backend, major, data-integrity]
|
||||||
status: open
|
status: open
|
||||||
created: 2026-05-29
|
created: 2026-05-29
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ title: "Admin can delete other admin accounts via new controller — legacy admi
|
|||||||
severity: major
|
severity: major
|
||||||
domain: User Management
|
domain: User Management
|
||||||
labels: [security, bug, backend, major, privilege-escalation]
|
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
|
status: open
|
||||||
created: 2026-05-29
|
created: 2026-05-29
|
||||||
source: Doc vs Code Audit 2026-05-29
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ issue: 035
|
|||||||
title: "Frontend getPaymentStatus and confirmPayment call non-existent endpoints GET /payment/:id/status and POST /payment/:id/confirm"
|
title: "Frontend getPaymentStatus and confirmPayment call non-existent endpoints GET /payment/:id/status and POST /payment/:id/confirm"
|
||||||
severity: major
|
severity: major
|
||||||
domain: Payment
|
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]
|
labels: [bug, frontend, major, broken-feature, dispute]
|
||||||
status: open
|
status: open
|
||||||
created: 2026-05-29
|
created: 2026-05-29
|
||||||
|
|||||||
@@ -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"
|
title: "reset-password-with-code endpoint has no password complexity validation — accepts weak passwords rejected by token-based reset"
|
||||||
severity: major
|
severity: major
|
||||||
domain: Authentication
|
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]
|
labels: [security, bug, backend, major, auth]
|
||||||
status: open
|
status: open
|
||||||
created: 2026-05-29
|
created: 2026-05-29
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ title: "POST /api/marketplace/purchase-requests/:id/final-approval creates dummy
|
|||||||
severity: major
|
severity: major
|
||||||
domain: Purchase Request
|
domain: Purchase Request
|
||||||
labels: [security, bug, backend, major, escrow, bypass]
|
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
|
status: open
|
||||||
created: 2026-05-29
|
created: 2026-05-29
|
||||||
source: Doc vs Code Audit 2026-05-29
|
source: Doc vs Code Audit 2026-05-29
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ issue: 045
|
|||||||
title: "addParticipants frontend sends { participants: string[] } array but backend expects { userId: string } single user"
|
title: "addParticipants frontend sends { participants: string[] } array but backend expects { userId: string } single user"
|
||||||
severity: major
|
severity: major
|
||||||
domain: Chat
|
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]
|
labels: [bug, frontend, major, chat]
|
||||||
status: open
|
status: open
|
||||||
created: 2026-05-29
|
created: 2026-05-29
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ title: "Frontend reloadNetworkRegistry and probeChain call backend endpoints tha
|
|||||||
severity: major
|
severity: major
|
||||||
domain: Admin
|
domain: Admin
|
||||||
labels: [missing-feature, backend, major, admin]
|
labels: [missing-feature, backend, major, admin]
|
||||||
status: open
|
status: resolved
|
||||||
created: 2026-05-29
|
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
|
source: Doc vs Code Audit 2026-05-29
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ title: "Frontend getConfirmationThresholdHistory calls GET /api/admin/settings/c
|
|||||||
severity: major
|
severity: major
|
||||||
domain: Admin
|
domain: Admin
|
||||||
labels: [missing-feature, backend, major, admin]
|
labels: [missing-feature, backend, major, admin]
|
||||||
status: open
|
status: resolved
|
||||||
created: 2026-05-29
|
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
|
source: Doc vs Code Audit 2026-05-29
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ issue: 052
|
|||||||
title: "'completed' payment status not counted in successfulPayments stats — admin dashboard undercounts"
|
title: "'completed' payment status not counted in successfulPayments stats — admin dashboard undercounts"
|
||||||
severity: major
|
severity: major
|
||||||
domain: Payment
|
domain: Payment
|
||||||
|
status: resolved
|
||||||
|
resolved: 2026-05-29
|
||||||
|
fix: "Added case 'completed' to the successfulPayments switch in paymentService.ts getPaymentStats."
|
||||||
labels: [backend, bug]
|
labels: [backend, bug]
|
||||||
status: open
|
status: open
|
||||||
created: 2026-05-29
|
created: 2026-05-29
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ issue: 053
|
|||||||
title: "Axios interceptor only retriggers token refresh for 401, not 403"
|
title: "Axios interceptor only retriggers token refresh for 401, not 403"
|
||||||
severity: major
|
severity: major
|
||||||
domain: Authentication
|
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]
|
labels: [frontend, bug]
|
||||||
status: open
|
status: open
|
||||||
created: 2026-05-29
|
created: 2026-05-29
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ issue: 054
|
|||||||
title: "Login rate limiter counts all attempts (not just failures) — users locked out after correct logins"
|
title: "Login rate limiter counts all attempts (not just failures) — users locked out after correct logins"
|
||||||
severity: major
|
severity: major
|
||||||
domain: Authentication
|
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]
|
labels: [backend, bug]
|
||||||
status: open
|
status: open
|
||||||
created: 2026-05-29
|
created: 2026-05-29
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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`
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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]]
|
||||||
@@ -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]]
|
||||||
39
Issues/ISSUE-076-scanner-ssrf-via-unvalidated-callbackurl.md
Normal file
39
Issues/ISSUE-076-scanner-ssrf-via-unvalidated-callbackurl.md
Normal file
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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]]
|
||||||
@@ -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
|
||||||
@@ -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]]
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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]]
|
||||||
@@ -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
|
||||||
@@ -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]]
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user