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:
Siavash Sameni
2026-05-30 18:41:44 +04:00
parent eab1d77582
commit dceaf82934
153 changed files with 6276 additions and 179 deletions

View File

@@ -37,7 +37,8 @@ End-to-end specification for **email + password** authentication, JWT issuance,
2. **Client-side guards**: `signInWithPassword()` (`action.ts:32-116`) verifies the browser is online and `localStorage` is writable; otherwise it throws a typed `AuthErrorHandler` error.
3. **HTTP request**: The frontend POSTs `{ email, password }` to `POST /api/auth/login` (resolved by `endpoints.auth.login` in `frontend/src/lib/axios.ts`). An `AbortController` is armed with a 60-second timeout.
4. **Validation middleware** runs `loginValidation` (`backend/src/services/auth/authValidation.ts`) — wires into Express via `authRoutes.ts:22`.
5. **Rate limiting**: `rateLimitService.checkLoginAttempts(email, 5, 15*60)` is called (`authController.ts:173`). The counter is incremented on **every login attempt** (before password comparison), not only on failures. Once 5 total attempts accumulate within a 15-minute window, the endpoint returns `429 TOO_MANY_ATTEMPTS`. The counter is reset upon a fully successful login (step 9). Counters live in Redis so they survive restarts.
5. **Cloudflare Turnstile CAPTCHA gate** (`captchaGate` middleware, commit `b8edbbf`): Before the rate-limiter runs, `captchaGate` checks the in-memory failure counter for the caller's IP. If that IP has accumulated **3 or more failed login attempts** within 15 minutes, a valid `cf-turnstile-response` token must be present in the request body. Without it the endpoint returns `429 { captchaRequired: true }`. If `TURNSTILE_SECRET_KEY` is not set (local dev), the gate is skipped. On CAPTCHA pass, the middleware calls Cloudflare's `siteverify` endpoint to validate the token before proceeding.
5a. **Rate limiting**: `rateLimitService.checkLoginAttempts(email, 5, 15*60)` is called (`authController.ts:173`). The counter is incremented on **every login attempt** (before password comparison), not only on failures. Once 5 total attempts accumulate within a 15-minute window, the endpoint returns `429 TOO_MANY_ATTEMPTS`. The counter is reset upon a fully successful login (step 9). Counters live in Redis so they survive restarts.
6. **User lookup**: `User.findOne({ email, status: "active" }).select("+password")``password` is `select: false` by default in the schema and must be explicitly projected.
7. **Password comparison**: `authService.comparePassword()` invokes `bcrypt.compare()` (cost factor 12 — see `authService.ts:102-105`). Constant-time per bcrypt's design.
8. **Email-verification gate**: If `!user.isEmailVerified`, returns `403 EMAIL_NOT_VERIFIED` with `needsVerification: true`. The frontend intercepts this in `action.ts:104-111` and redirects to `/auth/jwt/verify?email=...`.

View File

@@ -12,7 +12,8 @@ audit: "2026-05-29 — corrected against source: Dispute.ts, DisputeService.ts,
When something goes wrong (item not delivered, wrong item, seller misbehaviour), either party can open a **dispute**. A three-way chat (buyer, seller, admin mediator) is created automatically. After evidence is gathered, the admin **resolves** the dispute — selecting an action such as refund, replacement, compensation, warning, or ban.
> [!danger] SECURITY — Three open privilege-escalation bugs exist as of this audit. See [Security Gaps](#security-gaps) below.
> [!success] Security fixes applied (2026-05-30)
> The three privilege-escalation bugs documented in the original Security Gaps section were fixed in commit `1d881c5` (ISSUE-003, ISSUE-004) and `fce8a19` (resolver role). Role guards are now enforced on assign/status/resolve; route shadowing is eliminated by remounting the release-hold router at `/api/disputes/pr`. See [Security Gaps](#security-gaps) for the historical record and current state.
> [!warning] Real-time events not implemented
> Every Socket.IO emit in `DisputeService` is currently commented out. No `dispute-updated`, `new-notification`, or any other socket event fires for dispute creation, admin assignment, status changes, evidence uploads, or resolution. The dispute feature is CRUD-only at this stage.
@@ -23,7 +24,8 @@ When something goes wrong (item not delivered, wrong item, seller misbehaviour),
- **Seller** — party against whom the dispute is raised (or in rarer cases, initiator).
- **Admin / Mediator** — assigned to investigate.
- **Frontend** — buyer/seller "Report issue" buttons in the request detail view; admin dispute dashboard.
- **Backend** — `DisputeService` (`backend/src/services/dispute/DisputeService.ts`), dashboard/controller routes at `backend/src/routes/disputeRoutes.ts` (mounted first at `/api/disputes`), and release-hold helpers in `backend/src/services/dispute/disputeRoutes.ts` (mounted second at `/api/disputes`).
- **Admin / Mediator** — assigned to investigate (role `admin` or `resolver`).
- **Backend** — `DisputeService` (`backend/src/services/dispute/DisputeService.ts`), dashboard/controller routes at `backend/src/routes/disputeRoutes.ts` (mounted at `/api/disputes`), and release-hold helpers in `backend/src/services/dispute/disputeRoutes.ts` (mounted at `/api/disputes/pr` since commit `1d881c5`).
- **MongoDB** — `disputes`, `chats`, `purchaserequests`, `payments`.
- **Socket.IO** — no events fire today; all emits are TODO stubs (see warning above).
@@ -78,59 +80,40 @@ Valid values: `product_quality | delivery_delay | wrong_item | payment_issue | s
---
## Security Gaps
## Security Gaps (Historical — All Closed as of 2026-05-30)
### 1. `PATCH /api/disputes/:id/status` — no role guard
The following bugs were identified in the 2026-05-29 audit and fixed in commits `1d881c5` and `fce8a19`. The descriptions below are preserved for historical reference and audit trail.
**File:** `backend/src/routes/disputeRoutes.ts` line 26
### 1. `PATCH /api/disputes/:id/status` — no role guard ✅ FIXED
```ts
router.patch('/:id/status', DisputeController.updateStatus);
```
**Fix:** `authorizeRoles('admin', 'resolver')` added in commit `fce8a19`. Only admins and resolvers can change dispute status.
Despite comments in the router saying "admin only", there is **no `authorizeRoles` middleware**. Any authenticated buyer or seller can call this endpoint and change a dispute's status to `resolved` or `closed`, bypassing the admin resolution flow entirely. This is an open privilege-escalation bug.
### 2. `POST /api/disputes/:id/resolve` (dashboard router) — no role guard ✅ FIXED
### 2. `POST /api/disputes/:id/resolve` (dashboard router) — no role guard
**Fix:** `authorizeRoles('admin', 'resolver')` added in commit `fce8a19`. Only admins and resolvers can resolve disputes.
**File:** `backend/src/routes/disputeRoutes.ts` line 29
**Additional fix (ISSUE-004, commit `1d881c5`):** `DisputeService.resolveDispute` now calls `releaseHoldResolve()` on the linked `purchaseRequestId`, clearing the escrow hold automatically so the payment release is unblocked after resolution.
```ts
router.post('/:id/resolve', DisputeController.resolveDispute);
```
### 3. `POST /api/disputes/:id/assign` — no role guard ✅ FIXED
No role guard. Any authenticated user can post a resolution — including `action: 'ban_seller'`. Note that the **release-hold router's** `POST /:purchaseRequestId/resolve` (`backend/src/services/dispute/disputeRoutes.ts` line 77) **does** correctly apply `authorizeRoles('admin')`. The dashboard router's resolve endpoint does not.
### 3. `POST /api/disputes/:id/assign` — no role guard
**File:** `backend/src/routes/disputeRoutes.ts` line 23
```ts
router.post('/:id/assign', DisputeController.assignAdmin);
```
Any authenticated user can call this with their own user ID in `{ adminId }` and self-assign as mediator for any dispute.
**Fix:** `authorizeRoles('admin', 'resolver')` added in commit `fce8a19`. Only admins and resolvers can assign mediators.
---
## Route Shadowing
## Route Shadowing (Historical — Resolved as of 2026-05-30)
Both routers are mounted at `/api/disputes` in `app.ts`:
Previously both routers were mounted at `/api/disputes`, causing the dashboard router to intercept release-hold requests. Fixed in commit `1d881c5` (ISSUE-003):
```ts
// app.ts line 521 — mounted FIRST
// app.ts — current state
app.use("/api/disputes", dashboardDisputeRoutes); // src/routes/disputeRoutes.ts
// app.ts line 585 — mounted SECOND
app.use("/api/disputes", disputeRoutes); // src/services/dispute/disputeRoutes.ts
app.use("/api/disputes/pr", disputeRoutes); // src/services/dispute/disputeRoutes.ts — new prefix
```
Express evaluates routes in registration order. This creates two concrete hazards:
1. **`POST /api/disputes/:id/resolve`** — the dashboard router (mounted first) exposes `POST /:id/resolve` with no role guard. A request intended for the release-hold router's `POST /:purchaseRequestId/resolve` (which **does** require admin) will be intercepted and handled by the wrong, unguarded handler when a matching dispute `_id` is supplied.
2. **`POST /api/disputes/:purchaseRequestId/raise`** — this route exists only in the second (release-hold) router. It will be reached correctly only if the dashboard router does not first match the path. Since the dashboard router has no `/raise` route, requests pass through. However, as more routes are added to either router, collisions will grow silently.
**Recommendation:** Separate the two routers onto distinct path prefixes (e.g. `/api/disputes` for the dashboard controller, `/api/disputes/hold` for the release-hold service).
Release-hold endpoints now use the `/api/disputes/pr/` prefix:
- `POST /api/disputes/pr/:purchaseRequestId/raise`
- `GET /api/disputes/pr/:purchaseRequestId/status`
- `POST /api/disputes/pr/:purchaseRequestId/resolve`
---
@@ -171,7 +154,7 @@ Express evaluates routes in registration order. This creates two concrete hazard
9. All three parties chat in the dispute chat room (same socket mechanics as [[Chat Flow]]). Each party can upload more evidence via `POST /api/disputes/:id/evidence``DisputeService.addEvidence` (`:305-337`) appends to `dispute.evidence[]` and writes a `timeline` entry `evidence_added`. **No socket event fires for evidence uploads.**
10. The admin may also `PATCH /api/disputes/:id/status` with intermediate states or notes; this updates `dispute.status` and writes a `timeline` entry `status_changed`. **No socket event fires.**
> [!danger] `PATCH /api/disputes/:id/status` has no role guard — any authenticated user can change dispute status (see [Security Gaps](#security-gaps)).
> [!note] `PATCH /api/disputes/:id/status` now requires `admin` or `resolver` role (`authorizeRoles('admin', 'resolver')`, commit `fce8a19`). The previously open privilege-escalation gap is closed.
### Phase 4 — Resolution
@@ -190,10 +173,11 @@ Express evaluates routes in registration order. This creates two concrete hazard
- `dispute.closedAt = now`
- Appends `timeline` entry `dispute_resolved`.
- Saves.
- **Calls `releaseHoldResolve(purchaseRequestId)`** — this clears the escrow hold automatically so the payment release is unblocked (ISSUE-004 fix, commit `1d881c5`).
- **No socket event fires.** (`// TODO: Send notifications via Socket.IO`)
13. **Financial side-effect (manual today):** depending on the action, the admin then triggers either the **release** ([[Payout Flow]] / [[Escrow Flow]]) or the **refund** as a separate step. The dispute service records the resolution; full automatic dispatch through the release/refund policy engine is still a hardening item.
13. **Financial side-effect:** as of commit `1d881c5` the escrow hold is cleared automatically on resolution. The admin still needs to separately trigger the ledger-gated release ([[Payout Flow]] / [[Escrow Flow]]) or refund for actual fund movement.
> [!danger] `POST /api/disputes/:id/resolve` (dashboard router) has no role guard — any authenticated user can post any resolution action including `ban_seller` (see [Security Gaps](#security-gaps)).
> [!note] `POST /api/disputes/:id/resolve` now requires `admin` or `resolver` role (`authorizeRoles('admin', 'resolver')`, commit `fce8a19`). The previously open privilege-escalation gap is closed.
---

View 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 |

View File

@@ -5,7 +5,7 @@ related_models: ["[[SellerOffer]]", "[[PurchaseRequest]]", "[[Notification]]"]
related_apis: ["POST /api/marketplace/purchase-requests/:id/offers", "GET /api/marketplace/purchase-requests/:id/offers", "PATCH /api/marketplace/offers/:id"]
---
> **Last updated:** 2026-05-29aligned 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-30updated for offer-management page, `withdrawOffer` action, edit-while-pending, `getSellerOffers` API (commits 240a668e7d1375)
# Seller Offer Flow
@@ -90,24 +90,22 @@ The valid `SellerOffer` statuses are `pending | accepted | rejected | withdrawn`
- Notifications: `notifyOfferAccepted` to the winning seller, generic rejection notifications to the others (`SellerOfferService.acceptOffer` does the same in the manual path).
- Socket events notify the winner and reject/close competing offers.
### Withdrawal
### Edit / withdrawal while awaiting buyer acceptance
17. ⚠️ **`POST /api/marketplace/offers/:id/withdraw` does NOT exist as an HTTP route.** The `SellerOfferService.withdrawOffer()` service method exists but is dead code — it is not wired to any controller endpoint.
17. While a request is in `received_offers` status (buyer has not yet accepted), the seller may **edit** their pending offer or **withdraw** it entirely from the request-detail step-2 card (`step-2-waiting-for-payment.tsx`).
The only supported HTTP way to withdraw an offer is:
- **Edit**: toggles `mode` to `'edit'` inside `Step2WaitingForPayment`, re-mounts `Step1SendProposal` pre-populated with the existing offer values. On save, calls `PATCH /api/marketplace/offers/:id` (via `updateOffer` action, which now correctly uses `PATCH` instead of the old `PUT`).
- **Withdraw**: opens a `ConfirmDialog`, then calls `withdrawOffer(offerId)` in `src/actions/marketplace.ts` which uses `PUT /api/marketplace/offers/:id/status` with `{ status: 'withdrawn' }`.
```
PUT /api/marketplace/offers/:id
Body: { status: 'withdrawn' }
```
`canManageOffer` is only `true` when `requestDetails?.status === 'received_offers'`; once the buyer accepts and the status advances, both buttons are hidden.
Note also that the frontend page `/dashboard/seller/marketplace/offers` (a "My Offers" listing) **does not exist**. Withdrawal must be triggered from the individual request detail page.
The DB filter `{ status: 'pending' }` inside `SellerOfferService.withdrawOffer` means withdrawal is impossible once `accepted` or `rejected`.
The DB filter `{ status: 'pending' }` inside `withdrawOffer` means withdrawal is impossible once `accepted` or `rejected`.
> ⚠️ `POST /api/marketplace/offers/:id/withdraw` still does **not** exist as an HTTP route. Always use `PUT /api/marketplace/offers/:id/status` with `{ status: 'withdrawn' }`.
### Offer update — method mismatch
### Offer update — method mismatch resolved
> ⚠️ **Known mismatch**: The frontend sends `PUT /marketplace/offers/:id` to update an offer, but the backend route is registered as `PATCH /api/marketplace/offers/:id` (`marketplaceControllerRoutes.ts`). Depending on whether a proxy or middleware normalises the method, one of these may fail. Verify end-to-end and align to a single method.
> ✅ **Fixed (commit 240a668)**: The frontend `updateOffer` action now sends `PATCH /api/marketplace/offers/:id`, matching the backend. The `acceptOffer` action was also corrected from `PUT` to `PATCH`.
## Sequence diagram
@@ -157,10 +155,10 @@ sequenceDiagram
| `POST` | `/api/marketplace/purchase-requests/:id/offers` | Create offer | `purchaseRequestId` is a path param |
| `GET` | `/api/marketplace/purchase-requests/:id/offers` | Buyer view of offers on a request | |
| `GET` | `/api/marketplace/offers/:id` | Single offer details | |
| `PATCH` | `/api/marketplace/offers/:id` | Update price / ETA / notes (seller, while pending) | ⚠️ Frontend sends `PUT`; backend registers `PATCH` — method mismatch |
| `PATCH` | `/api/marketplace/offers/:id` | Update price / ETA / notes (seller, while pending) | Fixed: frontend now sends `PATCH` |
| `POST` | `/api/marketplace/offers/:id/accept` | Manual acceptance (when not via webhook) | |
| ~~`GET /api/marketplace/offers/seller/:sellerId`~~ | — | ~~Seller's own offer history~~ | ⚠️ NOT IMPLEMENTED — `getOffersBySeller()` service method exists but has no HTTP route |
| ~~`POST /api/marketplace/offers/:id/withdraw`~~ | — | ~~Seller withdraws~~ | ⚠️ NOT IMPLEMENTED — use `PATCH /api/marketplace/offers/:id` with `{ status: 'withdrawn' }` instead |
| `GET` | `/api/marketplace/offers/seller/:sellerId` | All offers by this seller (used by Offer Management page) | Implemented via `getSellerOffers` frontend action (commit 240a668) |
| `PUT` | `/api/marketplace/offers/:id/status` | Status mutation — use `{ status: 'withdrawn' }` to withdraw | The only HTTP withdraw path; `POST /api/marketplace/offers/:id/withdraw` does **not** exist |
## Database writes
@@ -211,6 +209,9 @@ sequenceDiagram
- Backend: `backend/src/services/marketplace/marketplaceController.ts`
- Backend: `backend/src/models/SellerOffer.ts`
- Backend: `backend/src/services/payment/paymentCoordinator.ts` (payment-state cascade)
- Frontend: `frontend/src/sections/request/components/seller-steps/step-1-send-proposal.tsx`
- Frontend: `frontend/src/sections/request/components/seller-steps/step-1-send-proposal.tsx` — proposal form (also re-used for edit)
- Frontend: `frontend/src/sections/request/components/seller-steps/step-2-waiting-for-payment.tsx` — awaiting-buyer card with edit/withdraw actions
- Frontend: `frontend/src/sections/request/components/buyer-steps/step-3-select-and-pay.tsx`
- Frontend: `frontend/src/app/dashboard/seller/marketplace/`
- Frontend: `frontend/src/app/dashboard/seller/marketplace/` — seller marketplace browse
- Frontend: `frontend/src/app/dashboard/seller/marketplace/offers/page.tsx` — Offer Management page (all offers, status filter, withdraw)
- Frontend: `frontend/src/actions/marketplace.ts``withdrawOffer`, `getSellerOffers` actions

View File

@@ -112,6 +112,24 @@ TREZOR_SAFEKEEPING_REQUIRED=false
Default is permissive so existing Request Network release/refund flows continue to work. Set it to `true` only after registering the operating admin's Trezor account (the frontend signing flow via `TrezorSignDialog` is already implemented). Any value other than the literal string `true` is treated as disabled.
## Break-Glass Mode (Emergency Bypass)
When `TREZOR_SAFEKEEPING_REQUIRED=true` but the Trezor device is unavailable (lost, hardware fault, key-holder absent), an admin can activate **break-glass mode** to temporarily bypass the safekeeping requirement:
| Endpoint | Action |
|---|---|
| `GET /api/admin/settings/break-glass` | Read current status (`active`, `expiresAt`, `activatedBy`) |
| `POST /api/admin/settings/break-glass` | Activate for **1 hour** — fires a Telegram alarm immediately |
| `DELETE /api/admin/settings/break-glass` | Cancel before expiry |
**Properties:**
- State is in-memory only (resets on server restart — intentional).
- Activation fires a Telegram alert via `tgNotify` regardless of `TG_NOTIFY_BOT_TOKEN` set status.
- The exported `isBreakGlassActive()` helper is called by `assertTrezorSignatureForOperation` — when `true`, the signature check is skipped.
- Maximum duration: 1 hour. After expiry the guard is automatically re-enabled.
**Source:** `backend/src/services/admin/breakGlassRoutes.ts` (commit `b21df25`).
## Safety Rules
- Never store Trezor seed words, private keys, or xprv/tprv values.