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>
7.8 KiB
Last updated: 2026-05-29 — aligned with code (see Doc vs Code Audit Report)
Trezor Safekeeping Flow
This flow adds hardware-backed custody controls without replacing the current payment model. The backend never stores private keys. Trezor support starts as a single hardware signer and is designed to upgrade to multisig later.
Default mode: optional. Existing release/refund flows do not require Trezor proof unless TREZOR_SAFEKEEPING_REQUIRED=true.
Note (corrected 2026-05-29): The frontend Trezor implementation does exist in current code — the 2026-05-29 audit's "zero frontend implementation" claim was based on an older snapshot. The active surface is:
src/app/dashboard/admin/trezor/page.tsx→TrezorSettingsView(registration + re-register UI)src/web3/trezor/trezorConnector.ts→ lazy-imports@trezor/connect-web(trezorGetXpub,trezorGetAddress,trezorSignMessage)src/components/trezor-sign-dialog/TrezorSignDialog.tsx→ build-instruction → sign-on-Trezor → enter-txHash → confirmsrc/actions/trezor.ts→ full API client (getTrezorAccount,getTrezorRegistrationMessage,registerTrezor,getTrezorOperationMessage,confirmRelease/confirmRefund) that builds thetrezor: { message, signature }objectThe legacy
confirmReleaseTx/confirmRefundTxhelpers insrc/actions/payment.tspost only{ txHash }(notrezorfield), but they have no UI callers — the active admin release/refund path goes throughTrezorSignDialog→actions/trezor.ts, which satisfies theassertTrezorSignatureForOperationguard whenTREZOR_SAFEKEEPING_REQUIRED=true.
Goals
- Generate a fresh receive address per user/payment from a registered Trezor xpub.
- Require a Trezor-produced signature before release/refund confirmation when safekeeping enforcement is enabled.
- Keep the Request Network payment adapter and legacy provider abstractions intact while adding custody controls.
- Preserve the existing
Paymentmodel and orchestration surface.
Actors
- Admin — the only party who can request operation messages and submit verify-operation calls. The registered Trezor must belong to an admin account; the safekeeping guard validates against the admin's
TrezorAccount.registrationAddress. - Any authenticated user — may call
POST /api/trezor/register(no role restriction on that endpoint).
Registration
- The Trezor owner (typically an admin) connects a Trezor and exports an Ethereum account xpub, for example
m/44'/60'/0'. - Backend builds a registration challenge:
GET /api/trezor/registration-message?xpub=...®istrationAddress=...
- The registration address must be the first derived address from the xpub:
m/44'/60'/0'/0/0
- The owner signs the challenge with that Trezor address.
- Frontend submits:
POST /api/trezor/registerxpubregistrationAddressproofMessageproofSignature- optional
basePath,deviceLabel
- Backend verifies:
- xpub is public, not private.
- registration address matches xpub-derived index
0. - signature recovers the registration address.
- Backend stores / updates the
TrezorAccountrecord. Upsert behaviour: if a record already exists for the user,xpub,basePath, andlabelare updated, butnextAddressIndexand the existingaddressesarray are preserved via$setOnInsert. Old address records continue to reference the previous xpub — a xpub mismatch is therefore possible after re-registration.
Address Generation
To issue the next payment address:
POST /api/trezor/addresses/next
{
"purpose": "deposit",
"paymentId": "..."
}
Valid values for purpose (as enumerated in the schema):
| Value | Description |
|---|---|
deposit |
Incoming payment address |
release |
Address used in a release operation |
refund |
Address used in a refund operation |
other |
General-purpose address |
The backend derives non-hardened receive addresses from the registered xpub:
m/44'/60'/0'/0/{index}
If a paymentId already has an address, the endpoint returns the same address instead of incrementing the index.
Transaction Approval (Admin-only)
POST /api/trezor/operation-message and POST /api/trezor/verify-operation are admin-only endpoints. Before a release/refund confirmation, the admin asks the backend for the exact operation message:
POST /api/trezor/operation-message
{
"operation": "release",
"paymentId": "...",
"transactionHash": "0x...",
"amount": 100,
"currency": "USDT",
"provider": "request.network"
}
The Trezor signs that message and the admin submits it. The frontend implements this flow via TrezorSignDialog, which calls getTrezorOperationMessage(), prompts the Trezor to sign, and then submits the release/refund confirmation through confirmRelease() / confirmRefund() in src/actions/trezor.ts with the full payload:
{
"txHash": "0x...",
"amount": 100,
"trezor": { "message": "<canonical operation message>", "signature": "0x..." }
}
The trezor object is included whenever a signature was produced, satisfying the backend assertTrezorSignatureForOperation guard. (The older confirmReleaseTx/confirmRefundTx helpers in src/actions/payment.ts post only { txHash }, but they are unused legacy code with no UI callers.)
Enforcement Flag
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
tgNotifyregardless ofTG_NOTIFY_BOT_TOKENset status. - The exported
isBreakGlassActive()helper is called byassertTrezorSignatureForOperation— whentrue, 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.
- Reject private extended keys at registration.
- Verify every signature locally before accepting it.
- Use exact transaction-intent messages; do not accept free-form signatures.
- Treat generated deposit addresses as public routing metadata, not as proof of payment.
- Keep ledger availability checks enabled for release/refund accounting.
Upgrade Path To Multisig
The current design stores a single trezor-eoa signer. The recommended production path is to replace the signer policy with:
addressType: safe-multisig- a Safe address per tenant/admin group
- threshold policy, such as
2-of-3 - Trezor owners as Safe signers
- release/refund flow creates a Safe transaction and records collected signatures before execution
The payment orchestration API should stay the same: build instruction, collect hardware-backed approval, confirm release/refund, append ledger entry. See PRD - Decentralized Custody and Smart-Contract Escrow Roadmap for the staged Safe-first path before any custom escrow contract.