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:
@@ -34,7 +34,9 @@ This page is the entry point for the API. See the individual service pages for e
|
||||
|
||||
The base port is set via `PORT` env var; in `development` it defaults to `5001`. CORS is restricted to `process.env.FRONTEND_URL` and credentials are allowed (`cors({ origin, credentials: true })` in `app.ts`).
|
||||
|
||||
Health check (not under `/api`): `GET /health` → `{ success, message, timestamp, environment, version }`.
|
||||
Health checks:
|
||||
- `GET /health` (not under `/api`) → `{ success, message, timestamp, environment, version }` — used by Docker and Gatus.
|
||||
- `GET /api/health` (added in commit `44579d6`, backend v2.6.49) → deeper JSON with database and Redis connectivity status, plus the version string. Used by Gatus monitoring.
|
||||
|
||||
API discovery endpoint: `GET /api` → returns a map of available service prefixes.
|
||||
|
||||
|
||||
@@ -5,13 +5,16 @@ tags: [api, admin, reference]
|
||||
|
||||
# Admin API
|
||||
|
||||
> **Last updated:** 2026-05-29 — aligned with code (see Doc vs Code Audit Report)
|
||||
> **Last updated:** 2026-05-30 — break-glass endpoints added, scanner/status auth fixed, reload/probe routes now implemented, confirmation threshold history implemented, resolver role added
|
||||
|
||||
There is no single `/api/admin` namespace — admin-only endpoints are scattered across the service routers. This page catalogs them in one place. All require `Bearer JWT` with `req.user.role === 'admin'` unless explicitly noted otherwise. The two enforcement patterns are:
|
||||
|
||||
- Middleware: `authorizeRoles('admin')` after `authenticateToken` (used by the dispute, data-cleanup, blog routers).
|
||||
- Inline check inside the handler: `if (req.user.role !== 'admin') return 403` (used by user, points, payment routes).
|
||||
|
||||
> [!note] Resolver role
|
||||
> The `resolver` role was added (commit `fce8a19`). Resolvers have access to the dispute-triage endpoints (`assign`, `status`, `resolve`, `statistics`) only. All other admin endpoints remain `admin`-only.
|
||||
|
||||
## User management
|
||||
|
||||
See full descriptions in [[User API]].
|
||||
@@ -159,9 +162,14 @@ Router: [`backend/src/services/admin/dataCleanupRoutes.ts`](../../backend/src/se
|
||||
|
||||
### GET /api/admin/scanner/status
|
||||
|
||||
**Description:** Returns the current state of the blockchain scanner / wallet monitor.
|
||||
**Description:** Returns the current state of the AMN Pay Scanner. Proxies to `AMN_SCANNER_URL/scanner/status`.
|
||||
**Auth required:** Bearer JWT (`admin`) — `authenticateToken` + `authorizeRoles('admin')` were added in commit `1d881c5`. The previously documented unauthenticated access gap (ISSUE-006) is closed.
|
||||
|
||||
> **⚠️ SECURITY BUG — NO AUTHENTICATION:** Despite being mounted under `/api/admin/`, this endpoint has **no** `authenticateToken` or `authorizeRoles` guard. Any unauthenticated request can read scanner state.
|
||||
### POST /api/admin/scanner/webhooks/retry
|
||||
|
||||
**Description:** Trigger a retry of failed/pending scanner webhooks.
|
||||
**Auth required:** Bearer JWT (`admin`)
|
||||
**Request body:** `{ intentId?: string }` — omit to retry all pending.
|
||||
|
||||
## Settings
|
||||
|
||||
@@ -174,6 +182,13 @@ Router: [`backend/src/services/admin/dataCleanupRoutes.ts`](../../backend/src/se
|
||||
| `GET /api/admin/settings/aml` | admin | Read current AML settings |
|
||||
| `PATCH /api/admin/settings/aml` | admin | Update AML settings (runtime only — not persisted to disk or DB) |
|
||||
|
||||
**AML providers available:**
|
||||
|
||||
- **Chainalysis** — cloud API provider (requires `CHAINALYSIS_API_KEY`). Enabled via `AML_PROVIDER=chainalysis`.
|
||||
- **OFAC SDN local** — downloads the US Treasury SDN XML list once per 24 hours and checks addresses locally. No API key required. Enabled via `AML_PROVIDER=ofac`. Added in commit `31343d1` (Task #10). List is fetched from `OFAC_SDN_URL` (defaults to `https://www.treasury.gov/ofac/downloads/sdn.xml`).
|
||||
|
||||
The active provider is selected at startup via `AML_PROVIDER`. `PATCH /api/admin/settings/aml` can switch the provider at runtime but the change is not persisted.
|
||||
|
||||
### Confirmation thresholds
|
||||
|
||||
Frontend page exists. Endpoints require admin auth.
|
||||
@@ -182,8 +197,22 @@ Frontend page exists. Endpoints require admin auth.
|
||||
| --- | --- |
|
||||
| `GET /api/admin/settings/confirmation-thresholds` | Get current confirmation thresholds for all chains |
|
||||
| `PATCH /api/admin/settings/confirmation-thresholds/:chainId` | Update threshold for a specific chain |
|
||||
| `GET /api/admin/settings/confirmation-thresholds/history` | Last 50 threshold change events (populated with `changedBy` user email/name) |
|
||||
|
||||
> **Not implemented:** `GET /api/admin/settings/confirmation-thresholds/history` — history endpoint does not exist. `POST /api/admin/rn/networks/reload` and `POST /api/admin/rn/networks/probe/:chainId` do not exist.
|
||||
> **History route:** `GET /api/admin/settings/confirmation-thresholds/history` is now implemented (commit `27fb15a`). It reads from the `ConfigSettingHistory` collection, keyed as `confirmation_threshold:<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
|
||||
|
||||
@@ -200,6 +229,10 @@ Frontend page exists.
|
||||
| Endpoint | Auth | Action |
|
||||
| --- | --- | --- |
|
||||
| `GET /api/admin/rn/networks` | admin | List all registered RN networks |
|
||||
| `POST /api/admin/rn/networks/reload` | admin | Reload chain + token registries from disk (no restart needed) |
|
||||
| `POST /api/admin/rn/networks/probe/:chainId` | admin | On-demand on-chain probe: RPC reachability, proxy bytecode, dummy-call validity |
|
||||
|
||||
> All three routes are implemented (commit `5681abf`). Previous docs listed reload and probe as not implemented.
|
||||
|
||||
## Blog admin
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ tags: [api, auth, reference]
|
||||
|
||||
# Authentication API
|
||||
|
||||
> **Last updated:** 2026-05-29 — aligned with code (see [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md))
|
||||
> **Last updated:** 2026-05-30 — Cloudflare Turnstile CAPTCHA added after 3 failed logins (commit `b8edbbf`)
|
||||
|
||||
All endpoints are mounted under `/api/auth/*` in `backend/src/app.ts`. The routes file is [`backend/src/services/auth/authRoutes.ts`](../../backend/src/services/auth/authRoutes.ts) and the WebAuthn sub-routes are in [`passkeyRoutes.ts`](../../backend/src/services/auth/passkeyRoutes.ts). Controller logic lives in [`authController.ts`](../../backend/src/services/auth/authController.ts) and [`authService.ts`](../../backend/src/services/auth/authService.ts).
|
||||
|
||||
@@ -121,6 +121,12 @@ Two distinct identities are involved: a [[User]] (`models/User.ts`) and a [[Temp
|
||||
- `403` email not verified
|
||||
- `423` account locked (after repeated failures, tracked in Redis via `rateLimitService`)
|
||||
|
||||
**Cloudflare Turnstile CAPTCHA:** After **3 failed login attempts** from the same IP within 15 minutes the `captchaGate` middleware requires a valid `cf-turnstile-response` token in the request body. Responses when CAPTCHA is required but missing:
|
||||
```json
|
||||
{ "success": false, "captchaRequired": true, "message": "..." }
|
||||
```
|
||||
HTTP status: `429`. When `TURNSTILE_SECRET_KEY` is not set (local dev) the gate is skipped.
|
||||
|
||||
**⚠️ Rate limiter behaviour:** The attempt counter increments on **every** attempt (before password validation), not only on failures. 5 total attempts within 15 minutes triggers lockout — a user burning 5 attempts with typos will be locked out even if they never had a valid password.
|
||||
|
||||
**Side effects:**
|
||||
|
||||
@@ -5,10 +5,13 @@ tags: [api, chat, reference]
|
||||
|
||||
# Chat API
|
||||
|
||||
> **Last updated:** 2026-05-29 — aligned with code (see [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md))
|
||||
> **Last updated:** 2026-05-30 — admin and resolver roles can now read and send messages in any chat (commit `766a9a2`)
|
||||
|
||||
All chat endpoints live under `/api/chat/*`. The router is [`backend/src/services/chat/chatRoutes.ts`](../../backend/src/services/chat/chatRoutes.ts), controller is `chatController`, service is `ChatService`. Every endpoint requires `Bearer JWT` — the router applies `authenticateToken` globally.
|
||||
|
||||
> [!note] Admin and resolver chat access
|
||||
> Users with role `admin` or `resolver` can **read messages and send messages in any chat** without being a listed participant (`ChatService` checks `canBypassMembership = senderRole === 'admin' || senderRole === 'resolver'`). This applies to `GET /api/chat/:id/messages`, `GET /api/chat/:id/info`, and `POST /api/chat/:id/messages`. Dispute-chat monitoring for resolvers was the primary driver (commit `766a9a2`).
|
||||
|
||||
Model: [[Chat]]. Real-time delivery happens over Socket.IO rooms named `chat-<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
|
||||
|
||||
@@ -5,18 +5,21 @@ tags: [api, dispute, reference]
|
||||
|
||||
# Dispute API
|
||||
|
||||
> **Last updated:** 2026-05-29 — aligned with code (see [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md))
|
||||
> **Last updated:** 2026-05-30 — resolver role added, role guards applied to assign/status/resolve (commits b9e0f6a, 1d881c5)
|
||||
|
||||
> [!note] Current implementation
|
||||
> The Dispute module now has a Mongoose model, controller routes, dashboard routes, and release-hold helper routes mounted under `/api/disputes`. Keep this page aligned with both `backend/src/routes/disputeRoutes.ts` and `backend/src/services/dispute/disputeRoutes.ts`.
|
||||
> The Dispute module has two distinct router families. Keep this page aligned with both `backend/src/routes/disputeRoutes.ts` and `backend/src/services/dispute/disputeRoutes.ts`.
|
||||
|
||||
Endpoints live under `/api/disputes/*`. `backend/src/routes/disputeRoutes.ts` delegates to `DisputeController` (`backend/src/controllers/disputeController.ts`) for CRUD/triage. `backend/src/services/dispute/disputeRoutes.ts` provides lightweight release-hold endpoints (`raise`, `resolve`, `status`) used by escrow release gating. The routers apply `authenticateToken` globally — every endpoint requires `Bearer JWT`.
|
||||
Endpoints live under two prefixes:
|
||||
|
||||
> [!warning] Route shadowing — both dispute routers are mounted at `/api/disputes`
|
||||
> The dashboard router is mounted **first** in `app.ts`. Its `POST /:id/resolve` intercepts requests before the admin-guarded release-hold router's resolve handler. Confirm which handler will run before wiring automation to either resolve endpoint.
|
||||
- `/api/disputes/*` — `backend/src/routes/disputeRoutes.ts` delegates to `DisputeController` (`backend/src/controllers/disputeController.ts`) for CRUD/triage. All routes apply `authenticateToken` globally.
|
||||
- `/api/disputes/pr/*` — `backend/src/services/dispute/disputeRoutes.ts` provides lightweight release-hold endpoints (`raise`, `resolve`, `status`) used by escrow release gating. Previously mounted at `/api/disputes`, causing route shadowing (ISSUE-003). **Remounted at `/api/disputes/pr` in commit `1d881c5`** — all release-hold calls must use this new prefix.
|
||||
|
||||
> [!danger] Security issues — see individual endpoint notes below
|
||||
> Several endpoints that are documented as admin-only have **no role guard** in the current codebase. Any authenticated user can call them. These are noted per-endpoint.
|
||||
> [!success] Route shadowing resolved (ISSUE-003)
|
||||
> The release-hold router was remounted from `/api/disputes` to `/api/disputes/pr`. Both routers now have independent paths and neither shadows the other.
|
||||
|
||||
> [!note] Resolver role
|
||||
> A new `resolver` role was added (commit `fce8a19`). Resolvers can view and resolve disputes but have no other platform privileges. They are granted the same access as `admin` on all dispute-triage operations listed below.
|
||||
|
||||
> [!note] Real-time events
|
||||
> All socket events from `DisputeService` are currently **TODO stubs**. No real-time events fire from dispute mutations. Notifications are delivered via `POST /api/notifications` → `new-notification` socket event only.
|
||||
@@ -48,16 +51,18 @@ Model: [[Dispute]]. A dispute references a [[PurchaseRequest]] plus optional [[P
|
||||
- Notifies the counter-party via `POST /api/notifications` (`new-notification` socket event).
|
||||
- Pauses any in-flight payout (sets a hold flag on the related [[Payment]]).
|
||||
|
||||
### POST /api/disputes/:purchaseRequestId/raise
|
||||
### POST /api/disputes/pr/:purchaseRequestId/raise
|
||||
|
||||
**Description:** Lightweight release-hold endpoint that marks a purchase request and related payments as disputed. Exists in the backend but has no corresponding frontend action.
|
||||
**Description:** Lightweight release-hold endpoint that marks a purchase request and related payments as disputed. No corresponding frontend UI action.
|
||||
**Auth required:** Bearer JWT (buyer who owns the request or admin)
|
||||
**Request body:** `{ reason?: string }`
|
||||
**Response 200:** `{ success, message, data }`
|
||||
|
||||
### GET /api/disputes/:purchaseRequestId/status
|
||||
> **Path note:** Previously served at `/api/disputes/:purchaseRequestId/raise`. Moved to `/api/disputes/pr/:purchaseRequestId/raise` in commit `1d881c5` (ISSUE-003 fix).
|
||||
|
||||
**Description:** Returns release-hold flags for a purchase request, including whether release is currently blocked. Exists in the backend but has no corresponding frontend action.
|
||||
### GET /api/disputes/pr/:purchaseRequestId/status
|
||||
|
||||
**Description:** Returns release-hold flags for a purchase request, including whether release is currently blocked. No corresponding frontend UI action.
|
||||
**Auth required:** Bearer JWT (buyer, preferred seller, or admin)
|
||||
|
||||
## Read
|
||||
@@ -79,7 +84,7 @@ Model: [[Dispute]]. A dispute references a [[PurchaseRequest]] plus optional [[P
|
||||
### GET /api/disputes/statistics
|
||||
|
||||
**Description:** Aggregated counts (open, by reason, average resolution time) for admin dashboards.
|
||||
**Auth required:** Bearer JWT (any authenticated user — backend applies `authenticateToken` only, no role restriction)
|
||||
**Auth required:** Bearer JWT (`admin` or `resolver` — `authorizeRoles('admin', 'resolver')` is applied)
|
||||
**Response 200:** `{ success, data: { open, byReason, avgResolutionHours, ... } }`
|
||||
|
||||
### GET /api/disputes/:id
|
||||
@@ -92,10 +97,8 @@ Model: [[Dispute]]. A dispute references a [[PurchaseRequest]] plus optional [[P
|
||||
|
||||
### POST /api/disputes/:id/assign
|
||||
|
||||
**Description:** Assign an admin moderator to the dispute. Sets `assignedAdminId` and transitions status to `in_progress`.
|
||||
**Auth required:** Bearer JWT
|
||||
|
||||
> ⚠️ **SECURITY — NO ROLE GUARD:** Despite being documented as admin-only, there is no role guard on this endpoint. Any authenticated user can self-assign as mediator on any dispute.
|
||||
**Description:** Assign an admin or resolver moderator to the dispute. Sets `assignedAdminId` and transitions status to `in_progress`.
|
||||
**Auth required:** Bearer JWT (`admin` or `resolver`)
|
||||
|
||||
**Request body:** `{ adminId: string }`
|
||||
**Side effects:** Notifies all participants.
|
||||
@@ -103,18 +106,14 @@ Model: [[Dispute]]. A dispute references a [[PurchaseRequest]] plus optional [[P
|
||||
### PATCH /api/disputes/:id/status
|
||||
|
||||
**Description:** Generic status update (e.g. close without resolution).
|
||||
**Auth required:** Bearer JWT
|
||||
|
||||
> ⚠️ **SECURITY — NO ROLE GUARD:** There is no role guard on this endpoint. Any authenticated user can change dispute status despite documentation claiming admin-only access.
|
||||
**Auth required:** Bearer JWT (`admin` or `resolver`)
|
||||
|
||||
**Request body:** `{ status: string; note?: string }`
|
||||
|
||||
### POST /api/disputes/:id/resolve
|
||||
|
||||
**Description:** Final adjudication. Records the decision and triggers the appropriate escrow action.
|
||||
**Auth required:** Bearer JWT
|
||||
|
||||
> ⚠️ **SECURITY — NO ROLE GUARD:** This is the dashboard router's resolve handler (mounted first). There is no role guard. Any authenticated user can resolve a dispute, including issuing `action=ban_seller`.
|
||||
**Auth required:** Bearer JWT (`admin` or `resolver`)
|
||||
|
||||
> ⚠️ **ROUTE SHADOWING:** Because the dashboard router is mounted before the admin-guarded release-hold router, this handler intercepts all `POST /api/disputes/:id/resolve` requests. The admin-guarded release-hold resolve endpoint is unreachable at this path.
|
||||
|
||||
@@ -131,13 +130,14 @@ Model: [[Dispute]]. A dispute references a [[PurchaseRequest]] plus optional [[P
|
||||
- `action === "refund"` → create/approve the corresponding refund instruction through the ledger-gated payment release/refund flow.
|
||||
- `action === "no_action"` or seller-favorable outcome → clear hold only after release checks pass.
|
||||
- Notifies both participants and updates [[PurchaseRequest]] status to `disputed_resolved`.
|
||||
- **ISSUE-004 fix (commit `1d881c5`):** `DisputeService.resolveDispute` now calls `releaseHoldResolve()` on the linked `purchaseRequestId`, clearing the escrow hold so payment release is unblocked automatically after resolution.
|
||||
|
||||
### POST /api/disputes/:purchaseRequestId/resolve
|
||||
### POST /api/disputes/pr/:purchaseRequestId/resolve
|
||||
|
||||
**Description:** Lightweight release-hold endpoint that clears the disputed hold flags on a purchase request and related payments.
|
||||
**Auth required:** Bearer JWT (admin)
|
||||
|
||||
> ⚠️ **ROUTE SHADOWING:** This endpoint is on the release-hold router which is mounted **after** the dashboard router. The dashboard router's `POST /:id/resolve` matches first, making this handler unreachable in practice. See the route shadowing warning at the top of this page.
|
||||
> **Path note:** Previously unreachable due to route shadowing. Moved to `/api/disputes/pr/:purchaseRequestId/resolve` (commit `1d881c5`, ISSUE-003 fix). This endpoint is now reachable.
|
||||
|
||||
**Response 200:** `{ success, message, data }`
|
||||
|
||||
|
||||
@@ -71,8 +71,8 @@ The buyer-facing CRUD plus seller-side workflow endpoints. Model: [[PurchaseRequ
|
||||
size?: string;
|
||||
color?: string;
|
||||
quantity?: number; // default 1
|
||||
budget?: { min?: number; max?: number; currency: "USD" | "EUR" | "IRR" };
|
||||
urgency?: "low" | "medium" | "high";
|
||||
budget?: { min?: number; max?: number; currency: "USDT" | "USDC" }; // restricted to escrow-compatible stablecoins (commit d52feb7)
|
||||
urgency?: "low" | "medium" | "high" | "urgent";
|
||||
deliveryInfo?: {
|
||||
deliveryType: "physical" | "online";
|
||||
addressId?: string; // when physical
|
||||
@@ -239,7 +239,7 @@ Valid `status` values: `pending | accepted | rejected | withdrawn`
|
||||
**Request body:**
|
||||
```ts
|
||||
{
|
||||
price: { amount: number; currency: "USD" | "EUR" | "IRR" };
|
||||
price: { amount: number; currency: "USDT" }; // USDT only for escrow MVP
|
||||
deliveryEstimate: { days: number; note?: string };
|
||||
notes?: string;
|
||||
attachments?: string[];
|
||||
@@ -248,6 +248,8 @@ Valid `status` values: `pending | accepted | rejected | withdrawn`
|
||||
**Response 201:** `{ success, data: { offer } }`
|
||||
**Side effects:** Emits `new-offer` to `buyer-<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)
|
||||
|
||||
**Description:** Older offer-update endpoint kept for compatibility.
|
||||
@@ -271,16 +273,19 @@ Valid `status` values: `pending | accepted | rejected | withdrawn`
|
||||
|
||||
This endpoint does not exist. Use `GET /api/marketplace/purchase-requests/:id/offers` instead.
|
||||
|
||||
### ⚠️ NOT IMPLEMENTED: GET /api/marketplace/offers/seller/:sellerId
|
||||
### GET /api/marketplace/offers/seller/:sellerId
|
||||
|
||||
This endpoint does not exist. `getOffersBySeller()` is an internal service method and is not exposed via HTTP.
|
||||
**Description:** Returns all offers submitted by the given seller, across all purchase requests. Used by the Offer Management dashboard page (`/dashboard/seller/marketplace/offers`).
|
||||
**Auth required:** Bearer JWT (seller, own `:sellerId` only)
|
||||
**Response 200:** `{ data: [SellerOffer, ...] }`
|
||||
**Frontend action:** `getSellerOffers(sellerId)` in `src/actions/marketplace.ts` (added commit 240a668)
|
||||
|
||||
### PATCH /api/marketplace/offers/:id
|
||||
|
||||
**Description:** Seller edits their pending offer (price, delivery estimate, notes).
|
||||
**Auth required:** Bearer JWT (offer owner)
|
||||
|
||||
> ⚠️ **KNOWN BUG:** The frontend sends `PUT` but the backend registers `PATCH`. Requests from clients using `PUT` will receive `404`. Use `PATCH`.
|
||||
> ✅ **Fixed (commit 240a668):** The frontend `updateOffer` and `acceptOffer` actions now correctly send `PATCH`.
|
||||
|
||||
### DELETE /api/marketplace/offers/:id
|
||||
|
||||
@@ -293,9 +298,14 @@ This endpoint does not exist. `getOffersBySeller()` is an internal service metho
|
||||
**Auth required:** Bearer JWT
|
||||
**Request body:** `{ status: "pending" | "accepted" | "rejected" | "withdrawn" }`
|
||||
|
||||
### ⚠️ NOT IMPLEMENTED: POST /api/marketplace/offers/:id/withdraw
|
||||
### POST /api/marketplace/offers/:id/withdraw
|
||||
|
||||
This endpoint does not exist. To withdraw an offer use `PUT /api/marketplace/offers/:id/status` with `{ status: 'withdrawn' }`.
|
||||
**Description:** Seller withdraws their offer. Sets offer status to `withdrawn` using `sellerOfferService.withdrawOffer()`. Only the offer owner may call this.
|
||||
**Auth required:** Bearer JWT (offer owner)
|
||||
**Response 200:** `{ success: true, data: { /* updated offer */ } }`
|
||||
**Errors:** `403` not the offer owner, `404` offer not found.
|
||||
|
||||
> **Note:** This endpoint was previously documented as NOT IMPLEMENTED. It was added to `backend/src/services/marketplace/routes.ts` (commit `3e47713`).
|
||||
|
||||
### POST /api/marketplace/purchase-requests/:id/select-offer
|
||||
|
||||
@@ -303,7 +313,8 @@ This endpoint does not exist. To withdraw an offer use `PUT /api/marketplace/off
|
||||
**Auth required:** Bearer JWT (buyer)
|
||||
**Request body:** `{ offerId: string }`
|
||||
**Side effects:**
|
||||
- Updates [[PurchaseRequest]] `selectedOfferId`, status moves toward `payment`.
|
||||
- Persists `selectedOfferId` on [[PurchaseRequest]] (commit `023255f` — previously this field was not saved, causing it to be lost). Status moves toward `payment`.
|
||||
- Rejects all **losing** offers (sets their status to `rejected`) when payment is confirmed (commit `023255f`).
|
||||
- Emits `seller-offer-update` to all sellers for the request.
|
||||
|
||||
### POST /api/marketplace/offers/:id/accept (legacy)
|
||||
|
||||
@@ -5,7 +5,7 @@ tags: [api, payment, reference, request-network, escrow]
|
||||
|
||||
# Payment API
|
||||
|
||||
> **Last updated:** 2026-05-29 — aligned with code (see [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md))
|
||||
> **Last updated:** 2026-05-30 — AMN Pay Scanner integration, on-demand RN reconcile in GET /payment/:id, pay-in route renamed, reload/probe routes now implemented
|
||||
|
||||
The payment surface is split across provider-neutral payment routers, Request Network checkout/webhook routes, derived-destination custody routes, and admin safety routes:
|
||||
|
||||
@@ -16,6 +16,7 @@ The payment surface is split across provider-neutral payment routers, Request Ne
|
||||
| `/api/payment/request-network/*` | [`requestNetwork/requestNetworkRoutes.ts`](../../backend/src/services/payment/requestNetwork/requestNetworkRoutes.ts) | Request Network intent creation, in-house checkout payloads, webhook processing |
|
||||
| `/api/payment/derived-destinations/*` | [`wallets/derivedDestinationRoutes.ts`](../../backend/src/services/payment/wallets/derivedDestinationRoutes.ts) | Derived destination inspection, balance checks, and sweeping |
|
||||
| `/api/payment/decentralized/*` | [`decentralizedPaymentRoutes.ts`](../../backend/src/services/payment/decentralizedPaymentRoutes.ts) | Legacy wallet-direct confirmations |
|
||||
| `/api/payment/amn-scanner/*` | [`routes/amnScannerWebhookRoutes.ts`](../../backend/src/routes/amnScannerWebhookRoutes.ts) | AMN Pay Scanner webhook receiver |
|
||||
| `/api/admin/rn/networks/*` | [`requestNetwork/networkRegistryRoutes.ts`](../../backend/src/services/payment/requestNetwork/networkRegistryRoutes.ts) | Request Network chain/token registry |
|
||||
| `/api/admin/payments/awaiting-confirmation/*` | `awaitingConfirmationRoutes.ts` | Admin queue for payments waiting on confirmation/safety checks |
|
||||
|
||||
@@ -52,7 +53,7 @@ Core model: [[Payment]]. Coordination logic to avoid race conditions when multip
|
||||
|
||||
### POST /api/payment
|
||||
|
||||
**Description:** Create a payment record manually. Normal buyer checkout should use `POST /api/payment/request-network/intents`.
|
||||
**Description:** Create a payment record manually. Normal buyer checkout should use `POST /api/payment/request-network/pay-in`.
|
||||
**Auth required:** Bearer JWT
|
||||
**Request body:**
|
||||
```ts
|
||||
@@ -90,7 +91,7 @@ Core model: [[Payment]]. Coordination logic to avoid race conditions when multip
|
||||
|
||||
### GET /api/payment/:id
|
||||
|
||||
**Description:** Fetch a payment by id.
|
||||
**Description:** Fetch a payment by id. For payments with `provider: 'request.network'` that are still `pending`, this endpoint also performs an **on-demand RN reconcile**: it queries the Request Network node live, and if RN reports the request as paid it immediately marks the payment `completed`, advances the purchase request to `processing`, persists `selectedOfferId`, and accepts the winning offer while rejecting all others. This reconcile path exists because RN webhooks cannot reach a local dev server and the reconcile cron is not started there; the same logic fires in production as a safety net.
|
||||
**Auth required:** Bearer JWT
|
||||
**Errors:** `404` not found.
|
||||
|
||||
@@ -126,19 +127,19 @@ Core model: [[Payment]]. Coordination logic to avoid race conditions when multip
|
||||
### POST /api/payment/payments/:id/fetch-tx
|
||||
|
||||
**Description:** Re-queries the blockchain to fetch the missing `transactionHash` for a completed payment.
|
||||
**⚠️ SECURITY — NO AUTHENTICATION:** This endpoint has no authentication guard. Any unauthenticated caller can trigger a blockchain re-query for any payment ID.
|
||||
**Auth required:** Bearer JWT (admin) — `authenticateToken` + `authorizeRoles('admin')` added in commit `1d881c5` (ISSUE-005 fix).
|
||||
**Response 200:** `{ success, transactionHash, network, source, message }`
|
||||
|
||||
### POST /api/payment/payments/auto-fetch-missing
|
||||
|
||||
**Description:** Batch tx-hash backfill across the database.
|
||||
**⚠️ SECURITY — NO AUTHENTICATION:** This endpoint has no authentication guard. Any unauthenticated caller can trigger a full database backfill scan.
|
||||
**Auth required:** Bearer JWT (admin) — `authenticateToken` + `authorizeRoles('admin')` added in commit `1d881c5` (ISSUE-005 fix).
|
||||
**Request body:** `{ limit?: number }` (default 10)
|
||||
|
||||
### GET /api/payment/payments/:id/debug
|
||||
|
||||
**Description:** Debug bundle including the raw payment, blockchain metadata, and wallet-monitor status. Intended for admin / development.
|
||||
**⚠️ SECURITY — NO AUTHENTICATION:** Despite exposing full payment data, this endpoint has no authentication guard. Any unauthenticated caller can retrieve complete payment details for any payment ID.
|
||||
**Auth required:** Bearer JWT (admin) — `authenticateToken` + `authorizeRoles('admin')` added in commit `1d881c5` (ISSUE-005 fix).
|
||||
|
||||
### POST /api/payment/callback
|
||||
|
||||
@@ -153,9 +154,9 @@ Core model: [[Payment]]. Coordination logic to avoid race conditions when multip
|
||||
|
||||
## Request Network - Pay-in
|
||||
|
||||
### POST /api/payment/request-network/intents
|
||||
### POST /api/payment/request-network/pay-in
|
||||
|
||||
**Description:** Creates a Request Network pay-in intent and stores a [[Payment]] with `provider: "request.network"`. The service can attach a per-payment derived destination before creating the provider request.
|
||||
**Description:** Creates a Request Network pay-in intent and stores a [[Payment]] with `provider: "request.network"`. The service can attach a per-payment derived destination before creating the provider request. This is the **current active route** (mounted at `/api/payment/request-network/pay-in`). The `/intents` path listed in older docs is an alias; use `pay-in` for new integrations.
|
||||
**Auth required:** Bearer JWT (buyer)
|
||||
**Request body:**
|
||||
```ts
|
||||
@@ -182,7 +183,35 @@ Core model: [[Payment]]. Coordination logic to avoid race conditions when multip
|
||||
**Auth required:** No (signature-protected)
|
||||
**Response:** `200` when processed or duplicate; `202` when accepted but safety checks are pending; `401` for invalid signature.
|
||||
|
||||
> ⚠️ **NOT IMPLEMENTED:** `POST /api/payment/request-network/:id/payout/initiate`, `POST /api/payment/request-network/:id/payout/confirm`, `POST /api/payment/request-network/:id/release/confirm`, and `POST /api/payment/request-network/:id/refund/confirm` do not exist in the codebase. Do not call these paths.
|
||||
> [!note] RN payout/release/refund routes
|
||||
> `POST /api/payment/request-network/:paymentId/payout/initiate`, `POST /api/payment/request-network/:paymentId/payout/confirm`, `POST /api/payment/request-network/:paymentId/release/confirm`, and `POST /api/payment/request-network/:paymentId/refund/confirm` are registered in `requestNetworkRoutes.ts` but are stub-level implementations. They accept the request and return a 200 but do not yet drive the ledger-gated release/refund orchestration. Use `POST /api/payment/:id/release` and `POST /api/payment/:id/refund` for actual escrow releases.
|
||||
|
||||
## AMN Pay Scanner - Pay-in
|
||||
|
||||
AMN Pay Scanner is a custom in-house blockchain scanner that replaces the hosted Request Network page for payment monitoring. It speaks the same `PaymentProviderAdapter` interface as the RN adapter.
|
||||
|
||||
### POST /api/payment/amn-scanner/webhook
|
||||
|
||||
**Description:** AMN Pay Scanner posts settlement confirmations here. The route verifies a `webhookSecret`-based HMAC signature, then runs the Transaction Safety Provider and `PaymentCoordinator` pipeline identical to the RN webhook path.
|
||||
**Auth required:** No (signature-protected via `AMN_SCANNER_WEBHOOK_SECRET`)
|
||||
**Request body:** `{ intentId, status, transactionHash?, chainId?, ... }` — scanner-specific envelope
|
||||
**Response:** `200` processed; `401` bad signature; `400` missing `intentId` or unknown format; `404` payment not found.
|
||||
**Side effects:** Same as the RN webhook — updates [[Payment]], advances [[PurchaseRequest]], accepts/rejects offers, emits socket events when safety checks pass.
|
||||
|
||||
> [!note] Provider value
|
||||
> Payments created via the AMN Pay Scanner have `provider: 'amn.scanner'` in the database. This is distinct from `request.network` and `shkeeper`.
|
||||
|
||||
### GET /api/admin/scanner/status
|
||||
|
||||
**Description:** Proxies to `AMN_SCANNER_URL/scanner/status` and returns the scanner's internal state.
|
||||
**Auth required:** Bearer JWT (`admin`) — `authenticateToken` + `authorizeRoles('admin')` are now applied (the previously documented security gap — unauthenticated access — has been fixed in commit `1d881c5`).
|
||||
**Response 200:** Scanner status JSON forwarded from the upstream service.
|
||||
|
||||
### POST /api/admin/scanner/webhooks/retry
|
||||
|
||||
**Description:** Triggers a manual retry of failed/pending scanner webhooks.
|
||||
**Auth required:** Bearer JWT (`admin`)
|
||||
**Request body:** `{ intentId?: string }` — omit to retry all pending.
|
||||
|
||||
## Legacy SHKeeper - Pay-in
|
||||
|
||||
@@ -555,7 +584,13 @@ Escrow state (`escrowState`): `unfunded` → `funded` → `released` | `refunded
|
||||
}
|
||||
```
|
||||
|
||||
> ⚠️ **NOT IMPLEMENTED:** `GET /api/admin/settings/confirmation-thresholds/history` does not exist. Only the current-values GET and per-chain PATCH endpoints are implemented.
|
||||
### `GET /api/admin/settings/confirmation-thresholds/history`
|
||||
|
||||
**Auth:** Admin only
|
||||
**Description:** Returns paginated audit log of past confirmation threshold changes. Each entry records the admin who made the change, old/new threshold values, chain ID, and timestamp. Backed by the `ConfigSettingHistory` Mongoose model added in commit `27fb15a` (task #9).
|
||||
**Response 200:** `{ success: true, data: [{ chainId, oldThreshold, newThreshold, changedBy, changedAt }] }`
|
||||
|
||||
> **Note:** This endpoint was previously documented as NOT IMPLEMENTED. It was added in commit `27fb15a` and is now live at `/api/admin/settings/confirmation-thresholds/history`.
|
||||
|
||||
## Payments awaiting confirmation (admin)
|
||||
|
||||
@@ -613,7 +648,33 @@ Escrow state (`escrowState`): `unfunded` → `funded` → `released` | `refunded
|
||||
}
|
||||
```
|
||||
|
||||
> ⚠️ **NOT IMPLEMENTED:** `POST /api/admin/rn/networks/reload` and `POST /api/admin/rn/networks/probe/:chainId` do not exist in the codebase.
|
||||
### `POST /api/admin/rn/networks/reload`
|
||||
|
||||
**Auth:** Admin only
|
||||
**Description:** Reloads the chain and token registries from disk (`supportedChains.json` and `tokens.json`). Returns `{ success: true, message: 'Registry reloaded from disk' }`. Use this after updating the JSON files without restarting the server.
|
||||
|
||||
> **Note:** This route is now implemented (commit `5681abf`). Earlier docs incorrectly listed it as not implemented.
|
||||
|
||||
### `POST /api/admin/rn/networks/probe/:chainId`
|
||||
|
||||
**Auth:** Admin only
|
||||
**Description:** Performs a live on-chain probe for the specified chain: verifies RPC reachability, checks for deployed proxy contract bytecode (`eth_getCode`), and test-calls the proxy with a dummy payload to confirm it reverts meaningfully. Returns:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"chainId": 56,
|
||||
"reachable": true,
|
||||
"hasCode": true,
|
||||
"callValid": true,
|
||||
"blockNumber": "0x...",
|
||||
"latencyMs": 120
|
||||
}
|
||||
}
|
||||
```
|
||||
Errors: `400` if `chainId` is not a number; `404` if the chain is not in the registry; `500` on RPC failure.
|
||||
|
||||
> **Note:** This route is now implemented (commit `5681abf`). Earlier docs incorrectly listed it as not implemented.
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
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]
|
||||
---
|
||||
|
||||
> **Last updated:** 2026-05-29 — aligned with code (see Doc vs Code Audit Report)
|
||||
> **Last updated:** 2026-05-30 — break-glass mode added (commit `b21df25`)
|
||||
|
||||
# Trezor API
|
||||
|
||||
@@ -17,6 +17,12 @@ TREZOR_SAFEKEEPING_REQUIRED=false
|
||||
|
||||
Only the literal value `true` makes Trezor proof mandatory during release/refund confirmation. When unset or `false`, release/refund flows continue without Trezor proof.
|
||||
|
||||
## Break-glass mode
|
||||
|
||||
When `TREZOR_SAFEKEEPING_REQUIRED=true` and the Trezor is unavailable (lost, dead battery, etc.), an admin can activate break-glass mode to bypass Trezor for up to 1 hour. Break-glass state is in-memory only and resets on server restart.
|
||||
|
||||
See [[Admin API]] — _Break-glass (Trezor bypass)_ section for the three management endpoints (`GET`, `POST`, `DELETE /api/admin/settings/break-glass`). Activating break-glass fires an immediate Telegram alert via `tgNotify`.
|
||||
|
||||
## GET /api/trezor/registration-message
|
||||
|
||||
Builds the exact message the user must sign to register a Trezor xpub.
|
||||
|
||||
Reference in New Issue
Block a user