Compare commits

...

5 Commits

Author SHA1 Message Date
Siavash Sameni
873a57874e Update docs for TON proof backend, Mini App fixes, and EVM wallet stub
PRD - TON Wallet Ownership Proof.md:
- Status updated from ready-to-implement -> backend-implemented.
- Added Implementation Status section documenting what is complete
  (challenge endpoint, tonProofService.ts, User model fields, 15 tests)
  and what remains (frontend proof wiring, verified badge).
- Acceptance criteria updated: backend items checked, frontend pending.

Handoff - Telegram Mini App Debug - 2026-05-24.md:
- New Implemented sections for session 3:
  - TON Wallet Ownership Proof backend (full detail of tonProofService,
    userController changes, User model fields, 15 unit tests).
  - Telegram Mini App shell dir="ltr" + smart displayName fallback.
  - Socket status suppressed on /telegram paths.
  - EVM WalletConnect stub card (disabled until project ID configured).
- Known Issues: TON proof updated to "frontend wiring pending";
  EVM WalletConnect section added with activation steps.
- Current Git State updated to Session 3.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 20:21:29 +04:00
Siavash Sameni
940ad0c655 Add full system audit reports and Telegram Mini App debug handoff
- Three-stream audit (security / logic / performance) with 35+ findings
  derived from actual source code, each with file:line and remediation
- Audit Index cross-references criticals across streams into prioritized
  fix tiers: immediately / before soft launch / before public launch
- Telegram Mini App debug handoff documenting what was implemented and
  all remaining work items with exact file lists and test commands
- Updated architecture, data model, auth API, and registration flow docs
  to reflect Telegram auth, TON wallet, and email verification additions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 17:20:08 +04:00
Siavash Sameni
2533bedb91 Update Telegram auth verification report 2026-05-24 16:15:40 +04:00
Siavash Sameni
fa7234cbe1 Document Telegram first-class auth 2026-05-24 16:12:46 +04:00
Siavash Sameni
7651d69811 Document telegram-native task 5 foundation 2026-05-24 13:19:54 +04:00
31 changed files with 3510 additions and 47 deletions

View File

@@ -0,0 +1,146 @@
# PRD: Telegram Phone-Number Authentication
## Problem
The current Telegram integration treats Telegram as a *secondary identity layer*: a user must already hold an Amanat email-password (or Google OAuth) account before they can link their Telegram identity. This creates unnecessary friction for users who arrive from Telegram, have never heard of Amanat, and reasonably expect the Mini App to just work — as every other well-built Telegram Mini App does.
Telegram accounts are phone-number-verified by Telegram itself. When the backend successfully verifies Mini App `initData`, it has already established that the requester holds a specific Telegram account tied to a phone number. That is sufficient identity to create and authenticate an Amanat account without asking for an email or password.
## Goal
Allow users to sign in to Amanat using their Telegram identity as the primary credential — with zero separate signup step. This applies to:
1. **Telegram Mini App context**`initData` is auto-available and already cryptographically verified.
2. **Telegram Login Widget** — the standard web widget that lets users authenticate via Telegram on any browser page; returns a signed payload verifiable against the bot token.
In both cases the user's phone-verification is owned by Telegram; Amanat trusts the signed assertion and does not re-verify the phone number itself.
## Platform assumptions
- Telegram Mini App `initData` is signed with HMAC-SHA-256 keyed on `HMAC-SHA-256("WebAppData", BOT_TOKEN)`. Server-side verification is already implemented in `telegramService.ts`.
- Telegram Login Widget returns `{id, first_name, last_name, username, photo_url, auth_date, hash}`. Hash is `HMAC-SHA-256(data_check_string, SHA256(BOT_TOKEN))`. The same bot token covers both flows.
- Telegram user IDs are stable, globally unique integers that do not change even if the user changes username or phone number.
- Telegram does not expose the raw phone number to third-party apps — the ID is the stable identity anchor.
Reference docs:
- Telegram Login Widget: https://core.telegram.org/widgets/login
- Mini Apps initData: https://core.telegram.org/bots/webapps#validating-data-received-via-the-mini-app
## What changes
### Backend
**New endpoint: `POST /auth/telegram`**
Accepts one of:
- `{ initData: string }` — from Mini App context
- `{ id, first_name, last_name, username, photo_url, auth_date, hash }` — from Login Widget
Steps:
1. Verify signature (reuse `verifyMiniAppInitData` for initData path; add `verifyTelegramLoginWidget` for widget path).
2. Extract `telegramUserId` (stable integer).
3. Look up `TelegramLink` by `telegramUserId`.
4. **If link found and active** → load the linked Amanat user → issue JWT + refresh token (same format as `POST /auth/login`).
5. **If no link found** → auto-provision a new Amanat user:
- `email`: `null` (nullable; add index exclusion for null values)
- `firstName` / `lastName`: from Telegram profile
- `username`: `tg_<telegramUserId>` as stable internal handle
- `role`: `buyer` (default; user can change)
- `isEmailVerified`: `false` — set a flag `telegramVerified: true` instead
- `status`: `active`
- `authProvider`: `telegram`
- Create `TelegramLink` record in the same transaction.
- Issue JWT + refresh token.
6. On success, always upsert `TelegramLink.lastSeenAt` and update name/username fields from latest Telegram profile.
7. Return: `{ token, refreshToken, user, isNewUser: boolean }``isNewUser: true` signals the frontend to show an onboarding nudge (optional email capture, preferred language, currency).
**User model changes:**
- `email`: make nullable (`type: String, sparse: true` — allows multiple null values in a unique sparse index)
- Add `telegramVerified: Boolean` (default `false`)
- Add `authProvider: 'email' | 'google' | 'telegram'` field
- Existing email-based users are unaffected; their `authProvider` defaults to `'email'`
**Rate limiting and security:**
- Apply the same replay protection already in `telegramService.ts` to this endpoint.
- Rate limit: 10 requests per IP per minute, 5 per Telegram user ID per minute.
- Log all auto-provisioning events as audit records.
- Reject `auth_date` older than `TELEGRAM_MINIAPP_MAX_AGE_MS` (already configurable).
**Blocked account handling:**
- If `TelegramLink.status === 'blocked'` → return 403 with `ACCOUNT_BLOCKED` code.
- If the linked Amanat user is suspended/deleted → return 403 with `ACCOUNT_SUSPENDED`.
### Frontend
**Mini App auto-login (inside Telegram):**
When `window.Telegram?.WebApp?.initData` is non-empty:
1. Skip the login page entirely.
2. POST `initData` to `/auth/telegram`.
3. Store the returned JWT in the existing auth context.
4. If `isNewUser === true`, show a lightweight onboarding overlay inside the Mini App to capture optional email and preferred settings before routing to the main app.
**Web login page — "Continue with Telegram" button:**
- Add a "Continue with Telegram" button alongside the existing Google button.
- Clicking it opens the Telegram Login Widget in a popup (`window.open` or inline script tag method).
- On callback, POST the widget payload to `/auth/telegram`.
- This works on any browser, not only inside Telegram.
**Auth types update:**
```typescript
// auth/types.ts additions
export interface User {
// ... existing fields ...
telegramVerified: boolean;
authProvider: 'email' | 'google' | 'telegram';
telegramUsername?: string;
}
```
**Onboarding nudge (post-Telegram-auth):**
New users created via Telegram auth should be shown a non-blocking screen:
- Optional: "Add an email for account recovery" (not required)
- Optional: preferred language and currency
- Skippable — routing to the main app without completing it is fine
## Non-goals
- Do not expose the user's raw phone number.
- Do not require email for Telegram-authenticated users.
- Do not use Telegram auth as a bypass for high-risk actions (release, payout address change, dispute resolution still require step-up confirmation as per task 5.6/5.8 policy).
- Do not auto-merge a Telegram-provisioned account with an existing email account unless the user explicitly initiates account linking from settings.
## Account merge and collision handling
| Scenario | Behavior |
|---|---|
| Telegram user arrives, no existing account | Auto-provision, create TelegramLink, return `isNewUser: true` |
| Telegram user arrives, TelegramLink already exists | Auth as linked user, update last-seen |
| Email user opens Mini App, not yet linked | Show "Link your Telegram" prompt (existing task 5.2 flow) — do NOT auto-merge |
| Two Telegram accounts try to link to same email user | Reject second; return 409 DUPLICATE_TELEGRAM_LINK |
| Same Telegram user tries to auth with two different app accounts | Impossible by design — TelegramLink.telegramUserId is unique |
## Acceptance criteria
1. A new Telegram user can complete authentication inside the Mini App without entering an email or password.
2. A returning Telegram user gets the same JWT session whether they authenticate via Mini App `initData` or the web Login Widget.
3. `POST /auth/telegram` rejects replayed `initData` within the existing replay window.
4. `POST /auth/telegram` rejects `auth_date` older than the configured max-age.
5. Blocked Telegram accounts receive a 403 response; they cannot circumvent it by unlinking and re-linking.
6. Auto-provisioned users have `authProvider: 'telegram'` and no email; existing email users are unaffected.
7. Admin UI (task 5.7) can distinguish `authProvider: telegram` users and shows `telegramVerified` status.
8. A Telegram-authed user who later adds an email can then also log in via email — both paths converge to the same user record.
9. High-risk actions still require the step-up policy regardless of auth provider.
10. The "Continue with Telegram" button is visible on the web login page in non-Mini-App contexts.
## Dependencies
- Task 5.2 (identity linking model) — TelegramLink model and verifyMiniAppInitData are already done; this task extends the auth path, not the linking model.
- Task 5.8 (security controls) — replay protection, rate limits, and audit logging from that task apply here too.
- Task 5.6 (high-risk action policy) — must not be weakened.

View File

@@ -19,3 +19,4 @@ Subtasks:
7. Add admin and support surface for Telegram-originated cases. 7. Add admin and support surface for Telegram-originated cases.
8. Add security, compliance, and abuse controls for Telegram. 8. Add security, compliance, and abuse controls for Telegram.
9. Prepare QA, rollout, analytics, and launch operations. 9. Prepare QA, rollout, analytics, and launch operations.
10. Implement Telegram as first-class authentication provider. ✅ Done 2026-05-24.

View File

@@ -487,67 +487,71 @@
"title": "Define Telegram product surface and flow map", "title": "Define Telegram product surface and flow map",
"description": "Document which Amanat workflows live in bot messages, which live in the Mini App, and which remain web/admin-only for first release.", "description": "Document which Amanat workflows live in bot messages, which live in the Mini App, and which remain web/admin-only for first release.",
"details": "Map buyer, seller, admin/support, unauthenticated, linked-user, and unlinked-user journeys. Specify deep-link entry points for request details, offer review, payment, dispute, delivery evidence, and account linking. Separate first-release scope from later enhancements and map every Telegram action to backend API/state transitions.", "details": "Map buyer, seller, admin/support, unauthenticated, linked-user, and unlinked-user journeys. Specify deep-link entry points for request details, offer review, payment, dispute, delivery evidence, and account linking. Separate first-release scope from later enhancements and map every Telegram action to backend API/state transitions.",
"status": "in-progress", "status": "done",
"dependencies": [], "dependencies": [],
"priority": "high", "priority": "high",
"testStrategy": "See Telegram-native PRD acceptance criteria.", "testStrategy": "See Telegram-native PRD acceptance criteria.",
"parentId": "undefined", "parentId": "undefined",
"updatedAt": "2026-05-24T06:12:47.328Z" "updatedAt": "2026-05-24T09:18:11.077Z"
}, },
{ {
"id": 2, "id": 2,
"title": "Build Telegram identity linking and session model", "title": "Build Telegram identity linking and session model",
"description": "Implement secure account linking between Telegram users and Amanat accounts.", "description": "Implement secure account linking between Telegram users and Amanat accounts.",
"details": "Backend must verify Telegram Mini App initData before creating a Telegram session. Store an auditable Telegram user ID to Amanat user link. Support existing users, new users, unlinking, blocked accounts, duplicate-link attempts, session expiry, replay protection, rate limits, and audit logs.", "details": "Backend must verify Telegram Mini App initData before creating a Telegram session. Store an auditable Telegram user ID to Amanat user link. Support existing users, new users, unlinking, blocked accounts, duplicate-link attempts, session expiry, replay protection, rate limits, and audit logs.",
"status": "pending", "status": "done",
"dependencies": [ "dependencies": [
1 1
], ],
"priority": "high", "priority": "high",
"testStrategy": "See Telegram-native PRD acceptance criteria.", "testStrategy": "See Telegram-native PRD acceptance criteria.",
"parentId": "undefined" "parentId": "undefined",
"updatedAt": "2026-05-24T09:18:13.054Z"
}, },
{ {
"id": 3, "id": 3,
"title": "Implement bot command and notification foundation", "title": "Implement bot command and notification foundation",
"description": "Create the Telegram bot backend for commands, inline keyboards, callback queries, deep links, and outbound notifications.", "description": "Create the Telegram bot backend for commands, inline keyboards, callback queries, deep links, and outbound notifications.",
"details": "Support start/help/link/status/request/offer/payment/dispute/settings basics. Use short opaque IDs or signed tokens for callback payloads. Process incoming updates idempotently with rate limits. Respect notification preferences, quiet/error states, failed delivery, blocked bot, and retry observability.", "details": "Support start/help/link/status/request/offer/payment/dispute/settings basics. Use short opaque IDs or signed tokens for callback payloads. Process incoming updates idempotently with rate limits. Respect notification preferences, quiet/error states, failed delivery, blocked bot, and retry observability.",
"status": "pending", "status": "done",
"dependencies": [ "dependencies": [
1, 1,
2 2
], ],
"priority": "high", "priority": "high",
"testStrategy": "See Telegram-native PRD acceptance criteria.", "testStrategy": "See Telegram-native PRD acceptance criteria.",
"parentId": "undefined" "parentId": "undefined",
"updatedAt": "2026-05-24T13:46:14.458Z"
}, },
{ {
"id": 4, "id": 4,
"title": "Build Telegram Mini App shell for marketplace workflows", "title": "Build Telegram Mini App shell for marketplace workflows",
"description": "Deliver the mobile-first Mini App that gives users the full Amanat workflow surface inside Telegram.", "description": "Deliver the mobile-first Mini App that gives users the full Amanat workflow surface inside Telegram.",
"details": "Use Telegram theme, safe-area, viewport, back button, haptics, and main/bottom button patterns. Support browsing requests, creating/editing requests, reviewing offers, payment state, evidence uploads, delivery actions, and dispute actions. Launch from bot profile, menu button, inline buttons, and direct links with startapp context. Handle unlinked accounts, expired sessions, unsupported clients, and fallback web links.", "details": "Use Telegram theme, safe-area, viewport, back button, haptics, and main/bottom button patterns. Support browsing requests, creating/editing requests, reviewing offers, payment state, evidence uploads, delivery actions, and dispute actions. Launch from bot profile, menu button, inline buttons, and direct links with startapp context. Handle unlinked accounts, expired sessions, unsupported clients, and fallback web links.",
"status": "pending", "status": "in-progress",
"dependencies": [ "dependencies": [
1, 1,
2 2
], ],
"priority": "high", "priority": "high",
"testStrategy": "See Telegram-native PRD acceptance criteria.", "testStrategy": "See Telegram-native PRD acceptance criteria.",
"parentId": "undefined" "parentId": "undefined",
"updatedAt": "2026-05-24T09:18:16.954Z"
}, },
{ {
"id": 5, "id": 5,
"title": "Add Telegram payment and wallet strategy", "title": "Add Telegram payment and wallet strategy",
"description": "Evaluate and implement safe payment entry points for Telegram-native users without weakening escrow accounting.", "description": "Evaluate and implement safe payment entry points for Telegram-native users without weakening escrow accounting.",
"details": "Compare Bot API payments/Stars, Wallet Pay, TON Pay, TON Connect, Request Network links, and existing crypto checkout. Select a first payment path and document rejected options. Store provider, Telegram user ID, deep-link source, payment reference, invoice/order/request ID, currency, amount, expiration, and idempotency key. Wallet/TON flows must validate recipient, asset, amount, memo/reference, confirmation status, and reconciliation evidence before crediting escrow. Refund/release behavior must remain compatible with canonical ledger and dispute holds.", "details": "Compare Bot API payments/Stars, Wallet Pay, TON Pay, TON Connect, Request Network links, and existing crypto checkout. Select a first payment path and document rejected options. Store provider, Telegram user ID, deep-link source, payment reference, invoice/order/request ID, currency, amount, expiration, and idempotency key. Wallet/TON flows must validate recipient, asset, amount, memo/reference, confirmation status, and reconciliation evidence before crediting escrow. Refund/release behavior must remain compatible with canonical ledger and dispute holds.",
"status": "pending", "status": "done",
"dependencies": [ "dependencies": [
2, 2,
4 4
], ],
"priority": "high", "priority": "high",
"testStrategy": "See Telegram-native PRD acceptance criteria.", "testStrategy": "See Telegram-native PRD acceptance criteria.",
"parentId": "undefined" "parentId": "undefined",
"updatedAt": "2026-05-24T09:18:18.909Z"
}, },
{ {
"id": 6, "id": 6,
@@ -583,7 +587,7 @@
"title": "Add security, compliance, and abuse controls for Telegram", "title": "Add security, compliance, and abuse controls for Telegram",
"description": "Threat-model the Telegram surface and add controls before launch.", "description": "Threat-model the Telegram surface and add controls before launch.",
"details": "Cover forged init data, callback replay, deep-link parameter tampering, phishing links, bot token leakage, spam, account takeover, wallet spoofing, fake payment proof, and support impersonation. Document secrets, bot webhook endpoints, Wallet Pay keys, TON Connect manifest, CORS, CSP, allowed origins, rate limits, and monitoring for update failures, abnormal callbacks, payment mismatches, blocked notifications, and suspicious wallet activity.", "details": "Cover forged init data, callback replay, deep-link parameter tampering, phishing links, bot token leakage, spam, account takeover, wallet spoofing, fake payment proof, and support impersonation. Document secrets, bot webhook endpoints, Wallet Pay keys, TON Connect manifest, CORS, CSP, allowed origins, rate limits, and monitoring for update failures, abnormal callbacks, payment mismatches, blocked notifications, and suspicious wallet activity.",
"status": "pending", "status": "done",
"dependencies": [ "dependencies": [
2, 2,
3, 3,
@@ -592,14 +596,15 @@
], ],
"priority": "high", "priority": "high",
"testStrategy": "See Telegram-native PRD acceptance criteria.", "testStrategy": "See Telegram-native PRD acceptance criteria.",
"parentId": "undefined" "parentId": "undefined",
"updatedAt": "2026-05-24T09:18:24.717Z"
}, },
{ {
"id": 9, "id": 9,
"title": "Prepare QA, rollout, analytics, and launch operations", "title": "Prepare QA, rollout, analytics, and launch operations",
"description": "Prepare the Telegram app and bot for controlled release.", "description": "Prepare the Telegram app and bot for controlled release.",
"details": "Test Telegram iOS, Android, Desktop, Web, light/dark themes, compact/fullscreen modes, slow network, blocked bot, expired sessions, and payment cancellation. Keep sandbox/test bot and production bot environments separated. Roll out through feature flags, internal allowlist, beta cohort, and production enablement. Track activation, linked accounts, request creation, offer response, payment start/completion, dispute activity, release approval, and notification opt-outs. Add runbooks for bot outage, Telegram API outage, payment provider outage, stuck payment, duplicate callback, suspicious wallet proof, and compromised bot token.", "details": "Test Telegram iOS, Android, Desktop, Web, light/dark themes, compact/fullscreen modes, slow network, blocked bot, expired sessions, and payment cancellation. Keep sandbox/test bot and production bot environments separated. Roll out through feature flags, internal allowlist, beta cohort, and production enablement. Track activation, linked accounts, request creation, offer response, payment start/completion, dispute activity, release approval, and notification opt-outs. Add runbooks for bot outage, Telegram API outage, payment provider outage, stuck payment, duplicate callback, suspicious wallet proof, and compromised bot token.",
"status": "pending", "status": "done",
"dependencies": [ "dependencies": [
3, 3,
4, 4,
@@ -610,15 +615,31 @@
], ],
"priority": "high", "priority": "high",
"testStrategy": "See Telegram-native PRD acceptance criteria.", "testStrategy": "See Telegram-native PRD acceptance criteria.",
"parentId": "undefined" "parentId": "undefined",
"updatedAt": "2026-05-24T09:18:26.638Z"
},
{
"id": 10,
"title": "Implement Telegram as first-class authentication provider",
"description": "Add a POST /auth/telegram endpoint and frontend login flow so users can authenticate with Amanat using only their Telegram identity — no email or password required.",
"details": "Source PRD: .taskmaster/docs/prd-telegram-phone-auth.md. Backend: create POST /auth/telegram that accepts Mini App initData or Telegram Login Widget payload, verifies the signature (reuse verifyMiniAppInitData; add verifyTelegramLoginWidget for the widget path), looks up TelegramLink by telegramUserId, and either authenticates the linked user or auto-provisions a new Amanat account (authProvider: telegram, telegramVerified: true, nullable email via sparse unique index). Returns JWT + refreshToken + isNewUser flag. Apply existing replay protection and rate limits. User model: make email nullable (sparse index), add authProvider and telegramVerified fields. Frontend: auto-detect Telegram Mini App context and skip login page; POST initData to /auth/telegram; show lightweight onboarding overlay for new users (optional email, language, currency). Add 'Continue with Telegram' button on web login page alongside Google OAuth. Security: blocked Telegram accounts return 403 regardless of re-linking attempts; high-risk action step-up policy is unchanged; never expose raw phone number.",
"status": "done",
"dependencies": [
2,
8
],
"priority": "high",
"testStrategy": "Verify: new Telegram user auto-provisions and receives JWT; returning user authenticates via both initData and Login Widget; replayed initData is rejected; stale auth_date is rejected; blocked account returns 403; existing email-password users are unaffected; email remains optional (not required) for Telegram-authed users; isNewUser flag triggers onboarding overlay; high-risk actions still require step-up confirmation.\n\nImplemented verification: backend typecheck; backend targeted Jest __tests__/telegram-auth.test.ts and __tests__/telegram-service.test.ts; frontend targeted Jest __tests__/auth/telegram-auth-action.test.ts and __tests__/sections/telegram/telegram-mini-app-shell.test.tsx. Full frontend typecheck still has unrelated pre-existing payment icon/payload errors outside Task 5.10.",
"parentId": "5",
"updatedAt": "2026-05-24T11:59:32.372Z"
} }
], ],
"updatedAt": "2026-05-24T06:12:47.328Z" "updatedAt": "2026-05-24T13:46:14.458Z"
} }
], ],
"metadata": { "metadata": {
"version": "1.0.0", "version": "1.0.0",
"lastModified": "2026-05-24T07:23:44.643Z", "lastModified": "2026-05-24T13:46:14.458Z",
"taskCount": 5, "taskCount": 5,
"completedCount": 4, "completedCount": 4,
"tags": [ "tags": [

View File

@@ -38,10 +38,11 @@ flowchart TB
subgraph BE["Backend tier — Node.js / Express 5"] subgraph BE["Backend tier — Node.js / Express 5"]
REST["REST API<br/>/api/*"] REST["REST API<br/>/api/*"]
SocketS["Socket.IO server<br/>rooms per user / chat / request"] SocketS["Socket.IO server<br/>rooms per user / chat / request"]
Auth["Auth service<br/>JWT + Passkey + Google"] Auth["Auth service<br/>JWT + Passkey + Google + Telegram"]
Market["Marketplace service<br/>Requests, Offers, Templates"] Market["Marketplace service<br/>Requests, Offers, Templates"]
ChatSvc["Chat service"] ChatSvc["Chat service"]
PaySvc["Payment service<br/>+ PaymentCoordinator"] PaySvc["Payment service<br/>SHKeeper + Request Network + ledger"]
TelegramSvc["Telegram service<br/>bot + Mini App + notifications"]
Disp["Dispute service"] Disp["Dispute service"]
Points["Points / Referrals"] Points["Points / Referrals"]
BlogSvc["Blog service"] BlogSvc["Blog service"]
@@ -65,6 +66,8 @@ flowchart TB
Google["Google OAuth"] Google["Google OAuth"]
Sentry["Sentry"] Sentry["Sentry"]
Alchemy["Alchemy RPC"] Alchemy["Alchemy RPC"]
TelegramAPI["Telegram Bot API<br/>+ Mini App"]
ReqNet["Request Network<br/>pay-in / webhooks"]
end end
Browser --> SSR Browser --> SSR
@@ -78,20 +81,25 @@ flowchart TB
ClientJS --> REST ClientJS --> REST
SocketC <--> SocketS SocketC <--> SocketS
REST --> Auth & Market & ChatSvc & PaySvc & Disp & Points & BlogSvc & AISvc & Notif & Files REST --> Auth & Market & ChatSvc & PaySvc & TelegramSvc & Disp & Points & BlogSvc & AISvc & Notif & Files
SocketS --> ChatSvc & Notif & Market SocketS --> ChatSvc & Notif & Market
Auth & Market & ChatSvc & PaySvc & Disp & Points & BlogSvc --> Mongo Auth & Market & ChatSvc & PaySvc & Disp & Points & BlogSvc & TelegramSvc --> Mongo
Auth & PaySvc & Notif --> RedisDB Auth & PaySvc & Notif --> RedisDB
Files --> Disk Files --> Disk
PaySvc <--> SHK PaySvc <--> SHK
SHK -.webhook.-> PaySvc SHK -.webhook.-> PaySvc
PaySvc <--> ReqNet
ReqNet -.webhook.-> PaySvc
PaySvc --> Chain PaySvc --> Chain
Wagmi --> DePay Wagmi --> DePay
DePay --> Chain DePay --> Chain
PaySvc -.tx fetch.-> Alchemy PaySvc -.tx fetch.-> Alchemy
TelegramSvc <--> TelegramAPI
TelegramAPI -.webhook.-> TelegramSvc
Auth --> TelegramAPI
Notif --> SMTP Notif --> SMTP
Auth --> Google Auth --> Google
AISvc --> OpenAI AISvc --> OpenAI
@@ -103,11 +111,12 @@ flowchart TB
### Authentication & identity — [[Authentication Flow]] ### Authentication & identity — [[Authentication Flow]]
Auth is the gate to every authenticated route. Amn supports three login methods in parallel: Auth is the gate to every authenticated route. Amn supports four login methods in parallel:
- **Email + password (JWT).** Standard `bcrypt`-hashed credentials, access + refresh token pair, six-digit email verification codes, and password reset codes. Source: `backend/src/services/auth/authService.ts`. - **Email + password (JWT).** Standard `bcrypt`-hashed credentials, access + refresh token pair, six-digit email verification codes, and password reset codes. Source: `backend/src/services/auth/authService.ts`.
- **Passkey (WebAuthn).** Platform and cross-platform authenticators are registered against the user account; multiple devices per user are stored in `User.passkeys[]` (`backend/src/models/User.ts:125`). - **Passkey (WebAuthn).** Platform and cross-platform authenticators are registered against the user account; multiple devices per user are stored in `User.passkeys[]` (`backend/src/models/User.ts:125`).
- **Google OAuth.** Server-side verification via `google-auth-library`. See `backend/src/services/auth/googleOAuthService.ts`. - **Google OAuth.** Server-side verification via `google-auth-library`. See `backend/src/services/auth/googleOAuthService.ts`.
- **Telegram (first-class).** `POST /api/auth/telegram` accepts a Telegram Mini App `initData` string or a Telegram Login Widget payload. The backend verifies the Telegram HMAC signature, then signs the user in or auto-creates a new Amanat account with `authProvider: "telegram"` and no required email. This means a user who opens the Telegram Mini App is authenticated with zero sign-up friction. See `backend/src/services/auth/authController.ts` (`telegramAuth`) and [[Authentication Flow#Telegram first-class auth flow]].
Roles are `admin | buyer | seller` (`backend/src/models/User.ts:94`). Role checks happen in route middleware. **Refresh tokens** are stored on the `User` document and rotated. Roles are `admin | buyer | seller` (`backend/src/models/User.ts:94`). Role checks happen in route middleware. **Refresh tokens** are stored on the `User` document and rotated.
@@ -123,13 +132,14 @@ Services live in `backend/src/services/marketplace/` and are exposed through `/a
### Payments — [[Payments Overview]] / [[SHKeeper Integration]] ### Payments — [[Payments Overview]] / [[SHKeeper Integration]]
Payments are where Amn is most distinctive. The backend supports **three payment surfaces** routed through a common `Payment` model (`backend/src/models/Payment.ts`): Payments are where Amn is most distinctive. The backend supports **four payment surfaces** routed through a common `Payment` model (`backend/src/models/Payment.ts`) via a provider-neutral adapter layer (`backend/src/services/payment/adapters/`):
- **SHKeeper** — `/api/payment/shkeeper`. Mounted at `backend/src/app.ts:327`. Issues a fresh wallet address per invoice, polls / webhooks for payment confirmation, and runs through `PaymentCoordinator` to avoid race conditions where a payment status is updated twice. Health is monitored in the background (`shkeeperHealthCheck.ts`, started in `app.ts:433`). - **SHKeeper** — `/api/payment/shkeeper`. Issues a fresh wallet address per invoice, polls / webhooks for payment confirmation, and runs through `PaymentCoordinator` to avoid race conditions. Health is monitored in the background (`shkeeperHealthCheck.ts`).
- **Decentralized (Wagmi + DePay)** — `/api/payment/decentralized`. The user signs and sends the transfer from their own wallet; the backend then verifies the transaction on-chain via `blockchainTxFetcher.ts` and the Alchemy SDK. - **Request Network** — `/api/payment/request-network`. Creates on-chain payment requests via the Request Network protocol, generates Secure Payment Page URLs for the buyer, and receives real-time payment status via signed webhooks (`x-request-network-signature`). Pay-in service: `requestNetworkPayInService.ts`; reconciliation: `requestNetworkReconciliationService.ts`.
- **Decentralized (Wagmi + DePay)** — `/api/payment/decentralized`. The user signs and sends the transfer from their own wallet; the backend verifies on-chain via `blockchainTxFetcher.ts` and the Alchemy SDK.
- **Payout** — `/api/payment/shkeeper/payout`. Admin-triggered release of escrow funds to the seller's wallet once delivery is confirmed. - **Payout** — `/api/payment/shkeeper/payout`. Admin-triggered release of escrow funds to the seller's wallet once delivery is confirmed.
All three surfaces converge on the same `Payment` record (with `direction: 'in' | 'out' | 'refund'`) and trigger the same downstream events: order status update, notification, points award. **Pending payments are auto-cleaned** by a background timer started in `app.ts:374`. All surfaces converge on the same `Payment` record (with `direction: 'in' | 'out' | 'refund'`) and share the internal **funds ledger** (`backend/src/services/payment/ledger/`) which tracks available / held / releasable amounts independently of the provider. **Pending payments are auto-cleaned** by a background timer started in `app.ts`.
### Real-time chat — [[Chat System]] ### Real-time chat — [[Chat System]]
@@ -202,7 +212,7 @@ OpenAI (model configurable per call) is exposed through `/api/ai/*`. The current
## Cross-cutting concerns ## Cross-cutting concerns
- **Observability** — Sentry is initialised at the very top of `app.ts` (line 2-3) and on the frontend in `sentry.{client,edge,server}.config.ts`. Logs flow through `backend/src/utils/logger.ts`. - **Observability** — Sentry is initialised at the very top of `app.ts` (line 2-3) and on the frontend in `sentry.{client,edge,server}.config.ts`. Logs flow through `backend/src/utils/logger.ts`.
- **Security** — `helmet`, CORS scoped to `FRONTEND_URL`, JWT with bcrypt-hashed passwords, role-gated middleware. Rate limiting is plumbed but currently disabled (see `app.ts:227`). - **Security** — `helmet`, CORS scoped to `FRONTEND_URL`, JWT with bcrypt-hashed passwords, role-gated middleware. Rate limiting is active (10 req/15 min on auth, 30 on payment, 100 global; Request Network and Telegram webhooks are skip-listed to avoid false limits).
- **Internationalisation** — Six locales (en, fr, vi, cn, ar, fa), with `fa` as default. RTL via `stylis-plugin-rtl`. See `frontend/src/locales/`. - **Internationalisation** — Six locales (en, fr, vi, cn, ar, fa), with `fa` as default. RTL via `stylis-plugin-rtl`. See `frontend/src/locales/`.
- **Theming** — MUI v7 with a custom theme in `frontend/src/theme/`. Dark mode is on the roadmap. - **Theming** — MUI v7 with a custom theme in `frontend/src/theme/`. Dark mode is on the roadmap.
- **Containerisation** — Docker Compose stacks for dev and prod live in both repos (`docker-compose.dev.yml`, `docker-compose.production.yml`). - **Containerisation** — Docker Compose stacks for dev and prod live in both repos (`docker-compose.dev.yml`, `docker-compose.production.yml`).

View File

@@ -129,7 +129,7 @@ The backend is `amn-backend@2.6.3-beta`, an Express 5 server in TypeScript backe
| body-parser | ^2.2.0 | Body parsing (legacy fallback) | Body middleware | | body-parser | ^2.2.0 | Body parsing (legacy fallback) | Body middleware |
| helmet | ^8.1.0 | HTTP security headers | `app.ts:189` | | helmet | ^8.1.0 | HTTP security headers | `app.ts:189` |
| cors | ^2.8.5 | Cross-origin policy | `app.ts:194` | | cors | ^2.8.5 | Cross-origin policy | `app.ts:194` |
| express-rate-limit | ^8.0.1 | Rate-limit middleware (currently off) | Plumbed in | | express-rate-limit | ^8.0.1 | Rate-limit middleware | Active — auth 10/15min, payment 30/15min, AI 20/15min, global 100/15min |
| express-validator | ^7.2.1 | Request validation | Auth, marketplace | | express-validator | ^7.2.1 | Request validation | Auth, marketplace |
| multer | ^2.0.2 | Multipart file uploads | `services/file/` | | multer | ^2.0.2 | Multipart file uploads | `services/file/` |
| sharp | ^0.34.3 | Image resizing / format conversion | Upload pipeline | | sharp | ^0.34.3 | Image resizing / format conversion | Upload pipeline |
@@ -211,10 +211,12 @@ The backend is `amn-backend@2.6.3-beta`, an Express 5 server in TypeScript backe
| Service | Purpose | Touchpoint in code | | Service | Purpose | Touchpoint in code |
|---|---|---| |---|---|---|
| **SHKeeper** | Self-hosted crypto payment processor — issues wallets, watches for incoming USDT, pays out | `backend/src/services/payment/shkeeper/` | | **SHKeeper** | Self-hosted crypto payment processor — issues wallets, watches for incoming USDT, pays out | `backend/src/services/payment/shkeeper/` |
| **Request Network** | On-chain payment request protocol — creates invoices, generates Secure Payment Pages, signs webhooks | `backend/src/services/payment/requestNetwork/` + adapters |
| **DePay** | Drop-in Web3 widget for wallet-to-wallet payment | `@depay/widgets` on frontend | | **DePay** | Drop-in Web3 widget for wallet-to-wallet payment | `@depay/widgets` on frontend |
| **EVM chains** (BSC, Ethereum mainnet, Sepolia, Polygon) | Settlement layer for stablecoin transfers | `frontend/src/web3/config.ts`, backend `blockchain/` | | **EVM chains** (BSC, Ethereum mainnet, Sepolia, Polygon) | Settlement layer for stablecoin transfers | `frontend/src/web3/config.ts`, backend `blockchain/` |
| **Alchemy RPC** | Hosted EVM RPC + transaction lookup | Frontend `alchemy-sdk`, backend `blockchainTxFetcher.ts` | | **Alchemy RPC** | Hosted EVM RPC + transaction lookup | Frontend `alchemy-sdk`, backend `blockchainTxFetcher.ts` |
| **MetaMask / WalletConnect** | Wallet connectors via Wagmi | `web3/config.ts` (WalletConnect commented out pending SSR fix) | | **MetaMask / WalletConnect** | Wallet connectors via Wagmi | `web3/config.ts` (WalletConnect commented out pending SSR fix) |
| **Telegram Bot API + Mini App** | Bot commands, inline keyboards, webhook updates, Mini App launch surface, Login Widget | `backend/src/services/telegram/`, `frontend/src/app/telegram/`, `frontend/src/utils/telegram-webapp.ts` |
| **OpenAI** | LLM for drafting / summarising | `backend/src/services/ai/` | | **OpenAI** | LLM for drafting / summarising | `backend/src/services/ai/` |
| **Google OAuth** | Federated login | `googleOAuthService.ts` | | **Google OAuth** | Federated login | `googleOAuthService.ts` |
| **SMTP** (provider configured per env) | Transactional email | `services/email/` | | **SMTP** (provider configured per env) | Transactional email | `services/email/` |

View File

@@ -37,10 +37,17 @@ backend/src/
│ ├── file/ # Multer uploads, MIME validation │ ├── file/ # Multer uploads, MIME validation
│ ├── marketplace/ # PurchaseRequest, SellerOffer, Template, Shop │ ├── marketplace/ # PurchaseRequest, SellerOffer, Template, Shop
│ ├── notification/ # Templates, delivery, mark-as-read │ ├── notification/ # Templates, delivery, mark-as-read
│ ├── payment/ # Payment orchestration + shkeeper/ subdir │ ├── payment/ # Payment orchestration + provider adapters + ledger
│ │ ├── adapters/ # Provider-neutral adapter interface + registry
│ │ ├── ledger/ # Internal funds ledger (available / held / releasable)
│ │ ├── reconciliation/ # Webhook + status reconciliation per provider
│ │ ├── migration/ # Legacy data backfill utilities
│ │ ├── observability/ # Logging and incident controls
│ │ ├── requestNetwork/ # Request Network pay-in, routes, webhook signature
│ │ └── shkeeper/ # SHKeeper API, webhook, payout │ │ └── shkeeper/ # SHKeeper API, webhook, payout
│ ├── points/ # Loyalty points, levels, redemption │ ├── points/ # Loyalty points, levels, redemption
│ ├── redis/ # Redis client, cache helpers │ ├── redis/ # Redis client, cache helpers
│ ├── telegram/ # Bot webhook, Mini App session, identity linking, notifications
│ ├── user/ # Profile, preferences, addresses │ ├── user/ # Profile, preferences, addresses
│ ├── admin/ # Admin-only operations │ ├── admin/ # Admin-only operations
│ └── email/ # Nodemailer transport + templates │ └── email/ # Nodemailer transport + templates
@@ -98,8 +105,8 @@ The bootstrap is intentionally linear and easy to audit. Execution order:
| 10 | `notFound` | tail | Returns 404 envelope for unmatched routes. | | 10 | `notFound` | tail | Returns 404 envelope for unmatched routes. |
| 11 | `errorHandler` | tail | Catches thrown errors, formats response. | | 11 | `errorHandler` | tail | Catches thrown errors, formats response. |
> [!warning] > [!note]
> Rate-limit middleware is **disabled by default for personal use** (see `app.ts:227` cited in the architecture review). Enable before any real public traffic — `express-rate-limit` is already a dependency. > Rate-limit middleware is **active** as of 2026-05-24: auth 10 req/15 min, payment 30/15 min, AI 20/15 min, global 100/15 min. Request Network and Telegram webhooks are exempt from the global limiter. Counters are in-memory — a Redis adapter is planned for distributed deployments.
--- ---
@@ -121,7 +128,8 @@ The full route table mounted by `app.ts`:
| `/api/payment/decentralized` | `services/payment/decentralizedPaymentRoutes.ts` | mixed | Web3 save, verify, receiver | | `/api/payment/decentralized` | `services/payment/decentralizedPaymentRoutes.ts` | mixed | Web3 save, verify, receiver |
| `/api/payment/shkeeper` | `services/payment/shkeeper/shkeeperRoutes.ts` | mixed | Intents, webhook, release, refund, config | | `/api/payment/shkeeper` | `services/payment/shkeeper/shkeeperRoutes.ts` | mixed | Intents, webhook, release, refund, config |
| `/api/payment/shkeeper/payout` | `services/payment/shkeeper/shkeeperPayoutRoutes.ts` | JWT (seller/admin) | Withdraw to wallet | | `/api/payment/shkeeper/payout` | `services/payment/shkeeper/shkeeperPayoutRoutes.ts` | JWT (seller/admin) | Withdraw to wallet |
| `/api/payment/request-network` | `services/payment/requestNetwork/requestNetworkRoutes.ts` | HMAC | Request Network webhooks | | `/api/payment/request-network` | `services/payment/requestNetwork/requestNetworkRoutes.ts` | HMAC sig | Request Network pay-in creation, Secure Payment Page, webhooks |
| `/api/telegram` | `services/telegram/telegramRoutes.ts` | mixed (some JWT, webhook uses secret-token) | Mini App verify/session, identity link/unlink, bot webhook |
| `/api/chat` | `services/chat/chatRoutes.ts` | JWT | Conversations, messages | | `/api/chat` | `services/chat/chatRoutes.ts` | JWT | Conversations, messages |
| `/api/notification` | `services/notification/notificationRoutes.ts` + `notificationControllerRouter` | JWT | List, mark read | | `/api/notification` | `services/notification/notificationRoutes.ts` + `notificationControllerRouter` | JWT | List, mark read |
| `/api/dispute` | `services/dispute/disputeRoutes.ts` | JWT | **Not implemented** — planned | | `/api/dispute` | `services/dispute/disputeRoutes.ts` | JWT | **Not implemented** — planned |
@@ -175,15 +183,19 @@ flowchart TB
file[file] file[file]
email[email] email[email]
socket[socket] socket[socket]
telegram[telegram]
auth --> user auth --> user
auth --> notify auth --> notify
auth --> telegram
market --> notify market --> notify
market --> chat market --> chat
market --> file market --> file
pay --> market pay --> market
pay --> notify pay --> notify
pay --> socket pay --> socket
telegram --> notify
telegram --> auth
dispute -.-> market dispute -.-> market
dispute -.-> chat dispute -.-> chat
dispute -.-> notify dispute -.-> notify

View File

@@ -17,17 +17,20 @@ How identity, authorization, transport, and integrity are handled across the pla
| Threat | Mitigation | | Threat | Mitigation |
|---|---| |---|---|
| Credential stuffing | bcrypt 12-round hashing + account lockout + rate-limit (when enabled) | | Credential stuffing | bcrypt 12-round hashing + account lockout + rate-limit |
| Session hijacking | Short-lived JWTs (7d), opaque refresh tokens (30d), token rotation | | Session hijacking | Short-lived JWTs (7d), opaque refresh tokens (30d), token rotation |
| CSRF | JWT in `Authorization` header (not cookie), CORS allow-list | | CSRF | JWT in `Authorization` header (not cookie), CORS allow-list |
| XSS | Helmet CSP, React auto-escaping, sanitize HTML before storage | | XSS | Helmet CSP, React auto-escaping, sanitize HTML before storage |
| SQL/NoSQL injection | Mongoose parameterized queries, no `$where` strings, schema validation | | SQL/NoSQL injection | Mongoose parameterized queries, no `$where` strings, schema validation |
| Webhook spoofing | HMAC SHA-256 over body + secret, constant-time compare | | Webhook spoofing | HMAC SHA-256 over body + secret (SHKeeper, Request Network, Telegram), constant-time compare |
| File upload abuse | Multer MIME validation, 5 MB cap, non-executable storage, served by Nginx not Node | | File upload abuse | Multer MIME validation, 5 MB cap, non-executable storage, served by Nginx not Node |
| Replay attacks | Per-payment idempotency on `providerPaymentId`, per-request `X-Request-Id` | | Replay attacks | Per-payment idempotency on `providerPaymentId`; Telegram initData in-memory replay map; per-request `X-Request-Id` |
| Account takeover | Email verification required, password reset code expiry (1h), passkey support | | Account takeover | Email verification required, password reset code expiry (1h), passkey support |
| Phishing | Passkey origin binding (`NEXT_PUBLIC_PASSKEY_ORIGIN`), email domain pinning | | Phishing | Passkey origin binding (`NEXT_PUBLIC_PASSKEY_ORIGIN`), email domain pinning |
| Data leakage | Role-gated endpoints, field-level projection (`select: false` on password), redacted logs | | Data leakage | Role-gated endpoints, field-level projection (`select: false` on password), redacted logs |
| Forged Telegram identity | `initData` HMAC-SHA256 verified server-side; `initDataUnsafe` never trusted; auth_date max-age enforced |
| Telegram callback replay | In-memory replay map with configurable window in `telegramService.ts` |
| Blocked Telegram user bypass | `TelegramLink.status === 'blocked'` returns 403 regardless of unlink/re-link attempts |
--- ---
@@ -79,7 +82,38 @@ sequenceDiagram
> [!warning] > [!warning]
> Dev env files ship `NEXT_PUBLIC_PASSKEY_RP_ID=localhost`. In production this MUST be the actual eTLD+1 domain (e.g., `amn.gg`) — passkeys are scoped to the RP ID and can't be transferred. > Dev env files ship `NEXT_PUBLIC_PASSKEY_RP_ID=localhost`. In production this MUST be the actual eTLD+1 domain (e.g., `amn.gg`) — passkeys are scoped to the RP ID and can't be transferred.
### 2.4 Refresh-token rotation ### 2.4 Telegram (first-class auth provider)
```mermaid
sequenceDiagram
actor U as User (Telegram)
participant FE as Frontend / Mini App
participant BE as Backend
participant DB as MongoDB
U->>FE: opens Mini App (initData available) or clicks Login Widget
FE->>BE: POST /api/auth/telegram { initData | loginWidget }
BE->>BE: verifyMiniAppInitData() or verifyTelegramLoginWidget()
BE->>BE: reject if auth_date stale / replay / bot account
BE->>DB: TelegramLink.findOne({ telegramUserId })
alt link exists and active
BE->>DB: load linked User
else no link — auto-provision
BE->>DB: User.create({ authProvider:"telegram", telegramVerified:true, email:null })
BE->>DB: TelegramLink.create(...)
end
BE->>DB: upsert TelegramLink.lastSeenAt
BE->>BE: generateToken() + generateRefreshToken()
BE-->>FE: 200 { user, tokens, isNewUser }
```
- Backend source: `backend/src/services/auth/authController.ts` (`telegramAuth`) and `backend/src/services/telegram/telegramService.ts`.
- Mini App path uses `HMAC-SHA256("WebAppData", BOT_TOKEN)` per Telegram spec; Login Widget path uses `HMAC-SHA256(data_check_string, SHA256(BOT_TOKEN))`.
- In-memory replay maps guard against duplicate `initData` submissions within a configurable window.
- Blocked `TelegramLink` records return `403` — users cannot circumvent by unlinking and re-linking.
- Users with `authProvider: "telegram"` have nullable email; email-based operations (password reset) are not applicable to them.
- See [[Authentication Flow#Telegram first-class auth flow]].
### 2.5 Refresh-token rotation
- On `POST /api/auth/refresh`, the backend: - On `POST /api/auth/refresh`, the backend:
- Verifies the supplied refresh token. - Verifies the supplied refresh token.
@@ -119,7 +153,9 @@ A single User may be `buyer` and `seller` simultaneously (combined role).
--- ---
## 5. Webhook integrity (SHKeeper) ## 5. Webhook integrity
### 5.1 SHKeeper
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
@@ -136,11 +172,26 @@ sequenceDiagram
end end
``` ```
- Body must be the raw bytes used for HMAC — apply `express.raw({ type: 'application/json' })` on the webhook route ONLY (the rest of the app uses parsed JSON). - Raw body must be used for HMAC — `express.raw({ type: 'application/json' })` is mounted on this route only (before the global `express.json()` parser).
- In dev (`NODE_ENV === 'development'`) signature verification can be bypassed for local testing — confirm this is gated and never reachable in prod. - In dev (`NODE_ENV === 'development'`) signature verification can be bypassed for local testing — confirm this is gated and never reachable in prod.
- Idempotency: identical webhook delivered twice should be no-op. Check by `(providerPaymentId, status)` tuple before mutating. - Idempotency: identical webhook delivered twice should be no-op. Check by `(providerPaymentId, status)` tuple before mutating.
See [[Payment Flow - SHKeeper]] for the full flow. ### 5.2 Request Network
- Webhooks arrive at `/api/payment/request-network/webhook` with an `x-request-network-signature` header.
- The backend verifies the signature using `backend/src/services/payment/requestNetwork/signature.ts` before any state mutation.
- The route is mounted **before** the global `express.json()` body parser so raw body bytes are available for signature computation.
- The global rate-limit middleware is configured to skip this path to avoid blocking high-frequency payment events.
- Reconciliation service (`requestNetworkReconciliationService.ts`) handles replayed or out-of-order webhooks idempotently.
### 5.3 Telegram Bot webhook
- Webhooks arrive at `/api/telegram/webhook` with an `x-telegram-bot-api-secret-token` header.
- The backend verifies the token with `verifyTelegramWebhookSecret()` (`telegramService.ts`) before processing updates.
- A per-update-id in-memory replay map prevents duplicate processing within the configured window.
- The global rate-limit middleware is configured to skip this path.
See [[Payment Flow - SHKeeper]] for the SHKeeper full flow.
--- ---
@@ -224,7 +275,7 @@ The codebase currently uses `morgan` (HTTP access logs) and ad-hoc `logger.info/
## Related ## Related
- [[Authentication Flow]] · [[Google OAuth Flow]] · [[Passkey (WebAuthn) Flow]] · [[Password Reset Flow]] - [[Authentication Flow]] (includes Telegram first-class auth flow) · [[Google OAuth Flow]] · [[Passkey (WebAuthn) Flow]] · [[Password Reset Flow]]
- [[Backend Architecture]] · [[Frontend Architecture]] · [[Real-time Layer]] - [[Backend Architecture]] · [[Frontend Architecture]] · [[Real-time Layer]]
- [[Payment Flow - SHKeeper]] — webhook HMAC details - [[Payment Flow - SHKeeper]] — webhook HMAC details
- [[Environment Variables]] — secret catalog - [[Environment Variables]] — secret catalog

View File

@@ -9,7 +9,7 @@ aliases: [Models Index, Schema Overview]
This section documents every Mongoose model that backs the marketplace. The persistence layer lives in `backend/src/models/` and is exported through a single barrel file at `backend/src/models/index.ts`. All models target MongoDB via Mongoose, lean on `timestamps: true` for `createdAt` / `updatedAt`, and follow a consistent pattern: one schema per file, an exported `I<Name>` TypeScript interface, and named exports for the compiled model. This section documents every Mongoose model that backs the marketplace. The persistence layer lives in `backend/src/models/` and is exported through a single barrel file at `backend/src/models/index.ts`. All models target MongoDB via Mongoose, lean on `timestamps: true` for `createdAt` / `updatedAt`, and follow a consistent pattern: one schema per file, an exported `I<Name>` TypeScript interface, and named exports for the compiled model.
> [!note] Scope > [!note] Scope
> Sixteen models are documented here. The "File" concept exists only at the service layer (`backend/src/services/file/`) and is not persisted as its own Mongoose collection, so it is not listed below. > Eighteen models are documented here. The "File" concept exists only at the service layer (`backend/src/services/file/`) and is not persisted as its own Mongoose collection, so it is not listed below.
> >
> [!warning] Implementation gap > [!warning] Implementation gap
> As of the 2026-05-24 audit, the following documented models **do not yet have Mongoose schema files** in `backend/src/models/`: > As of the 2026-05-24 audit, the following documented models **do not yet have Mongoose schema files** in `backend/src/models/`:
@@ -20,7 +20,7 @@ This section documents every Mongoose model that backs the marketplace. The pers
> - [[LevelConfig]] > - [[LevelConfig]]
> - [[ShopSettings]] > - [[ShopSettings]]
> The following *are* implemented in code and are documented accurately: > The following *are* implemented in code and are documented accurately:
> - [[User]], [[PurchaseRequest]], [[SellerOffer]], [[Payment]], [[Chat]], [[Notification]], [[RequestTemplate]], [[Address]], [[Category]], [[TempVerification]] > - [[User]], [[PurchaseRequest]], [[SellerOffer]], [[Payment]], [[Chat]], [[Notification]], [[RequestTemplate]], [[Address]], [[Category]], [[TempVerification]], [[TelegramLink]], [[TelegramSession]]
> Additionally, `FundsLedgerEntry.ts` and `TrezorAccount.ts` exist in `backend/src/models/` but are not yet documented in this vault. > Additionally, `FundsLedgerEntry.ts` and `TrezorAccount.ts` exist in `backend/src/models/` but are not yet documented in this vault.
## Index of Models ## Index of Models
@@ -41,6 +41,8 @@ This section documents every Mongoose model that backs the marketplace. The pers
- [[LevelConfig]] — Static configuration of loyalty tiers (level number, point thresholds, benefits, icon, color). Driven by admins; consumed by the [[User]].points.level field. - [[LevelConfig]] — Static configuration of loyalty tiers (level number, point thresholds, benefits, icon, color). Driven by admins; consumed by the [[User]].points.level field.
- [[ShopSettings]] — One-to-one storefront branding for a seller: name, description, avatar, cover image, review toggles, and social links. - [[ShopSettings]] — One-to-one storefront branding for a seller: name, description, avatar, cover image, review toggles, and social links.
- [[TempVerification]] — Short-lived signup record that holds candidate user data and a verification code. Auto-purges via TTL when `emailVerificationCodeExpires` passes. - [[TempVerification]] — Short-lived signup record that holds candidate user data and a verification code. Auto-purges via TTL when `emailVerificationCodeExpires` passes.
- [[TelegramLink]] — Permanent auditable association between a Telegram user ID and an Amanat [[User]]. Stores Telegram profile metadata, link source (`miniapp` / `bot` / `login_widget`), status (`active` / `blocked`), and last-seen timestamp. One per Telegram user (unique on both `userId` and `telegramUserId`).
- [[TelegramSession]] — Short-lived Telegram Mini App session token issued when `initData` is verified. Carries the `initDataFingerprint` for replay protection and auto-expires via a MongoDB TTL index on `expiresAt`.
## Relationship Diagram ## Relationship Diagram
@@ -83,6 +85,10 @@ erDiagram
LEVEL_CONFIG ||..|| USER : "level lookup" LEVEL_CONFIG ||..|| USER : "level lookup"
TEMP_VERIFICATION ||..|| USER : "promoted to" TEMP_VERIFICATION ||..|| USER : "promoted to"
TELEGRAM_LINK }o--|| USER : "links identity"
TELEGRAM_SESSION }o--o| USER : "session for"
TELEGRAM_SESSION }o--|| TELEGRAM_LINK : "matches"
``` ```
## Conventions Across All Models ## Conventions Across All Models

View File

@@ -16,12 +16,14 @@ The core identity document for every actor in the marketplace: buyers, sellers,
| Field | Type | Required | Default | Validation | Index | Description | | Field | Type | Required | Default | Validation | Index | Description |
| --- | --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- | --- |
| `email` | String | yes | — | lowercase, trim | unique | Primary login identifier. | | `email` | String | no | — | lowercase, trim | unique, sparse | Primary email login identifier. Nullable for Telegram-only accounts. |
| `password` | String | no | — | minlength 6 | — | Hashed password. Optional to support passkey-only 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` | yes | Authorisation tier. |
| `isEmailVerified` | Boolean | no | `false` | — | — | Set to true after [[TempVerification]] is consumed. | | `isEmailVerified` | Boolean | no | `false` | — | — | Set to true after [[TempVerification]] is consumed. |
| `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`. |
| `emailVerificationToken` | String | no | — | — | — | Legacy token-based email verification. | | `emailVerificationToken` | String | no | — | — | — | Legacy token-based email verification. |
| `emailVerificationCode` | String | no | — | — | — | OTP code for email verification. | | `emailVerificationCode` | String | no | — | — | — | OTP code for email verification. |
| `emailVerificationCodeExpires` | Date | no | — | — | — | Expiry for `emailVerificationCode`. | | `emailVerificationCodeExpires` | Date | no | — | — | — | Expiry for `emailVerificationCode`. |
@@ -76,10 +78,12 @@ The core identity document for every actor in the marketplace: buyers, sellers,
## Indexes ## Indexes
Defined explicitly (in addition to the implicit `email` unique index): Defined explicitly:
- `{ email: 1 }` unique sparse — allows multiple Telegram-only users without email while preserving uniqueness for email-bearing users.
- `{ role: 1 }``backend/src/models/User.ts:178` - `{ role: 1 }``backend/src/models/User.ts:178`
- `{ status: 1 }``backend/src/models/User.ts:179` - `{ status: 1 }``backend/src/models/User.ts:179`
- `{ authProvider: 1 }` — supports provider-level account reporting and cleanup.
> [!warning] Missing indexes > [!warning] Missing indexes
> The schema currently defines only `role` and `status` indexes. The `referralCode`, `referredBy`, and `points.level` indexes documented below are **not yet present** in `User.ts`: > The schema currently defines only `role` and `status` indexes. The `referralCode`, `referredBy`, and `points.level` indexes documented below are **not yet present** in `User.ts`:

View File

@@ -121,6 +121,37 @@ Two distinct identities are involved: a [[User]] (`models/User.ts`) and a [[Temp
- Pushes refresh token onto `user.refreshTokens`. - Pushes refresh token onto `user.refreshTokens`.
- Redis session start via `sessionService`. - Redis session start via `sessionService`.
### POST /api/auth/telegram
**Description:** First-class Telegram authentication. Accepts Telegram Mini App `initData` or a Telegram Login Widget payload, verifies the Telegram signature server-side, and signs the user into Amanat without requiring email or password.
**Auth required:** No
**Request body:**
```ts
// Mini App
{ initData: string; role?: "buyer" | "seller" }
// Login Widget
{ loginWidget: { id: string; first_name?: string; username?: string; auth_date: string; hash: string }; role?: "buyer" | "seller" }
```
**Response 200/201:**
```json
{
"success": true,
"data": {
"user": { "_id": "...", "authProvider": "telegram", "telegramVerified": true },
"tokens": { "accessToken": "...", "refreshToken": "..." },
"isNewUser": true,
"telegram": { "userId": "10001", "username": "alice", "source": "miniapp" }
}
}
```
**Errors:** `400` missing payload, `401` invalid/stale signature, `403` blocked Telegram account or inactive Amanat account, `409 TELEGRAM_REPLAY` reused Mini App `initData`, `429` rate-limited.
**Side effects:**
- Creates a Telegram-only [[User]] when no active `TelegramLink` exists. The user has no email, `authProvider: "telegram"`, and `telegramVerified: true`.
- Upserts `TelegramLink` for the Telegram ID and updates last-seen metadata.
- Stores the refresh token on the user document.
- Does not expose phone numbers; Telegram phone data is not requested or persisted.
### POST /api/auth/refresh-token ### POST /api/auth/refresh-token
**Description:** Exchanges a refresh token for a new access token. Rotates the refresh token. **Description:** Exchanges a refresh token for a new access token. Rotates the refresh token.
@@ -297,9 +328,11 @@ Routes are nested under `/api/auth/` via `passkeyRoutes`. Service: `passkeyServi
| HTTP | App code | Meaning | | HTTP | App code | Meaning |
| --- | --- | --- | | --- | --- | --- |
| 400 | `Validation Error` | `express-validator` rejected the body | | 400 | `Validation Error` | `express-validator` rejected the body |
| 401 | — | Bad credentials / missing token | | 401 | — | Bad credentials / missing token / invalid Telegram signature |
| 403 | — | Email not verified or insufficient role | | 403 | — | Email not verified, insufficient role, or blocked Telegram account |
| 409 | `USER_EXISTS` | Email already in use | | 409 | `USER_EXISTS` | Email already in use |
| 409 | `TELEGRAM_REPLAY` | Reused Telegram Mini App `initData` (replay protection) |
| 423 | — | Account temporarily locked after failed logins | | 423 | — | Account temporarily locked after failed logins |
| 429 | — | Rate-limited (auth tier: 10 req / 15 min / IP) |
See [[Error Codes]] for the global error shape. See [[Error Codes]] for the global error shape.

View File

@@ -95,10 +95,27 @@ sequenceDiagram
| Method | Endpoint | Source | | Method | Endpoint | Source |
|---|---|---| |---|---|---|
| `POST` | `/api/auth/login` | `backend/src/services/auth/authRoutes.ts:22``authController.login` | | `POST` | `/api/auth/login` | `backend/src/services/auth/authRoutes.ts:22``authController.login` |
| `POST` | `/api/auth/telegram` | `authRoutes.ts``authController.telegramAuth` |
| `POST` | `/api/auth/refresh-token` | `authRoutes.ts:24-27``authController.refreshToken` | | `POST` | `/api/auth/refresh-token` | `authRoutes.ts:24-27``authController.refreshToken` |
| `POST` | `/api/auth/logout` | `authRoutes.ts:68``authController.logout` (protected) | | `POST` | `/api/auth/logout` | `authRoutes.ts:68``authController.logout` (protected) |
| `GET` | `/api/auth/profile` | `authRoutes.ts:69``authController.getProfile` | | `GET` | `/api/auth/profile` | `authRoutes.ts:69``authController.getProfile` |
## Telegram first-class auth flow
Telegram is now a peer auth provider alongside email/password, Google, and passkeys.
1. The Telegram Mini App shell reads raw signed launch data from `window.Telegram.WebApp.initData`; browser login can submit a Telegram Login Widget payload.
2. The frontend posts the raw signed payload to `POST /api/auth/telegram`; it never trusts `initDataUnsafe` for authentication.
3. Backend verifies the Telegram signature:
- Mini App uses `verifyMiniAppInitData()` with the Telegram WebAppData HMAC algorithm.
- Login Widget uses `verifyTelegramLoginWidget()` with the Telegram Login Widget HMAC algorithm.
4. Backend rejects stale `auth_date`, bot accounts, replayed Mini App `initData`, and blocked `TelegramLink` records.
5. If an active `TelegramLink` exists, backend signs in that Amanat user. If no link exists, backend creates a Telegram-only user with nullable email, `authProvider: "telegram"`, `telegramVerified: true`, then creates the link.
6. Backend issues the standard access token and refresh token pair. The frontend stores them in `localStorage` under `accessToken` and `refreshToken`, then hydrates the current session.
7. If `isNewUser=true`, the Mini App opens a lightweight onboarding overlay and points the user to account settings for optional email, language, currency, and wallet details.
High-risk actions are unchanged: escrow release, refund, dispute-sensitive, and wallet-sensitive operations still use the existing protected backend authorization and step-up gates. Telegram auth only establishes the user session.
## Database writes ## Database writes
- **`users` collection**: `lastLoginAt` updated; `refreshTokens` array gains one entry per successful login or refresh. - **`users` collection**: `lastLoginAt` updated; `refreshTokens` array gains one entry per successful login or refresh.

View File

@@ -179,11 +179,14 @@ sequenceDiagram
## Linked flows ## Linked flows
- [[Authentication Flow]] — the next time the user signs in. - [[Authentication Flow]] — the next time the user signs in (includes the Telegram first-class auth section).
- [[Referral Flow]] — full points-awarding mechanics triggered here. - [[Referral Flow]] — full points-awarding mechanics triggered here.
- [[Google OAuth Flow]] — alternative path that bypasses `TempVerification` (Google identities are pre-verified). - [[Google OAuth Flow]] — alternative path that bypasses `TempVerification` (Google identities are pre-verified).
- [[Password Reset Flow]] — if the user forgets the password they set during verification. - [[Password Reset Flow]] — if the user forgets the password they set during verification.
> [!tip] Telegram — zero-step registration
> Users who open the Amanat Telegram Mini App do **not** go through this flow at all. `POST /api/auth/telegram` verifies the Telegram-signed `initData` and auto-provisions a new `User` (no email, `authProvider: "telegram"`) in a single round-trip. The `TempVerification` + email code cycle only applies to email-based sign-ups. See [[Authentication Flow#Telegram first-class auth flow]].
## Source files ## Source files
- Backend: `backend/src/services/auth/authController.ts:33-158` (register), `:364-469` (verify), `:498-539` (resend) - Backend: `backend/src/services/auth/authController.ts:33-158` (register), `:364-469` (verify), `:498-539` (resend)

View File

@@ -0,0 +1,311 @@
---
title: PRD — TON Wallet Ownership Proof
tags: [prd, ton, wallet, security, telegram]
created: 2026-05-24
status: backend-implemented
priority: high
---
# PRD — TON Wallet Ownership Proof
## Background
TON wallet connect was shipped as a first-pass integration: the backend validates address format and stores it, but does not require the user to prove they control the private key behind the address. Any user can submit any valid-looking TON address and it will be saved to their profile.
This matters for:
- **Seller payout routing** — if a seller's wallet address was spoofed, funds go to the wrong address.
- **On-chain proof of identity** — downstream features (NFT-gated access, on-chain reputation) require a real ownership proof.
- **Compliance** — KYC/AML flows that tie a real-world identity to a wallet need proof, not self-report.
Until proof is implemented, saved TON wallet addresses are self-reported metadata only and must not be used for automatic payout routing.
---
## Goal
Replace the format-only TON wallet save with a full TonConnect ownership proof flow:
- Backend issues a challenge nonce scoped to the user session.
- Frontend passes the challenge to TonConnect UI, which asks the user to sign it.
- Backend verifies the returned proof against the wallet's public key.
- On success, stores `walletProofVerified: true`, proof timestamp, and wallet provider.
---
## Non-Goals
- On-chain transaction signing or payment initiation (separate feature).
- Requiring proof for EVM wallets (already covered by `ethers.verifyMessage`).
- TON DNS or NFT resolution.
- Mandatory wallet for all users (wallet remains optional profile metadata).
---
## User Stories
| As a… | I want to… | So that… |
|-------|-----------|----------|
| Seller | Connect my Telegram Wallet with proof | Payments route to the right address |
| Buyer | Know my counterparty's wallet is verified | I trust the seller's payout address |
| Admin | See `walletProofVerified` on user profiles | I can audit wallet verification status |
| Developer | Have a clear nonce/challenge API | I can build additional proof-gated features |
---
## Flow
```
Frontend Backend TonConnect
| | |
|-- POST /wallet-address/ton-proof/challenge --> |
| |-- generate nonce |
| |-- store nonce (TTL 5 min) |
|<-- { payload, domain, timestamp } -- |
| | |
|-- tonConnectUI.connectWallet({ tonProof: payload }) ---------->|
| | |-- user signs proof
|<-- proof object (wallet, stateInit, signature) ---------------|
| | |
|-- PATCH /wallet-address { walletType:'ton', tonProof } --> |
| |-- verify proof |
| | - reconstruct message |
| | - verify against pubkey |
| | - check domain + nonce |
| |-- store wallet + proof meta |
|<-- { walletProofVerified: true } -- |
```
---
## Backend Changes
### 1. New endpoint: `POST /api/user/wallet-address/ton-proof/challenge`
**Auth:** Bearer JWT required.
**Request body:** none.
**Response:**
```json
{
"payload": "aGV4LWVuY29kZWQtbm9uY2U...",
"domain": "amn.gg",
"timestamp": 1716560000
}
```
**Implementation:**
- Generate 32-byte random nonce, hex-encode.
- Store in a TTL map (or Redis `SET NX EX 300`) keyed by `userId`.
- Return `{ payload: nonce, domain: "amn.gg", timestamp: now }`.
- One active challenge per user — issuing a new one invalidates the old.
---
### 2. Updated: `PATCH /api/user/wallet-address`
**When `walletType = 'ton'` and a `tonProof` object is present:**
Accept additional body field:
```ts
tonProof?: {
timestamp: number;
domain: { lengthBytes: number; value: string };
signature: string; // base64
payload: string; // the nonce from challenge
stateInit?: string; // base64, for wallets not yet deployed
}
```
**Verification logic (using `@ton/ton` and `@ton/crypto`):**
```ts
import { Address, Cell, beginCell, contractAddress } from '@ton/ton';
import nacl from 'tweetnacl';
// 1. Validate nonce matches stored challenge for this user
// 2. Validate timestamp is within 5-minute window
// 3. Validate domain === 'amn.gg'
// 4. Reconstruct the signed message:
const message = Buffer.concat([
Buffer.from('ton-proof-item-v2/'),
Buffer.from(new Uint32Array([domain.length]).buffer),
Buffer.from(domain, 'utf8'),
Buffer.from(new Uint64Array([timestamp]).buffer),
Buffer.from(payload, 'hex'),
]);
const hash = Buffer.from(sha256(Buffer.concat([
Buffer.from([0xff, 0xff]),
Buffer.from('ton-connect'),
sha256(message),
])));
// 5. Recover public key from stateInit if wallet not deployed,
// or fetch from blockchain (tonweb/lite-client) if deployed.
// 6. Verify: nacl.sign.detached.verify(hash, sig, pubkey)
// 7. Verify: Address.parse(walletAddress) matches derived address
```
**On success:**
- Store wallet fields: `profile.walletAddress`, `profile.walletType = 'ton'`, `profile.walletProvider`.
- Set new fields: `profile.walletProofVerified = true`, `profile.walletProofTimestamp = new Date()`.
- Clear the stored nonce.
**On failure:**
- Return `400 INVALID_TON_PROOF`.
- Do not update wallet fields.
- Do not clear the nonce (allow retry within TTL).
---
### 3. User model additions
```ts
// models/User.ts — inside profile
walletProofVerified?: boolean;
walletProofTimestamp?: Date;
```
---
### 4. Dependencies to add
```bash
yarn add @ton/ton @ton/crypto tweetnacl
```
`@ton/ton` is already a known dependency (the `@ton/core` frontend package is related). The backend needs `@ton/ton` for address parsing and cell operations, and `tweetnacl` for ed25519 verification.
---
## Frontend Changes
### 1. Fetch challenge before connecting
In `TonWalletProvider` (or the connect handler in `account-wallet-connection.tsx`):
```ts
// Before calling tonConnectUI.connectWallet()
const { payload } = await api.post('/api/user/wallet-address/ton-proof/challenge');
// Pass proof request to TonConnect
const proof: ConnectAdditionalRequest = {
tonProof: payload,
};
await tonConnectUI.connectWallet(proof);
```
### 2. Extract and send proof after connection
```ts
// After connection, wallet.connectItems.tonProof is populated
const wallet = tonConnectUI.wallet;
if (!wallet?.connectItems?.tonProof || 'error' in wallet.connectItems.tonProof) {
throw new Error('TON proof not returned by wallet');
}
await api.patch('/api/user/wallet-address', {
walletAddress: wallet.account.address,
walletType: 'ton',
walletProvider: wallet.device.appName,
tonProof: wallet.connectItems.tonProof.proof,
});
```
### 3. Show proof badge on wallet page
When `profile.walletProofVerified = true`, show a "Verified" badge next to the wallet address. When false (or absent), show "Unverified" with a "Verify ownership" button.
---
## Data Model
```ts
// Addition to profile sub-schema
walletProofVerified?: boolean; // true after successful TonConnect proof
walletProofTimestamp?: Date; // when proof was last verified
```
Existing fields retain their meaning:
- `walletAddress` — the TON address (friendly format)
- `walletType``'ton'` | `'evm'`
- `walletProvider` — e.g. `'Telegram Wallet'`, `'Tonkeeper'`
---
## API Reference
| Method | Path | Auth | Purpose |
|--------|------|------|---------|
| POST | `/api/user/wallet-address/ton-proof/challenge` | JWT | Get nonce for proof |
| PATCH | `/api/user/wallet-address` | JWT | Save wallet + verify proof |
---
## Tests
### Backend
- Challenge endpoint returns `{ payload, domain, timestamp }`.
- Challenge is bound to userId (different users get different nonces).
- Challenge expires: re-issuing invalidates previous.
- Wallet save with valid proof sets `walletProofVerified = true`.
- Wallet save with expired nonce returns 400.
- Wallet save with wrong domain returns 400.
- Wallet save with bad signature returns 400.
- Wallet save without `tonProof` object sets `walletProofVerified = false` (legacy format-only path).
### Frontend
- `connectWallet` is called with `tonProof: payload`.
- If proof is absent in response, shows error and does not call save endpoint.
- If backend returns 400, shows "Wallet verification failed" toast.
- Verified wallet shows badge; unverified shows "Verify" prompt.
---
## Migration
Existing users with a saved TON wallet address will have `walletProofVerified` absent (undefined / falsy). They should be prompted to re-verify when they next visit the wallet page. No automatic migration needed — the address is retained, only proof status changes.
---
## Implementation Status
### Backend — Complete (2026-05-24)
- `POST /api/user/wallet-address/ton-proof/challenge` implemented in `userController.ts`.
- Full ed25519 verification in `src/services/user/tonProofService.ts`:
- In-memory TTL challenge store (5-minute TTL per userId).
- Nonce cleared on success only (allows retry on failure).
- Verification: nonce match → expiry → domain → timestamp ±300s → stateInit hash matches address → extract pubkey → reconstruct signed message → `nacl.sign.detached.verify`.
- Public key extraction handles standard wallet v3/v4 data layout (skip 64 bits seqno+subwallet, read 32-byte pubkey).
- User model fields added: `profile.walletProofVerified`, `profile.walletProofTimestamp`.
- Route registered: `POST /api/user/wallet-address/ton-proof/challenge`.
- `@ton/ton@16.2.4`, `@ton/crypto@3.3.0`, `tweetnacl@1.0.3` added to backend `package.json`.
- 15 backend unit tests in `__tests__/ton-proof.test.ts` — all passing.
### Frontend — Pending
- Challenge fetch + `setConnectRequestParameters` + proof extraction not yet wired.
- Verified/unverified badge not yet rendered.
- EVM wallet stub card added (see below).
---
## Acceptance Criteria
- [x] `POST /api/user/wallet-address/ton-proof/challenge` returns a valid nonce object.
- [ ] `PATCH /api/user/wallet-address` with a real TonConnect proof from Telegram Wallet succeeds and sets `walletProofVerified: true`.
- [x] A replay of the same proof is rejected.
- [x] An expired nonce is rejected.
- [x] A proof with a mismatched domain is rejected.
- [ ] The wallet page shows "Verified" badge when `walletProofVerified = true`.
- [ ] The wallet page shows "Verify ownership" CTA when wallet is saved but unverified.
- [x] Backend tests cover all rejection cases with mocked proofs.
- [x] `@ton/ton` version is pinned in `package.json` (TON SDK breaks semver occasionally).
---
## Related
- [[Handoff - Telegram Mini App Debug - 2026-05-24]]
- [[Security Audit - 2026-05-24]] — TON wallet is currently self-reported metadata
- [[Data Model Overview]]
- TON Connect docs: https://docs.ton.org/develop/dapps/ton-connect/sign

View File

@@ -0,0 +1,539 @@
# Handoff - Telegram Mini App Debug - 2026-05-24
## Scope
This handoff covers the work done after bringing the app up under the consolidated `https://amn.gg` URL and debugging the Telegram Mini App flow.
Primary user-reported issues:
- The red `N` badge in the Telegram webview was interpreted as backend/socket failure.
- Telegram-created users without an existing Amanat account were blocked by the link/sign-in screen instead of being treated as buyer users.
- Wallet connect did not support Telegram Wallet / TON.
- Profile email updates for Telegram-created users did not persist and did not send confirmation email.
- The page needed visible debug statistics to identify which subsystem is working.
## Repositories Touched
Backend:
- `backend/src/models/User.ts`
- `backend/src/services/user/userController.ts`
- `backend/src/services/user/userControllerRoutes.ts`
- `backend/src/services/user/userRoutes.ts`
Frontend:
- `frontend/next.config.ts`
- `frontend/package.json`
- `frontend/yarn.lock`
- `frontend/public/tonconnect-manifest.json`
- `frontend/src/actions/account.ts`
- `frontend/src/app/layout.tsx`
- `frontend/src/auth/context/jwt/auth-provider.tsx`
- `frontend/src/auth/types.ts`
- `frontend/src/components/debug/telegram-debug-panel.tsx`
- `frontend/src/lib/axios.ts`
- `frontend/src/sections/account/account-general.tsx`
- `frontend/src/sections/account/account-wallet-connection.tsx`
- `frontend/src/types/user.ts`
- `frontend/src/web3/tonconnect-provider.tsx`
Vault:
- This file.
## Implemented
### Debug Statistics
Added `TelegramDebugPanel` to account/profile and wallet pages.
It reports:
- API URL
- Socket URL
- socket connected/disconnected state and socket id
- auth state
- user id, role, email, email verification state
- Telegram Mini App detection, platform, version, and initData presence
- saved wallet type/address
- last frontend account/wallet action and result
Visibility:
- Always visible in development.
- Visible inside Telegram Mini App.
- Visible in production browser with `?debug=1` or `localStorage.setItem('amn-debug', '1')`.
### Red `N` Badge
The red `N` indicator is the Next.js development indicator, not the backend/socket status.
Change:
- `frontend/next.config.ts` now sets `devIndicators: false`.
The actual socket status should be read from the debug panel or the app socket status component.
### Telegram User / Buyer Flow
Frontend auth mapping was changed so users without an email are not treated as email-unverified blockers:
- `isEmailVerified` is treated as true when the user has no email.
- If an email exists, normal verification state applies.
This supports Telegram-created buyer accounts without forcing immediate email collection.
### Profile Email Update And Verification
Backend profile update now accepts `email` and handles verification:
- Normalizes email to lowercase.
- Rejects duplicate email.
- Stores a six-digit verification code on the user.
- Sets `isEmailVerified=false` when email changes.
- Attempts to send a confirmation email.
- Returns `verificationEmailQueued`.
New authenticated endpoints:
- `POST /api/user/profile/email/resend-verification`
- `POST /api/user/profile/email/verify`
Frontend:
- `updateUserProfile` now unwraps the backend `{ user, verificationEmailQueued }` response correctly.
- Profile page shows a verification code input and resend/verify controls when a user has an unverified email.
- Debug events are emitted for profile update/resend/verify actions.
### Telegram Wallet / TON Support
Frontend:
- Added `@tonconnect/ui-react` and `@ton/core`.
- Added `TonWalletProvider` in the root layout.
- Added `public/tonconnect-manifest.json`.
- Wallet connection page now offers Telegram Wallet / TON connection inside Telegram Mini App.
- Connected TON wallet can be saved as `walletType: "ton"` with provider metadata.
Backend:
- User profile schema now supports:
- `profile.walletType: "evm" | "ton"`
- `profile.walletProvider`
- Current `/user/wallet-address` controller supports TON address validation.
- Legacy `/users/wallet-address` route also accepts TON.
- EVM still requires signature verification.
- TON currently validates address format only and skips EVM signature.
### Frontend Typecheck — All Pre-Existing Errors Resolved
All 13 pre-existing TypeScript errors in the frontend were fixed. `npx tsc --noEmit` now passes clean and can be used as a release gate.
Fixes applied:
- `src/web3/components/provider-payment.tsx` — changed invalid icon `solar:wallet-money-bold` to `solar:wallet-bold`.
- `src/components/payment/shkeeper-payment-widget.tsx` — moved `sellerId` from `metadata` to top-level payload (matches `IPaymentIntentPayload`).
- `src/sections/address/address-form.tsx` — added `as unknown as Resolver<AddressFormData>` double cast for Zod v4 + `@hookform/resolvers` v5 type mismatch.
- `src/sections/address/address-new-form.tsx` — same resolver cast fix.
- `src/sections/request-template/request-template-checkout-payment.tsx` — same resolver cast fix.
- `src/sections/request-template/request-template-new-edit-form.tsx` — added explicit `useForm<RequestTemplateSchemaType>` generic and resolver cast.
### Backend Tests for Email Verification and Wallet
Created `backend/__tests__/user-profile-email-wallet.test.ts` — 27 tests, all passing.
Coverage:
- Profile email update stores normalized email, sets `isEmailVerified=false`, returns `verificationEmailQueued`.
- Duplicate email rejected with 409.
- Resend verification: fails when no email, fails when already verified, succeeds otherwise.
- Verify email code: rejects malformed, rejects expired/wrong, sets `isEmailVerified=true` on correct code.
- TON wallet save: accepts valid friendly address, stores `walletType`, `walletAddress`, `walletProvider`.
- TON wallet save: rejects malformed address.
- EVM wallet save: rejects missing signature, rejects wrong signature.
`emailService` is mocked — no Resend calls are made during tests.
### Email Delivery Confirmed
Resend SMTP relay verified end-to-end. Configuration: port 465 SSL (`SMTP_SECURE=true`), sender `noreply@amn.gg`. Test email delivered successfully during session.
### Telegram Bot Command Handlers and Notification Foundation (Task 5.3)
Created `backend/src/services/telegram/botService.ts` (~480 lines).
Implemented:
- `sendBotMessage(chatId, text, options)` — raw Telegram Bot API call with inline keyboard support.
- `answerCallbackQuery(id, text?)` — acknowledges callback queries.
- `sendTelegramNotification(telegramUserId, text, options)` — looks up active `TelegramLink`, sends message, detects 403 (bot blocked by user) and marks link as `status: 'blocked'` to prevent future sends.
- `dispatchBotUpdate(update)` — routes incoming webhook updates to command handlers or callback handler.
- Command handlers: `/start` (welcome + Mini App button), `/help`, `/link`, `/status`, `/requests`, `/offers`, `/payments`, `/settings`.
- Per-chat rate limiter (10 messages per 10-second window) to prevent abuse.
- Mini App buttons use `web_app` inline keyboard type pointing to `https://amn.gg/telegram/`.
Modified `telegramService.ts`:
- Expanded `TelegramWebhookUpdate` type to include `message.from`, `message.chat`, `callback_query.message`.
- Wired `handleTelegramWebhook` to fire `dispatchBotUpdate` asynchronously (dynamic import) so Telegram always receives HTTP 200 immediately.
Updated `index.ts`:
- Exports: `sendTelegramNotification`, `sendBotMessage`, `NotificationDeliveryResult`, `BotSendOptions`, `InlineKeyboard`.
Created `backend/__tests__/telegram-bot-service.test.ts` — 17 tests, all passing.
Committed: "Implement Telegram bot command handlers and notification foundation (task 5.3)" — 4 files, 1092 insertions.
### Telegram Bot Commands Registered
Registered bot commands with Telegram Bot API (`setMyCommands`):
- `/start` — Open Amanat escrow marketplace
- `/help` — Show help and command list
- `/link` — Link your Amanat account
- `/status` — Check your active escrow status
- `/requests` — Browse service requests
- `/offers` — View your offers
- `/payments` — Check payment status
- `/settings` — Account settings
### PRD — TON Wallet Ownership Proof
Created `nick-doc/07 - Development/PRD - TON Wallet Ownership Proof.md`.
Covers: challenge nonce endpoint, TonConnect proof request flow, backend ed25519 verification with `@ton/ton` + `tweetnacl`, new User model fields, frontend badge/CTA, migration notes, acceptance criteria, and tests.
### TON Wallet Ownership Proof — Backend Implemented
Full backend implementation completed in session 3.
New file: `backend/src/services/user/tonProofService.ts`
- In-memory TTL challenge store keyed by `userId` (5-minute TTL).
- `generateChallenge(userId)` — generates 32-byte hex nonce, returns `{ payload, domain, timestamp }`.
- `verifyTonProof(userId, walletAddress, proof)` — full ed25519 verification:
1. Nonce match and expiry check.
2. Domain must equal `amn.gg`.
3. Timestamp within ±300 s of now.
4. `stateInit` required; its hash must match the claimed wallet address hash (anti-substitution).
5. Public key extracted from wallet data cell (skip 64-bit seqno+subwallet_id, read 32-byte pubkey — works for v3/v4/v5 standard wallets).
6. Signed message reconstructed: `ton-proof-item-v2/ + workChain (BE int32) + address.hash + domainLen (LE uint32) + domain + timestamp (LE int64) + payload (hex)`.
7. `sha256(0xff0xff + "ton-connect" + sha256(message))` — verified with `nacl.sign.detached.verify`.
8. Nonce cleared only on success (allows retry on failure within TTL).
Modified `backend/src/services/user/userController.ts`:
- `updateWalletAddress` extracts optional `tonProof` from body; calls `verifyTonProof` when present; sets `profile.walletProofVerified` and `profile.walletProofTimestamp`; returns `INVALID_TON_PROOF` on failure.
- New `getTonProofChallenge` handler calls `generateChallenge(userId)` and returns the nonce object.
Modified `backend/src/services/user/userControllerRoutes.ts`:
- `POST /api/user/wallet-address/ton-proof/challenge` registered (authenticated).
Modified `backend/src/models/User.ts`:
- Added `walletProofVerified?: boolean` and `walletProofTimestamp?: Date` to both IUser interface and Mongoose schema.
Added `@ton/ton@16.2.4`, `@ton/crypto@3.3.0`, `tweetnacl@1.0.3` to backend `package.json`.
New file: `backend/__tests__/ton-proof.test.ts` — 15 tests, all passing:
- Challenge generation (uniqueness, TTL invalidation).
- Valid proof accepted, nonce cleared (replay rejected).
- Rejection cases: no challenge, nonce mismatch, wrong domain, expired timestamp, missing stateInit, stateInit/address mismatch, bad signature, wrong keypair.
- Nonce preserved on failure (allows retry).
### Telegram Mini App Shell — LTR Fix and Smart Display Name
Modified `frontend/src/sections/telegram/telegram-mini-app-shell.tsx`:
- Added `dir="ltr"` to all three Container states (linked, unlinked, unsupported) so English content does not inherit the page-level RTL direction.
- Added smart `displayName` logic: falls back from Telegram name → auth user name → `"there"` if Telegram returns a generic or empty name (e.g., the literal string `"Telegram user"`).
### Socket Status — Suppressed Inside Telegram Mini App
Modified `frontend/src/socket/components/socket-status.tsx`:
- Added `usePathname` import; added path-guard `if (pathname?.startsWith('/telegram')) return null`.
- The "قطع" (disconnected) chip that briefly appeared on Mini App load was caused by normal Socket.IO connection latency (~0.5 s before handshake). Suppressing it on `/telegram` routes eliminates the false alarm without hiding useful status on other pages.
### EVM WalletConnect Stub Card
Telegram Wallet is TON-only. Request Network requires EVM. MetaMask is not available in Telegram; WalletConnect is the right approach but requires a project ID from `cloud.walletconnect.com`.
Added `EvmWalletStubCard` to `frontend/src/sections/account/account-wallet-connection.tsx`:
- Always rendered below the TON wallet card on the Account Wallet page.
- Shows WalletConnect logo, "به زودی" (coming soon) chip, and explanation.
- Connect button is disabled when `NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID` is not set; tooltip tells developer what to configure.
- Button activates automatically when the env var is added — wire the wagmi `walletConnect()` connector (already commented out in `src/web3/config.ts`) at that point.
Rendered from `frontend/src/sections/account/view/account-wallet-view.tsx`.
## Verification Completed
Backend:
```bash
cd backend
npm run typecheck --if-present
```
Result: passed.
Frontend focused tests:
```bash
cd frontend
npm test -- __tests__/account-test/account-actions.test.ts __tests__/auth/telegram-auth-action.test.ts __tests__/sections/telegram/telegram-mini-app-shell.test.tsx --runInBand
```
Result:
- 3 suites passed
- 38 tests passed
Diff hygiene:
```bash
git diff --check
```
Result: passed in backend and frontend after whitespace cleanup.
Public smoke checks completed during the session:
- `https://amn.gg/` returned HTTP 200.
- `https://amn.gg/tonconnect-manifest.json` returned HTTP 200 with JSON.
- `https://amn.gg/socket.io/?EIO=4&transport=polling` returned HTTP 200 polling handshake.
- `https://amn.gg/dashboard/account/wallet/` returned HTTP 200 after compilation.
## Known Issues Left
### TON Wallet Ownership Proof — Frontend Wiring Pending
Backend is fully implemented and tested (see "Implemented" section above, and PRD `07 - Development/PRD - TON Wallet Ownership Proof.md` — status: `backend-implemented`).
Remaining frontend work:
1. Add `getTonProofChallenge` action in `src/actions/account.ts` (calls `POST /api/user/wallet-address/ton-proof/challenge`).
2. In `handleConnectTelegramWallet` in `account-wallet-connection.tsx`:
- Fetch challenge nonce before opening modal.
- Call `tonConnectUI.setConnectRequestParameters({ state: 'ready', value: { tonProof: payload } })`.
- After connection, read `tonWallet.connectItems?.tonProof?.proof` and send to `PATCH /api/user/wallet-address` with `tonProof` field.
3. Show "Verified ✅" badge when `walletProofVerified: true`; show "Verify ownership" CTA when wallet is saved but unverified.
Until the frontend proof flow is wired, the backend accepts wallet saves without proof (legacy path — `walletProofVerified` will be absent/false). TON wallet addresses saved without proof must not be used for automatic payout routing.
### EVM Wallet (WalletConnect) Not Yet Functional
A stub card is now rendered on the wallet page with a disabled connect button. To activate:
1. Register at `cloud.walletconnect.com` (free), get a project ID.
2. Add `NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=<id>` to `.env.local` (and server env).
3. Uncomment the `walletConnect()` connector in `src/web3/config.ts`.
4. Wire the connect handler in `EvmWalletStubCard` to open the WalletConnect modal.
### Profile Update UX Needs Manual Telegram Test
The backend email verification endpoints are implemented and covered by tests. The UI flow in the actual Telegram Mini App has not been end-to-end verified manually:
- Start with a Telegram-created buyer without email.
- Add email in profile.
- Confirm warning/verification controls appear.
- Confirm Resend works.
- Enter verification code.
- Confirm session refresh shows verified state.
### Pangolin/Newt Routing Not Re-Verified
The desired public shape is one URL:
- Browser and Telegram app: `https://amn.gg`
- Backend proxied by path under the same host.
Expected route intent:
- `/*` → frontend container/service
- `/api/*` → backend container/service
- `/socket.io/*` → backend container/service with WebSocket support
- `/uploads/*` → backend container/service
- `/health` → backend container/service
If Telegram shows network errors, open `https://amn.gg/?debug=1` and check the debug panel API/Socket fields.
### Package Manager State
The frontend project declares Yarn 1 as package manager and `yarn.lock` was updated for TON dependencies.
`package-lock.json` was intentionally restored to avoid unrelated massive lockfile churn.
Next agent should continue using Yarn for dependency changes unless the project standard changes.
### Dev Runtime Note
During dependency installation, the mounted `node_modules` was temporarily broken by an Alpine/Yarn install attempt involving native packages. It was repaired on the host with dependency install and the public app recovered.
If the container shows stale Next errors in logs, check for a later successful startup before treating them as current.
## Current Git State At Handoff (Updated 2026-05-24 Session 3)
### Backend — Session 3 Changes (Uncommitted)
New files:
- `src/services/user/tonProofService.ts` — full TON proof verification service
- `__tests__/ton-proof.test.ts` — 15 unit tests (all passing)
Modified files:
- `src/models/User.ts` — added `walletProofVerified`, `walletProofTimestamp` fields
- `src/services/user/userController.ts` — TON proof challenge handler + proof verification in wallet save
- `src/services/user/userControllerRoutes.ts` — challenge endpoint registered
- `package.json` / `package-lock.json``@ton/ton`, `@ton/crypto`, `tweetnacl` added
Still uncommitted from earlier sessions:
- `src/services/user/userRoutes.ts`
- `__tests__/user-profile-email-wallet.test.ts` (27 tests)
### Frontend — Session 3 Changes (Uncommitted)
Modified:
- `src/sections/account/account-wallet-connection.tsx` — added `EvmWalletStubCard` component
- `src/sections/account/view/account-wallet-view.tsx` — renders `EvmWalletStubCard`
- `src/sections/telegram/telegram-mini-app-shell.tsx``dir="ltr"` on all containers, smart `displayName` fallback
- `src/socket/components/socket-status.tsx` — suppressed on `/telegram` paths
Other files still uncommitted from earlier sessions (listed in Session 2 handoff).
### Vault — Session 3 Changes (Uncommitted)
- `07 - Development/PRD - TON Wallet Ownership Proof.md` — status updated to `backend-implemented`, acceptance criteria checked
- `08 - Operations/Handoff - Telegram Mini App Debug - 2026-05-24.md` — this file
### Recommended Commit Split
1. Backend commit A — user/email/wallet:
- `src/models/User.ts`
- `src/services/user/userController.ts`
- `src/services/user/userControllerRoutes.ts`
- `src/services/user/userRoutes.ts`
- `__tests__/user-profile-email-wallet.test.ts`
2. Frontend commit:
- All frontend files above.
3. Vault commit:
- All vault files above.
Before committing, rerun:
```bash
cd /Users/manwe/CascadeProjects/escrow/backend
npm run typecheck --if-present
npx jest --testPathPattern='user-profile-email-wallet|telegram-bot-service' --runInBand
cd /Users/manwe/CascadeProjects/escrow/frontend
npx tsc --noEmit -p tsconfig.json
```
Do not include secrets, local `.env` files, or regenerated `package-lock.json` churn.
## Suggested Next Steps
1. Implement TON wallet ownership proof per `07 - Development/PRD - TON Wallet Ownership Proof.md`. This is the only remaining security-critical backend item.
2. Manually test profile email update + verification flow in the Telegram Mini App.
3. Recheck Pangolin/Newt routing and confirm Mini App loads via `https://amn.gg/telegram/`.
4. Build Telegram Mini App shell for marketplace workflows (Taskmaster task 5.4).
5. Add frontend tests for `updateWalletAddress` and `TelegramDebugPanel`.
6. Rebuild/redeploy frontend container after dependency changes (`@tonconnect/ui-react`, `@ton/core`).
7. Commit pending vault changes (PRD file, updated handoff).
## Detailed Remaining Work
### 1. ~~Confirm Telegram Bot Web App URL~~ ✓ Done
Bot commands registered via `setMyCommands`. Mini App buttons in `/start` and `/help` use `web_app` type pointing to `https://amn.gg/telegram/`. No BotFather URL change was needed for command dispatch.
If the menu button URL still points to an old URL, update it via BotFather → Bot Settings → Menu Button.
### 2. Recheck Pangolin/Newt Routing
Not touched this session. See "Known Issues Left" section above for routing checklist.
### 3. ~~Finish Email Verification Test Coverage~~ ✓ Done
27 tests in `backend/__tests__/user-profile-email-wallet.test.ts` cover all email verification cases. See "Implemented" section above.
### 4. ~~Finish Wallet Test Coverage~~ ✓ Done (format-only)
Backend tests for TON format validation and EVM signature enforcement are in `user-profile-email-wallet.test.ts`.
Ownership proof tests will be added when TonConnect proof verification is implemented (see item 5).
Recommended frontend tests still pending:
- `updateWalletAddress` sends legacy EVM payload unchanged when no options are passed.
- `updateWalletAddress` sends TON payload without signature when `{ walletType: "ton" }` is passed.
- `TelegramDebugPanel` renders socket/auth/TG fields when debug is enabled.
- Account profile page renders email verification controls for an unverified email.
### 5. Implement TON Ownership Proof
PRD written: `07 - Development/PRD - TON Wallet Ownership Proof.md`.
Implementation not started. See PRD for full spec. Summary:
- Add `yarn add @ton/ton @ton/crypto tweetnacl` to backend.
- Add `POST /api/user/wallet-address/ton-proof/challenge` (generates TTL nonce).
- Update `PATCH /api/user/wallet-address` to verify ed25519 proof when `tonProof` is present.
- Add `profile.walletProofVerified` and `profile.walletProofTimestamp` to User model.
- Frontend: request proof via `tonConnectUI.connectWallet({ tonProof: payload })`, send proof to backend.
- Frontend: show "Verified" badge or "Verify ownership" CTA.
### 6. Check Profile Update UX in Telegram
Backend and tests done. Manual Telegram Mini App test not yet done:
- Start with a Telegram-created buyer without email.
- Add email in profile, confirm page does not discard it.
- Confirm warning/verification controls appear.
- Confirm Resend works and email arrives from `noreply@amn.gg`.
- Enter verification code, confirm `isEmailVerified=true`.
### 7. ~~Resolve Existing Typecheck Debt~~ ✓ Done
All 13 pre-existing frontend TypeScript errors fixed. `npx tsc --noEmit` passes clean. Add it to the frontend verification gate.
### 8. Deployment/Container Follow-Up
Because dependencies changed in `frontend/package.json` and `yarn.lock`, the deployed frontend image/container should be rebuilt from a clean dependency install.
Recommended sequence:
```bash
cd /Users/manwe/CascadeProjects/escrow/frontend
yarn install --frozen-lockfile
yarn test __tests__/account-test/account-actions.test.ts __tests__/auth/telegram-auth-action.test.ts __tests__/sections/telegram/telegram-mini-app-shell.test.tsx --runInBand
cd /Users/manwe/CascadeProjects/escrow
docker compose build frontend backend
docker compose up -d frontend backend
```
If the existing compose setup relies on mounted `node_modules`, verify the mounted dependencies include `@tonconnect/ui-react` and `@ton/core`.
### 9. Spark Agent Status
The user asked to use codex-spark agents. Earlier in this session, existing subagents were already active and a fresh spark verifier could not be spawned because the agent thread limit was reached.
Next agent can continue with spark workers once capacity is available. Good isolated assignments:
- Worker A: backend tests for profile email verification.
- Worker B: backend tests for wallet address EVM/TON behavior.
- Worker C: frontend tests for debug panel and TON wallet action payload.
- Worker D: manual Telegram/Pangolin runtime verification report.
### 10. Safety Notes
- Do not commit Telegram bot token, Resend API key, Pangolin/Newt secret, or any `.env` file.
- Do not revert unrelated vault audit files; they predate this handoff and should be reviewed as separate documentation work.
- Do not regenerate package locks casually. The frontend project declares Yarn 1 and `yarn.lock` is the relevant lockfile for this change.
- Keep Trezor optional; this pass did not modify Trezor safekeeping behavior.
- Payment adapter remains optional; this pass did not force SHKeeper or Request Network.

View File

@@ -0,0 +1,123 @@
---
title: Audit Index — 2026-05-24
tags: [audit, index, security, logic, performance]
created: 2026-05-24
status: open
---
# Audit Index — 2026-05-24
Full-system audit triggered by completion of Telegram first-class auth, Request Network integration, rate-limiting enablement, and funds ledger. Three parallel audit streams were run against actual source code.
| Report | Findings |
|--------|---------|
| [[Security Audit - 2026-05-24]] | 6 critical · 5 high · 7 medium · 4 low |
| [[Logic Audit - 2026-05-24]] | 4 critical · 5 high · 7 medium · 2 low |
| [[Performance Audit - 2026-05-24]] | 6 high · 8 medium · 4 low |
---
## Cross-Cutting Criticals (Fix Immediately)
These items appear in multiple audit streams or are exploitable right now.
| ID | Severity | What | Where | Fix Effort |
|----|----------|------|-------|------------|
| SEC-C3 / LOG-CRIT3 | CRITICAL | Simulated transaction bypass (`SIM_*`) active in production | `paymentRoutes.ts:379` | 1 line — wrap in `NODE_ENV !== 'production'` |
| SEC-C4 | CRITICAL | `forceVerifyUser` gate is wrong (`!== development` → unset env passes) | `authController.ts:1127` | 1 line — flip to `=== 'development'` |
| SEC-C1 / SEC-C2 | CRITICAL | Hardcoded admin password + logged to stdout on every deploy | `init-admin.ts:7,50` | Remove fallback + delete log line + rotate credential |
| SEC-C5 / LOG-CRIT4 | CRITICAL | Hardcoded SHKeeper admin credential in source | `shkeeperPayoutService.ts:224` | Move to env var + rotate |
| SEC-C6 | CRITICAL | Access and refresh tokens share the same JWT signing secret | `authService.ts:18,54` | Add `REFRESH_TOKEN_SECRET` env var |
| LOG-CRIT1 | CRITICAL | Concurrent webhooks can double-process the same payment (no DB-level lock) | `paymentCoordinator.ts` / `shkeeperWebhook.ts` | Atomic `findOneAndUpdate` with status guard |
| LOG-CRIT2 | CRITICAL | Parallel Telegram auth creates orphan User documents (TOCTOU on link+user creation) | `authController.ts:377` | Upsert link first, create user only if upsert won |
---
## High-Priority Queue (Fix Before Soft Launch)
| ID | Stream | What | Where |
|----|--------|------|-------|
| SEC-H2 | Security | SHKeeper webhook authentication bypass via `User-Agent` / `crypto` heuristic | `shkeeperWebhook.ts:95` |
| SEC-H3 | Security | Request Network `allowTestMode: true` hardcoded — test header skips all sig verification | `requestNetworkRoutes.ts:104` |
| SEC-H5 | Security | `global.io.emit(...)` broadcasts financial event data to all connected sockets | `shkeeperWebhook.ts:546` |
| SEC-H4 | Security | Typing indicator IDOR — no chat membership check | `app.ts:267` |
| SEC-H1 | Security | Telegram in-memory replay map reset on restart; replay possible within 24h window | `telegramService.ts:395` |
| LOG-HIGH5 | Logic | `verifyEmailWithCode` non-atomic User.save + TempVerification.delete | `authController.ts:620` |
| LOG-HIGH3 | Logic | `refreshTokens[]` array grows unboundedly | `authController.ts:62` |
| LOG-HIGH4 | Logic | Blocked Telegram user bypasses block when TelegramLink is deleted | `authController.ts:355` |
| LOG-MED5 | Logic | Unauthenticated `/payment/callback` endpoint can mutate any payment status | `paymentControllerRoutes.ts:20` |
| PERF-H3 | Performance | Unbounded seller fan-out on new request: `User.find({role:'seller'})` + N socket emits | `PurchaseRequestService.ts:190` |
| PERF-H4 | Performance | Full chat document (~250 MB for large chats) loaded into memory for every paginated request | `ChatService.ts:370` |
---
## Medium Priority (Fix Before Public Launch)
| ID | Stream | What | Where |
|----|--------|------|-------|
| SEC-M1 | Security | OTP + reset codes logged in plaintext in all environments | `authController.ts:174,715` |
| SEC-M2 | Security | `Math.random()` used for OTP (not CSPRNG) | `authService.ts:226` |
| SEC-M3 | Security | No refresh token reuse/theft detection | `authController.ts:510` |
| SEC-M4 | Security | Profile update mass-assignment + `validateBeforeSave: false` | `authController.ts:921` |
| SEC-M5 | Security | Login Widget auth has no replay protection (Mini App has it) | `authController.ts:110` |
| SEC-M6 | Security | No JWT secret length enforcement at startup | `config/index.ts:42` |
| LOG-MED1 | Logic | Funds ledger availability check + append not atomic (concurrent double-release possible) | `releaseRefundService.ts:37` |
| LOG-MED2 | Logic | `updatePurchaseRequestStatus` bypasses state machine validator | `PurchaseRequestService.ts:551` |
| LOG-MED3 | Logic | `getUserPayments` queries `userId` (wrong field) — always returns empty | `paymentService.ts:342` |
| LOG-MED4 | Logic | Payout created without verifying a completed inbound payment exists | `shkeeperPayoutService.ts:42` |
| PERF-H1 | Performance | N+1: one `Payment.findOne` per request row in buyer dashboard | `PurchaseRequestService.ts:516` |
| PERF-H2 | Performance | Missing index on `Payment.purchaseRequestId` | `models/Payment.ts:190` |
| PERF-M1 | Performance | Missing compound index `(buyerId, createdAt)` on PurchaseRequest | `models/PurchaseRequest.ts:360` |
| PERF-M2 | Performance | Unanchored regex on title/description — full collection scan | `PurchaseRequestService.ts:703` |
| PERF-M7 | Performance | `user-online` event broadcast to all sockets globally | `app.ts:300` |
---
## Low Priority / Hardening
| ID | Stream | What |
|----|--------|------|
| SEC-L1 | Security | Passkey challenge debug logs expose all active challenges + all users' passkey IDs |
| SEC-L2 | Security | Login attempt counters in-memory (multi-replica bypass possible) |
| SEC-L3 | Security | `FRONTEND_URL` unset allows CORS `*` |
| SEC-M7 | Security | Legacy `verifyEmail` token route has no expiry check |
| LOG-LOW1 | Logic | Duplicate `/payment/callback` route definition |
| LOG-MED7 | Logic | `acceptOffer` notification uses undefined `offer.title` |
| PERF-M3 | Performance | Double-fetch pattern in update methods (no `.lean()` on pre-check) |
| PERF-M6 | Performance | `getSellers()` unbounded — no `.limit()` |
| PERF-M8 | Performance | Post-filter after pagination causes wrong `totalItems` count |
---
## Recommended Index Additions
Add these to eliminate the collection scans identified in the performance audit:
```ts
// models/Payment.ts
paymentSchema.index({ purchaseRequestId: 1, status: 1 });
// models/PurchaseRequest.ts
PurchaseRequestSchema.index({ buyerId: 1, createdAt: -1 });
PurchaseRequestSchema.index({ status: 1, createdAt: -1 });
PurchaseRequestSchema.index({ categoryId: 1, status: 1, createdAt: -1 });
PurchaseRequestSchema.index({ title: 'text', description: 'text', tags: 'text' });
```
---
## Items Confirmed Correctly Handled (PASS)
- HMAC timing-safe comparison on all webhooks ✓
- Telegram `initData` HMAC derivation and bot account rejection ✓
- Blocked Telegram user check on existing links ✓
- Refresh token rotation (old removed before new issued) ✓
- Password change / reset clears all refresh tokens ✓
- Socket.IO JWT enforcement on connect ✓
- `join-chat-room` membership check ✓
- bcrypt work factor = 12 ✓
- WebAuthn challenge consumed on first use ✓
- All TTL indexes (TempVerification, TelegramSession, Notification) ✓
- FundsLedgerEntry idempotency key (sparse unique index) ✓
- SHKeeper polling bounded and self-cleaning ✓
- Socket.IO room cleanup on disconnect ✓

View File

@@ -0,0 +1,343 @@
---
title: Logic & Correctness Audit — 2026-05-24
tags: [audit, logic, correctness, findings]
created: 2026-05-24
status: open
---
# Logic & Correctness Audit — 2026-05-24
Full-codebase review of business logic, state machines, race conditions, and data integrity. Triggered by completion of Request Network integration, Telegram first-class auth, and the internal funds ledger. Every finding is derived from actual source code — no hypothetical issues are included.
> [!danger] Action required
> 4 CRITICAL findings: two are exploitable in the current production code path (double-spend via concurrent webhooks, simulated-payment bypass); two cause data corruption (orphan User documents, non-atomic create/delete).
---
## CRITICAL
### CRIT-1 — Concurrent Webhooks Can Double-Process the Same Payment
**File:** `src/services/payment/shkeeper/shkeeperWebhook.ts:195241` and `src/services/payment/paymentCoordinator.ts:2560`
**Status:** Open
The duplicate-detection guard uses an in-memory 10-second window. There is no database-level atomic lock. Two simultaneous webhook deliveries for the same `external_id` will both read `payment` before either writes, both pass the in-memory deduplication check, both enter `PaymentCoordinator.coordinatePaymentUpdate`, and both proceed — because `coordinationState` is an in-memory `Map` whose `get/set` pair is not atomic under async concurrency (any `await` between them allows the event loop to interleave).
The `status === 'completed'` DB guard at `paymentCoordinator.ts:54` only protects re-processing of already-completed payments. The first webhook has not yet written `completed` when the second arrives, so both pass.
Side effects of double-processing:
- Duplicate chats created between buyer and seller (`chatService.createChat` has no uniqueness guard).
- Duplicate socket events sent to the buyer.
- Depending on timing, duplicate state transitions on `PurchaseRequest`.
**Remediation:** Replace the coordinator's check with an atomic MongoDB conditional update:
```ts
const result = await Payment.findOneAndUpdate(
{ _id: paymentId, status: { $ne: 'completed' } },
{ $set: { status: 'completed', ... } },
{ new: true }
);
if (!result) return; // already processed
```
For chat creation, add a unique compound index on `{ 'relatedTo.type': 1, 'relatedTo.id': 1 }` and handle the duplicate key error gracefully.
---
### CRIT-2 — Telegram Auto-Provisioning Race Creates Orphan User Documents
**File:** `src/services/auth/authController.ts:377434`
**Status:** Open
The flow is:
1. `TelegramLink.findOne({ telegramUserId })` → null (new user)
2. `new User({...}).save()` — creates User document
3. `TelegramLink.findOneAndUpdate({ telegramUserId }, ..., { upsert: true })`
Two parallel Telegram auth requests for the same `telegramUserId` both find no link, both create a new `User`, and both try to upsert `TelegramLink`. Because `TelegramLink.telegramUserId` has `unique: true`, only one upsert wins. The loser's `User.save()` has already committed, producing a fully-active `User` document with no `TelegramLink` — it can never be authenticated via Telegram and will not be cleaned up.
**Remediation:** Use optimistic locking or a MongoDB transaction. Practical approach without transactions:
```ts
// Step 1: try to upsert the link first
const link = await TelegramLink.findOneAndUpdate(
{ telegramUserId },
{ $setOnInsert: { telegramUserId, source, ... } },
{ upsert: true, new: true, rawResult: true }
);
const isNew = link.lastErrorObject?.upserted != null;
// Step 2: create User only if this process won the upsert
if (isNew) { user = await User.create({...}); await link.value.updateOne({ userId: user._id }); }
else { user = await User.findById(link.value.userId); }
```
---
### CRIT-3 — Simulated Transaction Bypass Active in Production
**File:** `src/services/payment/paymentRoutes.ts:379396`
**Status:** Open
*(Also cited in Security Audit C-3.)*
```ts
if (paymentHash.startsWith('SIM_') || ...) {
isVerified = true;
}
```
No environment guard. A client sending `paymentHash: "SIM_anything"` gets a completed `Payment` and the `PurchaseRequest` advances without any real funds.
**Remediation:** Wrap in `if (process.env.NODE_ENV !== 'production')`.
---
### CRIT-4 — Hardcoded Credential in Production Payment Code
**File:** `src/services/payment/shkeeper/shkeeperPayoutService.ts:224228`
**Status:** Open
*(Also cited in Security Audit C-5.)*
```ts
'Authorization': `Basic ${Buffer.from(`admin:!NMI4WdGkVQ#dQ`).toString('base64')}`
```
**Remediation:** `process.env.SHKEEPER_ADMIN_PASSWORD` — rotate immediately.
---
## HIGH
### HIGH-1 — `updatePurchaseRequest` Has a Read-Modify-Write Race Condition
**File:** `src/services/marketplace/PurchaseRequestService.ts:405425`
**Status:** Open
```ts
const currentRequest = await PurchaseRequest.findById(requestId); // read
// ... validate status ...
await PurchaseRequest.findByIdAndUpdate(requestId, updateData, { new: true }); // write
```
Two concurrent calls both read the same `currentRequest.status`, both pass `isValidStatusProgression`, and both write — potentially into conflicting terminal states or setting the same field twice.
Furthermore, `isValidStatusProgression` allows any status → any terminal state transition (e.g., `pending``completed` in one step), because the guard only blocks backward progression among non-terminal states.
**Remediation:** Use atomic optimistic locking:
```ts
const updated = await PurchaseRequest.findOneAndUpdate(
{ _id: requestId, status: currentStatus }, // guard: status unchanged
{ $set: { status: newStatus } },
{ new: true }
);
if (!updated) throw new ConflictError('State changed concurrently — retry');
```
Additionally, tighten `isValidStatusProgression` to use an explicit allowed-transitions table rather than a blanket terminal-from-anywhere rule.
---
### HIGH-2 — Seller Offer Acceptance Has No Concurrency Guard
**File:** `src/services/marketplace/SellerOfferService.ts:355426`
**Status:** Open
`acceptOffer` reads the offer, then updates it in a separate write. Two simultaneous accept calls for different offers on the same purchase request will both succeed: both set their target offer to `accepted`, and both run `updateMany` to reject all others — each query excluding its own `offerId`. The result is two `accepted` offers on the same purchase request.
**Remediation:** Use `findOneAndUpdate` with a status guard:
```ts
const updated = await SellerOffer.findOneAndUpdate(
{ _id: offerId, status: 'pending' },
{ $set: { status: 'accepted' } },
{ new: true }
);
if (!updated) throw new ConflictError('Offer already processed');
await SellerOffer.updateMany(
{ purchaseRequestId, _id: { $ne: offerId } },
{ $set: { status: 'rejected' } }
);
```
Consider wrapping in a MongoDB transaction to keep the accept + reject-others atomic.
---
### HIGH-3 — `refreshTokens[]` Array Grows Unboundedly
**File:** `src/services/auth/authController.ts:6263` and login handler
**Status:** Open
Every `login` pushes a new refresh token onto `user.refreshTokens[]` without any cap or expiry pruning. The `User` model has no `maxlength` on this array. A user logging in from many devices, or an attacker rapidly requesting tokens, causes the array to grow without limit — increasing document size and slowing any query that reads the user document.
The `refreshToken` rotation endpoint prunes the old token on rotation, but `login` does not prune expired tokens before pushing.
**Remediation:** Before pushing, prune expired tokens:
```ts
const now = Date.now() / 1000;
user.refreshTokens = user.refreshTokens.filter(t => {
try { return jwt.decode(t)?.exp > now; } catch { return false; }
});
```
Enforce a hard cap (e.g. `slice(-10)`) after pruning.
---
### HIGH-4 — Blocked Telegram User Can Bypass Block via Deleted TelegramLink
**File:** `src/services/auth/authController.ts:355363`
**Status:** Open
The block check queries `TelegramLink` where `status = 'blocked'` AND `blockedReason != 'unlinked_by_user'`. If the blocked user's `TelegramLink` is **deleted** entirely (not just status-changed), the check finds nothing, `activeLink` finds nothing, and the code auto-provisions a brand-new `User` with a fresh `TelegramLink` — bypassing the block entirely.
**Remediation:** Store the blocked Telegram user ID on the `User` document (a `blockedTelegramIds` set at the app level, or a `status: 'suspended'` flag tied to the Telegram ID) so the block persists even without a `TelegramLink` record.
---
### HIGH-5 — `verifyEmailWithCode`: Non-Atomic User Create + TempVerification Delete
**File:** `src/services/auth/authController.ts:620633`
**Status:** Open
```ts
await user.save(); // line 630
await TempVerification.findByIdAndDelete(tempVerification._id); // line 633
```
If `user.save()` succeeds but the delete fails (network error, timeout), the `TempVerification` document remains. The next call with the same `email+code` will find it valid, attempt `User.create` again, and hit the unique email index — returning a 500 error instead of a clean "already verified" message. This persists until the MongoDB TTL scan runs (up to 15 min + scan interval).
**Remediation:** Delete the TempVerification **before** saving the user, or check for duplicate-key errors after `user.save()` and delete the TempVerification in the catch handler.
---
## MEDIUM
### MED-1 — Funds Ledger Availability Check Is Not Atomic with Append
**File:** `src/services/payment/orchestration/releaseRefundService.ts:3755,99114`
**Status:** Open
`validateReleaseAvailability` reads a balance via aggregation, checks `availableBalance >= amount`, then `appendFundsLedgerEntry` inserts the entry. Two concurrent release/refund calls can both read the same balance snapshot, both pass the availability check, and both insert entries — resulting in a double-spend from the ledger.
The `idempotencyKey` on `FundsLedgerEntry` prevents exact duplicates when the same `txHash` is used, but does not prevent two different `txHash` values from both passing the availability check concurrently.
**Remediation:** Wrap the check + insert in a MongoDB transaction, or add a single-release constraint: before inserting a `release` entry, verify no prior `release` entry exists for the same `paymentId`.
---
### MED-2 — `updatePurchaseRequestStatus` Bypasses the State Machine Validator
**File:** `src/services/marketplace/PurchaseRequestService.ts:551589`
**Status:** Open
`updatePurchaseRequestStatus` calls `PurchaseRequest.findByIdAndUpdate` directly, without calling `isValidStatusProgression`. The webhook path (`paymentCoordinator.ts:208`) uses this method, meaning payment-triggered state transitions skip the state machine guard entirely.
**Remediation:** Add `isValidStatusProgression(currentStatus, newStatus)` check inside `updatePurchaseRequestStatus` before writing.
---
### MED-3 — `getUserPayments` Queries Non-Existent Field `userId`
**File:** `src/services/payment/paymentService.ts:342346`
**Status:** Open
```ts
query.userId = userId;
```
The `Payment` schema has no top-level `userId` field. The buyer is stored as `buyerId` and the seller as `sellerId`. This query will always return zero results for current payments; only legacy payments with `metadata.legacyUserId` are returned (none for new users). The endpoint appears non-functional for all users.
**Remediation:**
```ts
query.$or = [{ buyerId: userId }, { sellerId: userId }];
```
---
### MED-4 — Payout Created Without Verifying an Inbound Completed Payment Exists
**File:** `src/services/payment/shkeeper/shkeeperPayoutService.ts:4280`
**Status:** Open
`createPayoutTask` takes `buyerId`, `sellerId`, `purchaseRequestId`, and `amount` from the request body. The service checks for duplicate payouts (`existingPayout` idempotency) but does not verify that a corresponding inbound `Payment` with `status: 'completed'` and `direction: 'in'` exists. An admin could accidentally initiate a payout for a purchase request that has never been paid.
**Remediation:**
```ts
const inboundPayment = await Payment.findOne({
purchaseRequestId, direction: 'in', status: 'completed'
});
if (!inboundPayment) throw new AppError(400, 'No completed inbound payment for this request');
```
---
### MED-5 — Unauthenticated `/payment/callback` Endpoint Can Mutate Payment Status
**File:** `src/services/payment/paymentControllerRoutes.ts:20`
**Status:** Open
`POST /callback` is mounted with no authentication middleware. The handler accepts `{ transactionId, status, amount }` and updates a payment's status to any value without verification. Unlike the SHKeeper webhook (which has optional HMAC), this endpoint has zero authentication.
**Remediation:** Either remove the endpoint (the SHKeeper webhook path supersedes it), restrict to internal network only (Nginx `allow` rules), or add HMAC verification.
---
### MED-6 — In-Memory Payment Coordinator State Not Shared Across Replicas
**File:** `src/services/payment/paymentCoordinator.ts:2425`
**Status:** Open
The `coordinationState` Map is process-local. In a multi-process deployment (PM2 cluster, k8s with >1 replica), the 10-second debounce window provides no protection — each process has its own empty map at startup, and webhooks routed to different replicas will both process. The existing DB `status === 'completed'` guard is the only durable protection.
*(The code comment at line 24 acknowledges this. Fix is the same as CRIT-1 — atomic DB conditional update makes this moot.)*
---
### MED-7 — Seller Offer Notification Uses Undefined `offer.title` Field
**File:** `src/services/marketplace/SellerOfferService.ts:403,415`
**Status:** Open
`SellerOffer` does not have a `title` field directly — the title belongs to the associated `PurchaseRequest`. `offer.title` resolves to `undefined` in the notification message body, resulting in broken notification text for buyers and sellers.
**Remediation:** Either populate the `purchaseRequestId` and use `purchaseRequest.title`, or populate the field name from the offer's own descriptive field.
---
## LOW
### LOW-1 — Duplicate Callback Route Definitions
**File:** `src/services/payment/paymentRoutes.ts:29,256`
**Status:** Open
`POST /callback` is defined at line 29 (mapped to `handlePaymentCallback`) and again as `POST /payments/callback` at line 256. These resolve to different URL paths given the router mount, but suggest dead code. No security impact; creates maintenance confusion.
**Remediation:** Remove the dead route definition.
---
### LOW-2 — `paymentCoordinator.ts` Map Grows Without Bound Until Cleanup Fires
**File:** `src/services/payment/paymentCoordinator.ts:35`
**Status:** Open
The cleanup function fires after each coordination cycle but only on active payments. Under a webhook flood before cleanup, the Map accumulates entries. Not a crash risk at current scale; becomes a concern at high webhook volume.
**Remediation:** The atomic DB fix in CRIT-1 eliminates the need for the Map entirely. Until then, add a size cap and a periodic cleanup interval independent of payment processing.
---
## Confirmed PASS
| Check | Result | Source |
|-------|--------|--------|
| TempVerification TTL index | PASS | `TempVerification.ts:59``expireAfterSeconds: 0` on expiry field |
| Legacy email token cleared on first use | PASS | `authController.ts:667` — token set to undefined before save |
| SHKeeper HMAC timing-safe comparison | PASS | `shkeeperWebhook.ts:84``crypto.timingSafeEqual` |
| Socket event DB as source of truth | PASS | DB write committed before emit; emit failure is non-fatal |
| `FundsLedgerEntry` idempotency key (unique) | PASS | `FundsLedgerEntry.ts:86` — sparse unique index |
| `TelegramSession` TTL index | PASS | `TelegramSession.ts:66` |
| Password change clears refresh tokens | PASS | `authController.ts:887` |
---
## Summary
| Severity | Count |
|----------|-------|
| CRITICAL | 4 |
| HIGH | 5 |
| MEDIUM | 7 |
| LOW | 2 |
**Top priority:** CRIT-1 (concurrent webhook double-processing) and CRIT-2 (Telegram race condition) require atomic DB operations. CRIT-3 (simulation bypass) and CRIT-4 (hardcoded credential) are one-line fixes.
---
## Related
- [[Security Audit - 2026-05-24]]
- [[Performance Audit - 2026-05-24]]
- [[Funds Ledger and Escrow State Machine Specification]]
- [[Payment Provider Adapter Spec]]
- [[Authentication Flow]]
- [[Platform Logical Audit - 2026-05-24]]

View File

@@ -0,0 +1,338 @@
---
title: Performance Audit — 2026-05-24
tags: [audit, performance, database, findings]
created: 2026-05-24
status: open
---
# Performance Audit — 2026-05-24
Performance review of MongoDB queries, indexes, in-memory data structures, Socket.IO emission patterns, and pagination. Triggered by completion of Request Network integration, Telegram auth, and the internal funds ledger. Every finding is derived from actual source code.
> [!warning] Scale risk
> H3 (unbounded seller fan-out) and H4 (full chat document in memory) are time-bombs that will cause outages once user counts and message volumes grow past modest scale. Both have zero performance headroom at their current implementation.
---
## HIGH
### H1 — N+1 Query: `getPurchaseRequestsByBuyer` Issues One Payment Lookup Per Request Row
**File:** `src/services/marketplace/PurchaseRequestService.ts:516534`
**Status:** Open
```ts
await Promise.all(requests.map(async (request) =>
Payment.findOne({ purchaseRequestId: request._id })
))
```
A page of 10 requests produces 11 DB round-trips (1 list + 10 payment lookups). With a 50-item page, that is 51 round-trips on every buyer dashboard load.
**Remediation:**
```ts
const requestIds = requests.map(r => r._id);
const payments = await Payment.find({ purchaseRequestId: { $in: requestIds } }).lean();
const paymentMap = new Map(payments.map(p => [p.purchaseRequestId.toString(), p]));
// then: requestWithPayment = { ...r, payment: paymentMap.get(r._id.toString()) }
```
---
### H2 — Missing Index on `Payment.purchaseRequestId`
**File:** `src/models/Payment.ts:190196`
**Status:** Open
The Payment schema indexes `buyerId`, `sellerId`, `status`, `transactionHash`, and `providerPaymentId` — but not `purchaseRequestId`. Every call to `Payment.findOne({ purchaseRequestId })` (used in `getPurchaseRequestsByBuyer`, `getPurchaseRequestById`, payment coordinator, notification handlers) causes a full collection scan.
**Remediation:** Add to `Payment.ts`:
```ts
paymentSchema.index({ purchaseRequestId: 1, status: 1 });
```
---
### H3 — Unbounded Seller Fan-Out on New Public Purchase Request
**File:** `src/services/marketplace/PurchaseRequestService.ts:190191`
**Status:** Open
```ts
sellers = await User.find({ role: "seller", status: "active" }).select(...)
```
When a public purchase request is created, every active seller is fetched with no limit. For 10 000 sellers:
- 10 000 `Notification` documents are inserted (not bulk-inserted — individual creates in a loop).
- Each insert calls `emitRealTimeNotification``getUnreadCount` (an additional `countDocuments` query per seller = 10 000 extra queries).
- A 50 ms staggered `setTimeout` per seller means the event loop is managing 500 seconds of scheduled callbacks.
**Remediation:**
1. Cap the fan-out with a configurable batch limit (e.g., 500 most-relevant sellers by category).
2. Use `Notification.insertMany(docs)` for bulk creation.
3. Push a single room-broadcast (`io.to('sellers').emit(...)`) rather than per-user socket emits.
4. Move heavy fan-out to a background job queue (Bull/BullMQ).
---
### H4 — Full Chat Document with All Embedded Messages Loaded for Pagination
**File:** `src/services/chat/ChatService.ts:370408`
**Status:** Open
```ts
const chat = await Chat.findById(chatId).populate("messages.senderId", ...)
const messages = chat.messages.sort(...).slice(skip, skip + limit)
```
A chat with 50 000 messages (~5 KB each = 250 MB) is fully loaded into Node.js memory on every paginated message request. Pagination happens in JavaScript after the full document is fetched.
**Remediation (minimum):** Use MongoDB's `$slice` projection:
```ts
const chat = await Chat.findById(chatId, {
messages: { $slice: [skip, limit] },
participants: 1,
unreadCounts: 1
}).lean();
```
**Recommended long-term:** Extract messages into a separate `Message` collection with server-side pagination (`Message.find({ chatId }).sort({ timestamp: -1 }).skip(skip).limit(limit).lean()`).
---
### H5 — Extra `countDocuments` Query on Every Single Notification Creation
**File:** `src/services/notification/NotificationService.ts:131152`
**Status:** Open
`emitRealTimeNotification` calls `emitUnreadCountUpdate`, which calls `getUnreadCount` (a full `countDocuments` query) synchronously within the notification creation hot path. When H3's 10 000-seller fan-out fires, this produces 10 000 additional `countDocuments` queries in rapid succession.
**Remediation:** Maintain an unread count field directly on the `User` or in a Redis hash. Increment on notification insert rather than re-counting. Or skip the re-query and have the client increment its local counter on socket event receipt.
---
### H6 — `getUserPayments` Queries Non-Indexed Field and Has No Default Limit
**File:** `src/services/payment/paymentService.ts:331370`
**Status:** Open
The query builds `query.userId = userId` (line 343), but the Payment schema stores buyers as `buyerId` (ObjectId) — not `userId`. The query hits `metadata.legacyUserId`, which has no index. Additionally, if `options.limit` is not supplied the query returns all matching documents with no limit.
**Remediation:**
1. Fix field: `query.$or = [{ buyerId: userId }, { sellerId: userId }]`
2. Add default limit: `.limit(options.limit ?? 100)`
---
## MEDIUM
### M1 — Missing Compound Index `(buyerId, createdAt)` on PurchaseRequest
**File:** `src/models/PurchaseRequest.ts:360369`
**Status:** Open
`getPurchaseRequestsByBuyer` queries `{ buyerId }` with `.sort({ createdAt: -1 })`. The existing single-field index on `buyerId` satisfies the filter but forces MongoDB to sort results post-scan. A compound index eliminates the sort entirely.
**Remediation:** Add to `PurchaseRequest.ts`:
```ts
PurchaseRequestSchema.index({ buyerId: 1, createdAt: -1 });
PurchaseRequestSchema.index({ status: 1, createdAt: -1 });
PurchaseRequestSchema.index({ categoryId: 1, status: 1, createdAt: -1 });
```
---
### M2 — Unanchored Case-Insensitive Regex Search — Full Collection Scan
**File:** `src/services/marketplace/PurchaseRequestService.ts:703720`
**Status:** Open
```ts
{ title: { $regex: searchTerm, $options: "i" } }
```
An unanchored, case-insensitive regex cannot use a B-tree index. MongoDB performs a full collection scan with in-memory string matching on every search request.
**Remediation:** Create a text index:
```ts
PurchaseRequestSchema.index({ title: 'text', description: 'text', tags: 'text' });
```
Query with `{ $text: { $search: searchTerm } }` and sort by `{ score: { $meta: 'textScore' } }`.
---
### M3 — Double-Fetch Pattern in Update Methods (No `.lean()` on Pre-Check)
**File:** `src/services/marketplace/PurchaseRequestService.ts:406425,557572`
**Status:** Open
Both `updatePurchaseRequest` and `updatePurchaseRequestStatus` call `PurchaseRequest.findById(requestId)` to validate current status, then call `findByIdAndUpdate`. The pre-check `findById` instantiates a full Mongoose document that is immediately discarded.
**Remediation:** Use `findByIdAndUpdate` with `{ new: false }` to get the pre-update document in a single round-trip, or use `.lean().select('status')` on the pre-check to minimize overhead.
---
### M4 — Regex on Embedded `messages.content` Array — Full Array Deserialization
**File:** `src/services/chat/ChatService.ts:318320`
**Status:** Open
```ts
{ "messages.content": { $regex: filters.search, $options: "i" } }
```
MongoDB must deserialize the entire embedded `messages` array for every chat document to evaluate this filter. There is no index support for nested array field regex searches.
**Remediation:** Move to a dedicated `Message` collection with a text index on `content`, then query `Message.find({ chatId: { $in: userChatIds }, $text: { $search: searchTerm } })`.
---
### M5 — In-Memory Coordination State Not Shared Across Replicas
**File:** `src/services/payment/paymentCoordinator.ts:25`
**Status:** Open
The `coordinationState` Map is process-local. In a multi-replica deployment, each process starts with an empty map; the 10-second debounce provides no cross-process deduplication. The DB guard in CRIT-1 (Logic Audit) is the correct production fix; this Map is supplementary at best.
*(Code comment at line 24 already acknowledges this — a Redis migration is planned.)*
---
### M6 — `getSellers()` Is Unbounded (No Limit)
**File:** `src/services/marketplace/PurchaseRequestService.ts:728741`
**Status:** Open
```ts
await User.find({ role: "seller", isEmailVerified: true }).select(...).lean()
```
No `.limit()`. Returns every seller in the collection. This method is exposed via API.
**Remediation:** Add `.limit(500)` at minimum, or paginate with a cursor.
---
### M7 — `user-online` Event Broadcast to All Connected Sockets Globally
**File:** `src/app.ts:300`
**Status:** Open
```ts
socket.broadcast.emit('user-status-change', { userId, status: 'online', ... })
```
This pushes the online-presence event to every connected socket regardless of whether they have a relationship with the user. With 10 000 concurrent connections, this is 10 000 individual socket writes per login event.
**Remediation:** Restrict to relevant rooms. For a marketplace, "relevant" means chat participants and active purchase request counterparties:
```ts
userChatIds.forEach(chatId => {
socket.to(`chat-${chatId}`).emit('user-status-change', { userId, status: 'online' });
});
```
---
### M8 — Post-Filter After Pagination Causes Wrong `totalItems` Count
**File:** `src/services/marketplace/PurchaseRequestService.ts:258337`
**Status:** Open
The code paginates the base query, then applies visibility filtering (offer ownership, anonymization) after pagination. This means `totalItems` in the response is the pre-filter count, not the count of items the caller can actually see. Users may see fewer items than indicated by the pagination metadata (e.g., "showing 8 of 10" when only 6 are visible).
**Remediation:** Move visibility filtering into the MongoDB query (aggregation pipeline with `$match` conditions) so `countDocuments` reflects the actual visible set.
---
## LOW
### L1 — `getUserChats` Runs Count Query Sequentially After Find
**File:** `src/services/chat/ChatService.ts:324335`
**Status:** Open
`Chat.find(query)` and `Chat.countDocuments(query)` are independent queries run sequentially. They should run in parallel.
**Remediation:**
```ts
const [chats, total] = await Promise.all([
Chat.find(query).lean(),
Chat.countDocuments(query)
]);
```
*(The notification service at `NotificationService.ts:5361` already does this correctly — apply the same pattern.)*
---
### L2 — `findByIdAndUpdate` Results Not Using `.lean()`
**File:** `src/services/marketplace/PurchaseRequestService.ts:418425,566572`
**Status:** Open
`findByIdAndUpdate` with `.populate(...)` returns full Mongoose document objects for data that is only read and returned to the client. `.lean()` saves ~1530% memory and processing per returned document.
**Remediation:** Add `.lean()` to read-only `findByIdAndUpdate` calls.
---
### L3 — Telegram Replay Maps Are Process-Local (Multi-Replica Gap, Same as M5)
**File:** `src/services/telegram/telegramService.ts:395396`
**Status:** Open
```ts
const initDataReplayWindow = new Map<string, number>();
const webhookReplayWindow = new Map<string, number>();
```
`cleanupReplayMap` is called lazily before each check — entries are evicted correctly once the window expires, so the Maps do not grow unboundedly under normal load. However, like `coordinationState`, they are process-local and will not catch replays across multiple Node replicas.
**Remediation:** Redis `SET NX EX` (same as Security Audit H-1 recommendation).
---
### L4 — PasskeyService Challenge Map: 5-Minute Cleanup Is Correct, Process-Local Only
**File:** `src/services/auth/passkeyService.ts:5663`
**Status:** Open
Cleanup is implemented and the code comment (lines 5055) already notes the Redis migration path. Not a performance risk at current scale. Same multi-replica caveat as L3.
---
## Confirmed PASS
| Check | Result | Source |
|-------|--------|--------|
| `TempVerification` TTL index | PASS | `TempVerification.ts:59` — MongoDB-native TTL, no app polling |
| `TelegramSession` TTL index | PASS | `TelegramSession.ts:66` |
| `Notification` 90-day TTL index | PASS | `Notification.ts:77``expireAfterSeconds: 7776000` |
| `FundsLedgerEntry` indexes (purchaseRequestId, paymentId, idempotencyKey) | PASS | `FundsLedgerEntry.ts:86107` |
| SHKeeper polling rate bounded | PASS | `shkeeperSimpleAuto.ts:64` — 2-minute interval, self-cleaning |
| Socket.IO room cleanup on disconnect | PASS | Socket.IO removes socket from all rooms automatically on disconnect |
| `getUserNotifications` count + find in parallel | PASS | `NotificationService.ts:5361` — already uses `Promise.all` |
| Telegram replay map bounded (lazy eviction) | PASS | `telegramService.ts:100``cleanupReplayMap` evicts expired entries |
---
## Index Additions Summary
The following indexes should be added to eliminate collection scans identified in this audit:
```ts
// Payment.ts
paymentSchema.index({ purchaseRequestId: 1, status: 1 });
// PurchaseRequest.ts
PurchaseRequestSchema.index({ buyerId: 1, createdAt: -1 });
PurchaseRequestSchema.index({ status: 1, createdAt: -1 });
PurchaseRequestSchema.index({ categoryId: 1, status: 1, createdAt: -1 });
// PurchaseRequest.ts (text search)
PurchaseRequestSchema.index({ title: 'text', description: 'text', tags: 'text' });
```
Caution: adding indexes increases write latency and storage. Run `db.collection.getIndexes()` before adding to avoid duplicating existing indexes (some `unique: true` fields already create indexes automatically).
---
## Summary
| Severity | Count |
|----------|-------|
| HIGH | 6 |
| MEDIUM | 8 |
| LOW | 4 |
**Highest urgency:** H3 (seller fan-out) and H4 (full chat in memory) will cause visible degradation with modest growth. H1 (N+1 payments) and H2 (missing index) are easy wins with high query-count impact.
---
## Related
- [[Security Audit - 2026-05-24]]
- [[Logic Audit - 2026-05-24]]
- [[Backend Architecture]]
- [[Data Model Overview]]
- [[Funds Ledger and Escrow State Machine Specification]]

View File

@@ -0,0 +1,405 @@
---
title: Security Audit — 2026-05-24
tags: [audit, security, findings]
created: 2026-05-24
status: open
---
# Security Audit — 2026-05-24
Full-codebase security review triggered by the completion of Telegram first-class auth, Request Network payment integration, and rate-limiting enablement. Every finding below was verified against actual source code — no hypothetical issues are included.
> [!danger] Action required
> 6 CRITICAL findings exist. C-3 and C-4 are exploitable in any non-`development` environment right now and must be fixed before staging is accessible to external testers.
---
## CRITICAL
### C-1 — Hardcoded Admin Password as Fallback Default
**File:** `src/infrastructure/database/init-admin.ts:7`
**Status:** Open
```ts
const adminPassword = process.env.ADMIN_PASSWORD || 'Moji6364';
```
If `ADMIN_PASSWORD` is not set, the admin account is seeded with a password that is now committed to version control. Combined with the predictable default email (`admin@marketplace.com`), the admin account is trivially discoverable.
**Remediation:**
- Remove the `|| 'Moji6364'` fallback entirely.
- Add a startup assertion: `if (!process.env.ADMIN_PASSWORD) throw new Error('ADMIN_PASSWORD is required')`.
- Rotate the credential immediately on any environment where the default may have been used.
---
### C-2 — Admin Password Logged to Stdout on Every Fresh Deploy
**File:** `src/infrastructure/database/init-admin.ts:50`
**Status:** Open
```ts
console.log(`🔑 Password: ${adminPassword}`);
```
The raw admin password (env-supplied or hardcoded default) is written to stdout on every seeding run. Container log aggregators, CloudWatch, Sentry breadcrumbs, and anyone with log-viewer access will have the credential.
**Remediation:** Delete line 50 entirely. Log only `"Admin user created successfully"` — never the credential value.
---
### C-3 — Simulated Transaction Bypass Active in Production
**File:** `src/services/payment/paymentRoutes.ts:379396`
**Status:** Open
```ts
if (paymentHash.startsWith('SIM_') ||
(paymentHash.startsWith('0x') && paymentHash.length < 64)) {
isVerified = true;
...
}
```
No environment guard. Any client that sends `paymentHash: "SIM_anything"` in production gets `isVerified = true`, causing a real `Payment` record to be created with `status: 'completed'` and `PurchaseRequest` to advance to `processing` — without any actual funds.
**Remediation:**
```ts
if (process.env.NODE_ENV !== 'production') {
// SIM_ test path
}
```
Or gate on a separate `ENABLE_PAYMENT_SIMULATION=true` env flag that is absent from production env files.
---
### C-4 — `forceVerifyUser` Gate Uses Wrong Condition
**File:** `src/services/auth/authController.ts:11271159` and `src/services/auth/authRoutes.ts:60`
**Status:** Open
```ts
if (process.env.NODE_ENV !== "development") {
return ResponseHandler.forbidden(res, "...");
}
```
`undefined !== "development"` is `true`, so when `NODE_ENV` is unset (common in CI, some staging configs) the guard **passes** and any unauthenticated caller can verify any user's email instantly, bypassing the entire verification flow.
**Remediation:**
- Change condition to `process.env.NODE_ENV === "development"` (allowlist, not denylist).
- Require an admin JWT on the route regardless of env.
- Consider removing the route from `authRoutes.ts` and only mounting it conditionally at the app level.
---
### C-5 — Hardcoded SHKeeper Admin Credential in Source
**File:** `src/services/payment/shkeeper/shkeeperPayoutService.ts:224228`
**Status:** Open
```ts
'Authorization': `Basic ${Buffer.from(`admin:!NMI4WdGkVQ#dQ`).toString('base64')}`
```
A plaintext password is committed to version-controlled source. It will be included in every build artifact, Docker image layer, and any repository fork or export.
**Remediation:**
- Replace with `process.env.SHKEEPER_ADMIN_PASSWORD`.
- Add to required-env-vars startup check.
- Rotate the credential immediately.
---
### C-6 — Access Token and Refresh Token Share the Same Signing Secret
**File:** `src/services/auth/authService.ts:18,54`
**Status:** Open
Both `generateToken` (access) and `generateRefreshToken` (refresh) use `this.JWT_SECRET`. The only distinction between token types is the `type: 'refresh'` payload claim — a field under caller control. Algorithm-confusion or secret-leakage attacks can forge long-lived refresh tokens using the access token secret.
**Remediation:**
- Introduce `REFRESH_TOKEN_SECRET` env var (separate value, ≥32 chars).
- Sign refresh tokens with it; verify refresh tokens only against it.
- This fully segregates the two token families.
---
## HIGH
### H-1 — Telegram Replay Map Lost on Server Restart (In-Memory Only)
**File:** `src/services/telegram/telegramService.ts:395407`
**Status:** Open
```ts
const initDataReplayWindow = new Map<string, number>();
```
`initData` replay protection, the webhook replay window, and the Request Network delivery ID deduplication are all stored in process-local `Map` objects. Any process restart, pod restart, or scale-out creates a fresh empty map. A captured `initData` is replayable immediately after any restart, within the configured max-age window (up to 24 hours).
**Remediation:** Replace with Redis `SET NX EX` pattern:
```ts
const key = `replay:telegram:${fingerprint}`;
const inserted = await redis.set(key, '1', 'NX', 'EX', windowSeconds);
if (!inserted) throw new ReplayError();
```
---
### H-2 — SHKeeper Webhook Authentication Has Spoofable Bypass
**File:** `src/services/payment/shkeeper/shkeeperWebhook.ts:95103`
**Status:** Open
```ts
const isShkeeperWebhook =
req.headers['x-shkeeper-api-key'] ||
req.headers['user-agent']?.includes('python-requests') ||
payload.crypto === 'BNB-USDT';
```
Any caller who sets `User-Agent: python-requests/2.x.x` or includes `"crypto": "BNB-USDT"` in the body can inject arbitrary payment completion events. The `x-shkeeper-api-key` header value is not validated — its presence alone is sufficient.
Additionally, the HMAC failure branch only rejects in `production` environments (`!== 'development'`), meaning unset `NODE_ENV` silently ignores invalid signatures.
**Remediation:**
- Remove the heuristic `isShkeeperWebhook` fallback entirely.
- Require `SHKEEPER_WEBHOOK_SECRET` at startup; fail if absent.
- Change environment guard from `!== 'development'` to `=== 'development'` (or always enforce).
---
### H-3 — Request Network `allowTestMode: true` Hardcoded in Production Route
**File:** `src/services/payment/requestNetwork/requestNetworkRoutes.ts:104105` and `signature.ts:5153`
**Status:** Open
```ts
allowTestMode: true,
// in signature.ts:
if (allowTestMode && isTestHeader(headers, testHeader)) {
return true; // skip verification
}
```
Any request bearing `x-request-network-test: 1` (or `true` / `yes`) completely bypasses HMAC signature verification. The flag is unconditional — there is no environment check.
**Remediation:**
```ts
allowTestMode: process.env.NODE_ENV !== 'production',
```
Or introduce an explicit `ENABLE_RN_TEST_MODE=true` env flag absent from production.
---
### H-4 — Typing Indicator Socket Events Have No Chat Membership Check (IDOR)
**File:** `src/app.ts:267291`
**Status:** Open
`typing-start` and `typing-stop` handlers verify only `data.userId === userId` (the caller is who they claim to be) but do not verify the caller is a participant of `data.chatId`. Any authenticated user who knows a chat ID can broadcast typing presence to that room — information disclosure.
**Remediation:** Check membership before broadcasting:
```ts
const chat = await Chat.findById(data.chatId, { participants: 1 }).lean();
const isMember = chat?.participants.some(p => p.userId.equals(userId));
if (!isMember) return;
```
Or maintain a per-socket set of joined room IDs and validate against it without a DB round-trip.
---
### H-5 — Financial Events Broadcast to All Connected Sockets
**File:** `src/services/payment/shkeeper/shkeeperWebhook.ts:546,560`
**Status:** Open
```ts
global.io?.emit('seller-offer-update', { sellerId: ..., paymentId: ..., transactionHash: ... });
```
`io.emit()` sends to every connected socket. Payment completion events including `paymentId`, `transactionHash`, and `offerId` are sent to all users — any client can observe other users' financial events.
**Remediation:**
```ts
global.io?.to(`seller-${selectedOffer.sellerId}`).emit('seller-offer-update', { ... });
global.io?.to(`buyer-${payment.buyerId}`).emit('payment-completed', { ... });
```
---
## MEDIUM
### M-1 — OTP and Reset Codes Logged in Plaintext (All Environments)
**File:** `src/services/auth/authController.ts:174,203,715,757`
**Status:** Open
```ts
console.log(`🔢 Generated verification code for ${email}: ${emailVerificationCode}`);
console.log(`🔢 Generated password reset code for ${email}: ${resetCode}`);
```
6-digit OTPs and password reset codes are written to stdout with no environment guard. Anyone with log access can take over any unverified account or reset any password.
**Remediation:** Delete all four log lines. Never log secrets or codes in any environment.
---
### M-2 — `Math.random()` Used for OTP Generation (Not a CSPRNG)
**File:** `src/services/auth/authService.ts:226`
**Status:** Open
```ts
return Math.floor(100000 + Math.random() * 900000).toString();
```
`Math.random()` is not cryptographically secure. Replace with:
```ts
const bytes = crypto.randomBytes(3);
return (100000 + (bytes.readUIntBE(0, 3) % 900000)).toString();
```
---
### M-3 — Refresh Token Rotation Has No Theft Detection
**File:** `src/services/auth/authController.ts:510529`
**Status:** Open
Token rotation works (old is removed, new is issued). However, if an attacker steals a refresh token and rotates it first, the legitimate user's subsequent refresh gets `403` but the attacker's session continues. There is no "token already rotated" detection that would invalidate all sessions for that user.
**Remediation:** Implement refresh token families. If a token that has already been rotated is presented again, treat it as a theft signal and set `user.refreshTokens = []` (force re-login everywhere).
---
### M-4 — Profile Update Uses `validateBeforeSave: false` with Full Object Spread
**File:** `src/services/auth/authController.ts:921938`
**Status:** Open
```ts
user.profile = { ...user.profile, ...profile };
user.preferences = { ...user.preferences, ...preferences };
await user.save({ validateBeforeSave: false });
```
The entire `profile` and `preferences` objects from the request body are spread without an explicit allowlist. `validateBeforeSave: false` additionally skips schema-level guards. A user could inject unexpected fields into their document.
**Remediation:** Use an explicit pick:
```ts
const allowedProfile = pick(profile, ['phone', 'bio', 'website']);
user.profile = { ...user.profile, ...allowedProfile };
```
Remove `{ validateBeforeSave: false }`.
---
### M-5 — Telegram Login Widget Path Has No Replay Protection
**File:** `src/services/auth/authController.ts:110116`
**Status:** Open
The Mini App flow correctly calls `checkMiniAppReplay` / `rememberMiniAppInitData`. The Login Widget flow (the `else` branch) calls only `verifyTelegramLoginWidget` — no replay check. The Login Widget payload contains a static `hash` valid for up to `miniAppMaxAgeMs` (up to 24 hours). An intercepted payload can be replayed freely within that window.
**Remediation:** Apply the same replay-map (or Redis key) logic to the Login Widget path, keying on `loginWidget.hash`.
---
### M-6 — No JWT Secret Strength Enforcement at Startup
**File:** `src/shared/config/index.ts:42`
**Status:** Open
`JWT_SECRET` is read with `process.env.JWT_SECRET!`. An empty string, `"secret"`, or `"changeme"` is silently accepted, producing trivially forgeable JWTs.
**Remediation:** Add to startup checks:
```ts
if (config.jwtSecret.length < 32) throw new Error('JWT_SECRET must be at least 32 characters');
```
---
### M-7 — Legacy `verifyEmail` Token Route Has No Expiry Check
**File:** `src/services/auth/authController.ts:667692`
**Status:** Open
The code-based flow (`/verify-email-code`) enforces a 15-minute expiry. The legacy URL-based flow (`GET /verify-email/:token`) queries only by token value, with no `emailVerificationTokenExpires` check. If the token field is persisted without an expiry date, it never becomes invalid.
**Remediation:** Either add `emailVerificationTokenExpires: { $gt: new Date() }` to the query, or deprecate and remove this route if the code-based flow has fully replaced it.
---
## LOW
### L-1 — Passkey Challenge Debug Logs Expose All Active Challenges
**File:** `src/services/auth/passkeyService.ts:128130,207212`
**Status:** Open
```ts
console.log('🔍 Available challenges:', Array.from(this.storedChallenges.keys()));
// ...
allUsers.forEach(u => { console.log(`User ${u.email}:`, u.passkeys.map(pk => pk.id)); });
```
On any failed passkey assertion, every registered user's email and all their passkey IDs are dumped to logs. An attacker who triggers many failed assertions can enumerate the passkey corpus from log infrastructure.
**Remediation:** Remove both blocks entirely. Log only the failed assertion's credential ID.
---
### L-2 — In-Memory Login Attempt Counters Not Shared Across Replicas
**File:** `src/services/auth/authService.ts:112`
**Status:** Open
`authAttempts: Map<string, ...>` is process-local. In a multi-replica deployment, an attacker distributing login attempts across replicas bypasses per-user lockout.
**Remediation:** Move to Redis with TTL-expiring keys (same pattern as the rate-limiter Redis adapter that is already planned).
---
### L-3 — CORS Origin: Unset `FRONTEND_URL` Allows All Origins
**File:** `src/app.ts:332`
**Status:** Open
`cors({ origin: process.env.FRONTEND_URL })` — if `FRONTEND_URL` is unset, `cors` treats `undefined` as "allow all origins."
**Remediation:** Add a startup assertion that `FRONTEND_URL` is a non-empty string.
---
### L-4 — Auth Rate-Limit Counters Are In-Memory (Multi-Replica Gap)
**File:** `src/app.ts` (rate-limit middleware configuration)
**Status:** Open
Same class as L-2. The rate-limit counters are in-memory. Distributing requests across replicas bypasses per-IP limits. A Redis store adapter is already planned.
---
## Confirmed PASS (Verified Correctly Handled)
| Check | Result | Source |
|-------|--------|--------|
| HMAC timing-safe comparison (SHKeeper + RN) | PASS | `shkeeperWebhook.ts:84`, `signature.ts:74` |
| Telegram HMAC derivation (Mini App + Login Widget) | PASS | `telegramService.ts:223-278` |
| Bot account rejection | PASS | `telegramService.ts:278,353` |
| Blocked Telegram user check (`status: 'blocked'`) | PASS | `authController.ts:355-363` |
| Refresh token rotation (old removed before new issued) | PASS | `authController.ts:527-529` |
| Password change clears all refresh tokens | PASS | `authController.ts:887` |
| Password reset clears all refresh tokens | PASS | `authController.ts:797,846` |
| Socket.IO JWT enforcement on connect | PASS | `app.ts:76-94` |
| `join-user-room` IDOR prevention | PASS | `app.ts:114` |
| `join-chat-room` membership check | PASS | `app.ts:241-247` |
| bcrypt work factor = 12 | PASS | `authService.ts` |
| WebAuthn challenge consumed on first use | PASS | `passkeyService.ts:87` |
| Telegram `auth_date` freshness enforcement | PASS | `telegramService.ts:208-223` |
---
## Summary
| Severity | Count |
|----------|-------|
| CRITICAL | 6 |
| HIGH | 5 |
| MEDIUM | 7 |
| LOW | 4 |
**Immediate priority:** C-3 (simulation bypass) and C-4 (forceVerify gate) are one-line fixes and are exploitable today. C-1, C-2, C-5 (hardcoded/logged credentials) must be resolved and rotated before any external access to staging or production.
---
## Related
- [[Security Architecture]]
- [[Authentication Flow]]
- [[Webhook Security Spec]]
- [[Threat Model - Amanat Escrow Platform]]
- [[Logic Audit - 2026-05-24]]
- [[Performance Audit - 2026-05-24]]

View File

@@ -0,0 +1,110 @@
---
title: Task 5 Telegram-Native Verification Report
tags: [taskmaster, verification, telegram]
created: 2026-05-24
status: draft
---
# Task 5 Telegram-Native Verification Report
## 1) Deliverable map
| Subtask | Artifact | Location |
|---|---|---|
| 5.1 | Product surface map | `09 - Audits/Task 5.1 Telegram Product Surface and Flow Map.md` |
| 5.2 | Identity linking and session model | `09 - Audits/Task 5.2 Telegram Identity Linking and Session Model.md`, backend Telegram service/model/routes |
| 5.3 | Bot command and notification foundation | `09 - Audits/Task 5.3 Telegram Bot Command and Notification Foundation.md`, backend Telegram webhook skeleton |
| 5.4 | Mini App shell | `09 - Audits/Task 5.4 Telegram Mini App Shell Implementation.md`, frontend `/telegram` route |
| 5.5 | Payment/wallet strategy | `09 - Audits/Task 5.5 Telegram Payment and Wallet Strategy.md` |
| 5.6 | Escrow/delivery/dispute/release action plan | `09 - Audits/Task 5.6 Telegram Escrow Delivery Dispute Release Actions.md` |
| 5.7 | Admin/support operating surface plan | `09 - Audits/Task 5.7 Telegram Admin Support Surface.md` |
| 5.8 | Security/compliance controls | `09 - Audits/Task 5.8 Telegram Security, Compliance, and Abuse Controls.md` |
| 5.9 | QA/rollout/analytics/runbooks | `09 - Audits/Task 5.9 QA Rollout Analytics and Launch Runbooks.md` |
## 2) Documentation verification executed
Commands run while producing this report:
```bash
cd nick-doc && task-master next
cd nick-doc && task-master show 5.1
cd nick-doc && task-master show 5.5
cd nick-doc && task-master show 5.8
cd nick-doc && task-master show 5.9
rg --files nick-doc/.taskmaster/docs
npm run typecheck --silent
npx jest __tests__/telegram-service.test.ts __tests__/telegram-routes.test.ts --runInBand --silent
npx eslint src/services/telegram/telegramService.ts src/services/telegram/telegramRoutes.ts src/models/TelegramLink.ts src/models/TelegramSession.ts __tests__/telegram-service.test.ts __tests__/telegram-routes.test.ts
npx jest __tests__/utils-test/telegram-webapp.test.ts __tests__/sections/telegram/telegram-mini-app-shell.test.tsx --runInBand
npx eslint src/utils/telegram-webapp.ts src/sections/telegram/telegram-mini-app-shell.tsx src/app/telegram/page.tsx
npx tsc --noEmit
```
## 2.1 Code artifacts added
Backend:
- `src/models/TelegramLink.ts`
- `src/models/TelegramSession.ts`
- `src/services/telegram/telegramService.ts`
- `src/services/telegram/telegramRoutes.ts`
- `src/services/telegram/index.ts`
- `/api/telegram` route mount behind feature flag
- `__tests__/telegram-service.test.ts`
- `__tests__/telegram-routes.test.ts`
Frontend:
- `src/app/telegram/page.tsx`
- `src/sections/telegram/telegram-mini-app-shell.tsx`
- `src/sections/telegram/index.ts`
- `src/utils/telegram-webapp.ts`
- `paths.telegram.root`
- `__tests__/utils-test/telegram-webapp.test.ts`
- `__tests__/sections/telegram/telegram-mini-app-shell.test.tsx`
## 3) Constraint checks against Task 4 docs
- Canonical ledger and escrow states as authority: verified and referenced in all 4 Task 5 documents.
- Provider adapters kept optional in the first phase: explicitly stated in Task 5.5.
- Dispute hold and authorization gates preserved for Telegram-initiated financial transitions: repeated across all documents with links to:
- `Funds Ledger and Escrow State Machine Specification`
- `Payment Provider Adapter Spec`
- `Webhook Security Spec`
- `Security Ownership and Launch Decision Criteria`
## 3.1 Test results
Backend:
- `npm run typecheck --silent` passed.
- `npx jest __tests__/telegram-service.test.ts __tests__/telegram-routes.test.ts --runInBand --silent` passed: 2 suites, 12 tests.
- Scoped backend ESLint passed on Telegram service/model/route/test files.
Frontend:
- `npx jest __tests__/utils-test/telegram-webapp.test.ts __tests__/sections/telegram/telegram-mini-app-shell.test.tsx --runInBand` passed: 2 suites, 6 tests.
- Scoped frontend ESLint passed on Telegram utility/route/component files.
- `npx tsc --noEmit` is blocked by two pre-existing non-Telegram errors:
- `src/components/payment/shkeeper-payment-widget.tsx` missing `sellerId` in an `IPaymentIntentPayload`.
- `src/web3/components/provider-payment.tsx` uses unregistered `solar:wallet-money-bold` icon.
## 4) Remaining gaps and open questions
1. Bot command actions are still a skeleton: updates are classified and deduped,
but commands/callbacks do not yet execute product workflows.
2. Outbound Telegram notifications, preferences, blocked-bot handling, retry
metrics, and quiet mode are not implemented.
3. Mini App shell is present, but request/offer/payment/dispute data loading and
action execution are still pending.
4. Deep-link token schema still needs final claim set and signing key rotation schedule.
5. Analytics schema and dashboard ownership for Telegram events remains to be implemented in code and monitoring tooling.
6. Runbook automation checks for replay checks and payment mismatch drilldown are not yet scripted in operations tooling.
## 5) Recommendation
This pass completes the Task 5 foundation for product mapping, Telegram identity
linking/session verification, Mini App shell, payment/wallet strategy, security
controls, and launch QA documentation. Keep Task 5 open until bot actions,
notifications, real Mini App marketplace workflows, Telegram escrow actions, and
admin/support surfaces are implemented and verified.

View File

@@ -0,0 +1,122 @@
---
title: Task 5.1 Telegram Product Surface and Flow Map
tags: [taskmaster, telegram, product, flow-map, canonical-state]
created: 2026-05-24
status: draft
---
# Task 5.1 Telegram Product Surface and Flow Map
Source: `/.taskmaster/docs/prd-telegram-native-app-bot-wallet.md`
This map defines the first-release Telegram-native surface for Task 5.1 and keeps the financial core bound to the canonical
[`Funds Ledger and Escrow State Machine Specification`](Funds%20Ledger%20and%20Escrow%20State%20Machine%20Specification.md) and
Task 4 authorization/risk controls.
## 1) Channel split (first release)
| Area | Bot messages | Mini App | Web/Admin-only (first release) |
|---|---|---|---|
| Discovery & onboarding | `/start`, `/help`, `/link`, `/settings` | Onboarding cards, link prompts, fallback CTA | Full self-serve onboarding docs |
| Request browse/create | summary list + quick actions only | Full in-app list, create/edit request form | Full advanced catalog/search UI |
| Offers | receive offer summary, accept/reject shortcuts | full offer list and detail, offer accept/reject | Seller negotiation detail with admin context |
| Payment start | payment shortcut button + deep link payload | payment state UI + provider launch from in-app | Payment method setup, advanced failure handling |
| Delivery evidence | status update nudges + evidence deep link | upload evidence from in-app flow | Full media manager + admin-side attachments |
| Dispute | dispute open/summary + status check | open/respond/track dispute in app | Admin moderation, manual evidence review |
| Release/Refund | only status blockers and support handoff | confirmation request + release/refund request | Execute actions through canonical payment API |
| Notification settings | toggle on/off, blocked-state view | mirror of notification status and quiet mode | Full support/notification audit UI |
| Support | support contact + report IDs | support request and context handoff | Admin/operator console and action audit |
## 2) Canonical state mapping (all actions)
All Telegram actions must map to existing canonical states and API-side checks:
- `PurchaseRequest.status`: `pending``received_offers``in_negotiation``payment``processing``delivery``delivered``confirming``completed` / `cancelled`
- `Payment.escrowState`: `FUNDED` / `PARTIALLY_FUNDED` / `RELEASABLE` / `DISPUTED` / `RELEASING` / `RELEASED` / `REFUNDING` / `REFUNDED` / `FAILED` / `CANCELLED`
- `Dispute.status`: `OPEN` / `UNDER_REVIEW` / `RESOLVED_*` / `CLOSED` as appropriate
The only transitions that may modify funds, release eligibility, or dispute holds are those enforced by the canonical service
layer from the
[`Funds Ledger and Escrow State Machine Specification`](Funds%20Ledger%20and%20Escrow%20State%20Machine%20Specification.md).
## 3) Journey mapping by persona
### 3.1 Unauthenticated user
- See bot start/help and public support command.
- Any authenticated action routes to account-link flow.
- No request write actions without auth link.
### 3.2 Unlinked Telegram user
1. Start in bot or Mini App.
2. Prompt account linking (`/link` equivalent) using signed callback token.
3. Complete account link or return to web flow.
4. After link success, deep link returns user to prior action context (request, offer, payment, or dispute).
### 3.3 Linked buyer
- Browse/create requests, send offer review decision, start payment, upload delivery evidence, open disputes, monitor request state, request release/refund.
- No direct fund release path: Telegram actions read state and submit intent only.
### 3.4 Linked seller
- Receive offer and request notifications, update offer availability, review buyer disputes, upload shipping/proof artifacts.
- No direct payout mutation from Telegram; actions remain canonical requests to existing payment/dispute endpoints.
### 3.5 Admin/support
- Dedicated support context in admin view only (first release).
- Telegram-origin IDs and linked wallet/payment provenance shown in existing admin interfaces.
- Any manual override for Telegram-originated cases follows the same step-up/two-person policy as web flows when configured in
[`Security Ownership and Launch Decision Criteria`](Security%20Ownership%20and%20Launch%20Decision%20Criteria.md).
## 4) Deep-link and context contract
Deep-link entry points in order of priority:
1. `request` detail
2. `offer-review` (accept/reject)
3. `payment-start`
4. `dispute-open` / `dispute-track`
5. `delivery-evidence`
6. `account-link`
Rules:
- No raw IDs in clear URLs when not needed.
- Use short opaque or signed context IDs.
- Expire context tokens and bind to Telegram user + wallet/session.
- Every context hit verifies canonical ownership/state before rendering any action.
### Example startapp routes (illustrative)
- `.../startapp=request&rid=<opaque>`
- `.../startapp=offer&oid=<opaque>`
- `.../startapp=payment&pid=<opaque>`
- `.../startapp=dispute&did=<opaque>`
- `.../startapp=verify`
## 5) Release phasing
| Phase | Telegram surface | Backend dependency |
|---|---|---|
| Phase 0 (MVP) | bot + limited Mini App for state-safe buyer/seller actions; no Telegram-only financial mutations | Payment and ledger gates already enforced |
| Phase 1 | richer navigation, richer filters, deeper support surfaces | Identity linking + event fanout |
| Phase 2 | admin operator actions in Telegram | Additional ops hardening and audit scope |
## 6) Acceptance traceability
Directly mapped to PRD section "Task 1":
- Buyer/seller/admin/support journeys and entry contexts documented: **yes**
- Deep links and entry points defined with phase split: **yes**
- Canonical API/state mapping in table + journey sections: **yes**
- Non-native and admin surfaces identified: **yes**
- Backward compatibility with existing web flows retained: **yes**
## 7) Open items for implementation
1. Finalize concrete deep-link payload schema (opaque token claim set).
2. Finalize Mini App navigation IDs and route-to-state matrix in frontend routing.
3. Confirm unsupported actions for `DISPUTED`/`FAILED` states at Telegram action layer.

View File

@@ -0,0 +1,127 @@
---
title: Task 5.10 Telegram First-Class Authentication
tags: [audit, taskmaster, telegram, auth, testing]
---
# Task 5.10 Telegram First-Class Authentication
Date: 2026-05-24
## Scope
Task 5.10 implemented Telegram as a first-class Amanat auth provider.
- Backend now exposes `POST /api/auth/telegram`.
- The endpoint accepts Telegram Mini App `initData` or Telegram Login Widget payloads.
- Telegram signatures are verified server-side.
- Telegram-only users can be created without email/password.
- Standard Amanat JWT and refresh tokens are returned.
- Frontend Mini App sessions auto-authenticate from raw signed `initData`.
- The web login view includes a Telegram continuation button.
## Backend Implementation
Changed project: `backend`
- `src/models/User.ts`
- `email` is now optional with a sparse unique index.
- Added `authProvider: "email" | "google" | "telegram"`.
- Added `telegramVerified`.
- `src/models/TelegramLink.ts`
- Added `login_widget` as a supported source.
- `src/services/telegram/telegramService.ts`
- Added Telegram Login Widget verification.
- Added normalized Telegram auth identity helpers.
- `src/services/auth/authController.ts`
- Added `telegramAuth`.
- Auto-provisions Telegram users with nullable email.
- Rejects replayed Mini App data, stale auth dates, invalid signatures, bot accounts, blocked links, and inactive users.
- `src/services/auth/authRoutes.ts`
- Added public `POST /telegram`, mounted externally as `/api/auth/telegram`.
## Frontend Implementation
Changed project: `frontend`
- `src/lib/axios.ts`
- Added `/auth/telegram` as a public endpoint.
- Added `endpoints.auth.telegram`.
- `src/auth/context/jwt/action.ts`
- Added `signInWithTelegram`.
- Stores returned access/refresh tokens using the existing auth storage path.
- `src/sections/telegram/telegram-mini-app-shell.tsx`
- Auto-authenticates Mini App sessions once from raw `initData`.
- Shows an onboarding dialog for newly provisioned Telegram accounts.
- Keeps standard sign-in/dashboard fallbacks.
- `src/auth/components/form-socials.tsx`
- Added Telegram icon button.
- `src/auth/view/jwt/jwt-sign-in-view.tsx`
- Added Telegram sign-in handler for both Mini App `initData` and browser Login Widget payloads.
## Security Notes
- Frontend never trusts `initDataUnsafe` for auth; it only sends raw signed `initData`.
- Backend validates Telegram HMAC signatures using the configured bot token.
- Mini App `auth_date` freshness and replay window are enforced before JWT issuance.
- Login Widget `auth_date` freshness is enforced.
- Telegram bot accounts are rejected.
- Blocked `TelegramLink` records are rejected with 403.
- Telegram phone numbers are neither requested nor persisted.
- High-risk escrow and wallet actions still use the existing protected API and step-up rules.
## Verification
Backend:
```bash
npm run typecheck
npx jest __tests__/telegram-auth.test.ts __tests__/telegram-service.test.ts --runInBand --forceExit
```
Result:
- Backend typecheck passed.
- `__tests__/telegram-auth.test.ts` passed.
- `__tests__/telegram-service.test.ts` passed.
- 14 targeted backend tests passed.
Frontend:
```bash
npm test -- __tests__/auth/telegram-auth-action.test.ts __tests__/sections/telegram/telegram-mini-app-shell.test.tsx --runInBand
npx tsc --noEmit -p tsconfig.json
```
Result:
- Targeted frontend Jest tests passed.
- 7 targeted frontend tests passed.
- Full frontend typecheck still reports unrelated pre-existing errors in payment UI code outside Task 5.10:
- `src/components/payment/shkeeper-payment-widget.tsx`
- `src/web3/components/provider-payment.tsx`
## Acceptance Coverage
| Requirement | Status |
| --- | --- |
| New Telegram user auto-provisions and receives JWT | Covered by backend test |
| Returning user authenticates via Mini App initData | Covered through same route and link lookup |
| Returning user authenticates via Login Widget | Covered by backend test |
| Replayed Mini App initData rejected | Covered by backend test |
| Stale `auth_date` rejected | Covered by backend test |
| Blocked Telegram account returns 403 | Covered by backend test |
| Existing email/password users unaffected | Covered by backend test |
| Email optional for Telegram users | Covered by backend test and sparse user model |
| `isNewUser` triggers onboarding overlay | Covered by frontend shell behavior |
| High-risk actions retain step-up requirements | No high-risk action code changed |
## Operational Follow-up
If a deployed MongoDB already has an old non-sparse unique `email` index, replace it with the sparse unique index before creating multiple Telegram-only users:
```js
db.users.dropIndex("email_1")
db.users.createIndex({ email: 1 }, { unique: true, sparse: true })
```
Run this only after confirming the existing index name in the target database.

View File

@@ -0,0 +1,62 @@
---
title: Task 5.2 Telegram Identity Linking and Session Model
tags: [taskmaster, telegram, identity, session]
created: 2026-05-24
status: implemented-foundation
---
# Task 5.2 Telegram Identity Linking and Session Model
This document captures the first backend implementation pass for Task 5.2.
## Implemented foundation
- `TelegramLink` model maps one active Telegram user ID to one Amanat user.
- `TelegramSession` model stores Mini App session tokens, Telegram user ID,
optional Amanat user ID, initData fingerprint, auth date, source, expiry, and
active state.
- `/api/telegram/miniapp/verify` validates Telegram Mini App `initData` without
creating an Amanat session.
- `/api/telegram/miniapp/session` creates a Telegram session after verified
`initData`.
- `/api/telegram/link` lets an authenticated Amanat user link, read, or unlink a
Telegram account.
## Security model
- Backend verifies Mini App `initData` with Telegram's server-side signature
scheme before trusting Telegram identity.
- `initDataUnsafe` remains client-only display context and is not trusted for
backend authorization.
- Bot accounts are rejected.
- Stale `auth_date` values are rejected by configured TTL.
- Replayed Mini App payloads are rejected inside the configured replay window.
- Duplicate active Telegram-to-Amanat links are rejected.
## Configuration
| Variable | Purpose |
|---|---|
| `TELEGRAM_FEATURE_ENABLED` | Master feature flag |
| `TELEGRAM_MINIAPP_ENABLED` | Mini App session/linking flag |
| `TELEGRAM_BOT_TOKEN` | Server-side token used to verify Mini App signatures |
| `TELEGRAM_INITDATA_MAX_AGE_SEC` | Maximum accepted `auth_date` age |
| `TELEGRAM_INITDATA_REPLAY_WINDOW_MS` | Replay window for duplicate Mini App submissions |
| `TELEGRAM_SESSION_TTL_SEC` | Telegram session lifetime |
## Tests
- `backend/__tests__/telegram-service.test.ts`
- `backend/__tests__/telegram-routes.test.ts`
Coverage includes valid/invalid signature, expired initData, replay detection,
session persistence, link/unlink behavior, duplicate link rejection, route-level
Mini App validation, and secret non-leakage.
## Remaining work
- Move replay protection from in-memory maps to Redis or another shared store for
multi-instance deployments.
- Bind Telegram session tokens to first-party web session controls once the
final session architecture is implemented.
- Add admin/support controls for revoking or blocking Telegram links.

View File

@@ -0,0 +1,48 @@
---
title: Task 5.3 Telegram Bot Command and Notification Foundation
tags: [taskmaster, telegram, bot, notifications]
created: 2026-05-24
status: partial-foundation
---
# Task 5.3 Telegram Bot Command and Notification Foundation
This document captures the first backend bot foundation pass.
## Implemented foundation
- `/api/telegram/status` reports feature and webhook readiness without leaking
bot or webhook secrets.
- `/api/telegram/webhook` is mounted only when Telegram features are enabled.
- Webhook requests require `x-telegram-bot-api-secret-token` to match
`TELEGRAM_WEBHOOK_SECRET_TOKEN`.
- Webhook update handling dedupes by `update_id` or callback ID.
- Webhook handler classifies updates as:
- `command`
- `callback`
- `noop`
- `duplicate`
## Configuration
| Variable | Purpose |
|---|---|
| `TELEGRAM_WEBHOOK_ENABLED` | Enables webhook route |
| `TELEGRAM_WEBHOOK_SECRET_TOKEN` | Telegram webhook secret token |
| `TELEGRAM_WEBHOOK_REPLAY_WINDOW_MS` | Duplicate update replay window |
## Tests
- Service-level tests cover command/callback/noop classification and duplicate
handling.
- Route-level tests cover secret-token rejection and accepted command webhook
processing.
## Remaining work
- Implement actual command actions for `/start`, `/help`, `/link`, `/status`,
`/request`, `/offer`, `/payment`, `/dispute`, and `/settings`.
- Add signed opaque callback payload issuance and resolution.
- Add outbound notification delivery, blocked-bot detection, retry metrics, and
notification preferences.
- Persist webhook dedupe keys in shared storage instead of process memory.

View File

@@ -0,0 +1,42 @@
---
title: Task 5.4 Telegram Mini App Shell Implementation
tags: [taskmaster, telegram, mini-app, frontend]
created: 2026-05-24
status: partial-foundation
---
# Task 5.4 Telegram Mini App Shell Implementation
This document captures the first frontend implementation pass for the Telegram
Mini App surface.
## Implemented foundation
- Added `/telegram` route.
- Added `src/utils/telegram-webapp.ts` for safe Telegram WebApp context parsing.
- Added `TelegramMiniAppShell` with three first-pass states:
- unsupported browser context,
- Telegram detected but unlinked,
- Telegram detected and authenticated web user present.
- Added basic marketplace/escrow action cards for requests, request creation,
payment/escrow, chat, and account.
- Added Telegram chrome attachment helper for MainButton, BackButton, and haptic
feedback when available.
- Added safe-area and Telegram theme helpers.
## Tests
- `frontend/__tests__/utils-test/telegram-webapp.test.ts`
- `frontend/__tests__/sections/telegram/telegram-mini-app-shell.test.tsx`
Coverage includes standard browser fallback, Telegram context parsing,
safe-area/theme fallback, start parameter parsing, unsupported state, unlinked
state, and linked state rendering.
## Remaining work
- Wire Mini App shell to backend Telegram session endpoints.
- Add request/offer/payment/dispute data loading inside the Mini App route.
- Add upload and dispute action surfaces that call canonical backend APIs.
- Add Telegram BackButton routing behavior once final navigation stack is known.
- Add client matrix QA across iOS, Android, Desktop, and Web Telegram clients.

View File

@@ -0,0 +1,105 @@
---
title: Task 5.5 Telegram Payment and Wallet Strategy
tags: [taskmaster, telegram, payment, wallet, provider-adapter]
created: 2026-05-24
status: draft
---
# Task 5.5 Telegram Payment and Wallet Strategy
Source: `/.taskmaster/docs/prd-telegram-native-app-bot-wallet.md`
## 1) Strategy principle for this track
For first release, Telegram-native payment initiation must feed the existing canonical payment system and must **not** bypass
ledger-led state transitions. This means:
- payment proof and funds movement remain authorized by
[`Funds Ledger and Escrow State Machine Specification`](Funds%20Ledger%20and%20Escrow%20State%20Machine%20Specification.md),
- release/refund actions remain governed by service-layer guards and dispute holds,
- Telegram provider adapters are optional extensions, not a hard dependency.
In practice: Telegram can open payment flows through one canonical path first, and optional providers are layered later.
## 2) Option comparison
| Option | Fit for Telegram UX | Funds correctness risk | Regulatory / abuse risk | Complexity | Decision |
|---|---|---|---|---|---|
| Bot API Payments / Telegram Stars | Moderate | High (revenue and settlement mapping differs from existing ledger assumptions) | Medium | Medium | Deferred |
| Wallet Pay | Moderate | Medium-High (token-specific proof path) | High | High | Deferred |
| TON Pay | Medium | Medium-High (provider proof consistency + chain mapping) | High | High | Deferred |
| TON Connect + direct wallet pay | Medium | Medium if proven; requires strict recipient+amount+memo checks | High | High | Deferred |
| Request Network links | High (already introduced) | Low-Medium (existing adapter/service contract) | Medium | Low | **Primary for phase 1** |
| Existing Web3/crypto checkout path | High | Medium (requires recipient/amount/network proof hardening) | High | Medium | Keep as fallback only |
## 3) Selected phase-1 path
### Primary phase-1 strategy
- Telegram bot/Mini App starts the payment by invoking the same canonical payment intent creation API used for web flows.
- Payment provider is written as metadata; provider-neutral adapter remains optional and feature-flagged.
- Telegram identity metadata is persisted with the intent:
- `provider` (e.g., `request_network` or `shkeeper`)
- `telegramUserId`
- `telegramChatId` (if available)
- `launchSource` (`bot`, `mini_app`, `web_fallback`)
- `paymentReference`
- `invoiceId` / `orderId` / `requestId`
- `currency`, `amount`, `expiresAt`
- idempotency key (request-scoped)
### Optional providers
- Wallet Pay / TON Pay / TON Connect paths are documented as optional and must be implemented behind explicit
feature flags and dedicated adapter configuration only when:
- replay-safe proofs are standardized,
- canonical refund/release reconciliation supports provider-native evidence,
- monitoring/reporting has parity with existing paths.
- Until that date, these options are not required to merge for Task 5.1-5.9 handoff.
## 4) Data integrity requirements for any Telegram payment/Wallet path
Before any pay-in is accepted into escrow, enforce:
1. recipient validation (exact canonical recipient for the active escrow policy),
2. asset validation (supported chain + token),
3. amount validation against payment intent and currency precision,
4. memo/reference validation (idempotent and tamper-evident),
5. confirmation proof validation (`confirmed`, `on-chain`, or provider webhook),
6. canonical reconciliation step before mark-complete,
7. invariant re-check with ledger balances before release/refund operations.
These map directly to:
- [`Webhook Security Spec`](Webhook%20Security%20Spec.md)
- [`Payment Provider Adapter Spec`](Payment%20Provider%20Adapter%20Spec.md) (if optional provider is enabled)
- [`Funds Ledger and Escrow State Machine Specification`](Funds%20Ledger%20and%20Escrow%20State%20Machine%20Specification.md) (all financial state transitions).
## 5) Anti-divergence record model
Every Telegram payment intent should write a small canonical trace record with:
- canonical `paymentId`
- `canonicalSourceChannel = telegram`
- `telegramContext` payload hash
- `provider` and `providerReference`
- `idempotencyKey`
- `createdFrom` deep-link/command/callback identifier
- reconciliation status (`created`, `started`, `confirmed`, `failed`, `reconciled`)
This avoids duplicate checkout creation and makes Telegram callback replay and callback mismatch explainable in operations.
## 6) Refund/release compatibility
Refund/release logic remains unchanged from the canonical engine:
- cannot release funds not in releasable state,
- no release/release-amount mutation from Telegram without canonical precondition checks,
- disputed states block direct payout operations,
- release/refund events are only reflected after ledger updates succeed.
## 7) Open follow-up decisions
1. Decide whether to onboard `Wallet Pay` as phase-2 provider once adapter-level proof validation and reconciliation parity are met.
2. Decide whether to expose optional "wallet direct top-up" button in Mini App from day one or keep explicit link-first flow.
3. Finalize provider timeout and retry policy for Telegram-triggered intents under slow-network conditions.

View File

@@ -0,0 +1,38 @@
---
title: Task 5.6 Telegram Escrow Delivery Dispute Release Actions
tags: [taskmaster, telegram, escrow, disputes, release]
created: 2026-05-24
status: planned
---
# Task 5.6 Telegram Escrow Delivery Dispute Release Actions
Task 5.6 is not complete in this first Task 5 pass. This document defines the
implementation boundary required before Telegram shortcuts can affect escrow
state.
## Required behavior
- Telegram users can view current escrow state and next allowed actions.
- Delivery confirmation, evidence upload, refund request, dispute open/respond,
and release approval route through existing backend precondition checks.
- High-risk actions require fresh confirmation and audit logging with Telegram
context.
- Disputed or held funds cannot be released through Telegram shortcuts.
## Required backend constraints
- Use canonical purchase request, payment, dispute, and ledger state.
- Reject release/refund actions unless the funds state machine says the action is
allowed.
- Apply the same step-up and two-person policy as web/admin flows.
- Record Telegram user ID, chat/update ID, deep-link source, and callback token
ID in audit metadata.
## Required tests
- Buyer cannot confirm delivery before delivery state.
- Disputed funds cannot be released.
- Replayed Telegram callback cannot create a second action.
- Stale callback token is rejected.
- Telegram release/refund action emits the same audit fields as web release.

View File

@@ -0,0 +1,40 @@
---
title: Task 5.7 Telegram Admin Support Surface
tags: [taskmaster, telegram, admin, support]
created: 2026-05-24
status: planned
---
# Task 5.7 Telegram Admin Support Surface
Task 5.7 is not complete in this first Task 5 pass. This document defines the
admin/support scope required for Telegram-originated cases.
## Required admin/support visibility
- Telegram linked identity on user profile.
- Bot notification status and blocked-bot state.
- Mini App launch source and latest Telegram session metadata.
- Payment provider and wallet/payment references for Telegram-originated intents.
- Telegram webhook/callback event history for support investigation.
## Required admin/support actions
- Resend link prompt.
- Revoke Telegram link.
- Block Telegram bot access for a user.
- Inspect Telegram-originated event history.
- Escalate payment/dispute issues to canonical admin workflows.
## Security requirements
- Admin overrides remain gated by Task 4 step-up/two-person controls.
- Support can inspect Telegram context but cannot mutate funds state.
- Every support/admin action writes structured audit metadata.
## Required tests
- Support can read Telegram link metadata but cannot release/refund funds.
- Admin link revocation invalidates active Telegram link.
- Blocked Telegram user cannot create a new Mini App session.
- Admin override paths still require step-up when configured.

View File

@@ -0,0 +1,138 @@
---
title: Task 5.8 Telegram Security, Compliance, and Abuse Controls
tags: [taskmaster, telegram, security, compliance, abuse]
created: 2026-05-24
status: draft
---
# Task 5.8 Telegram Security, Compliance, and Abuse Controls
Source: `/.taskmaster/docs/prd-telegram-native-app-bot-wallet.md`
## 1) Threat model alignment
Threats are prioritized by impact to funds and identity:
- forged `initData`
- callback/replay abuse
- deep-link tampering
- phishing / link substitution
- bot token leakage
- spam / command flood
- account takeover
- wallet spoofing
- fake payment proof
- support impersonation
These correspond to existing concerns in
[`Threat Model - Amanat Escrow Platform`](Threat%20Model%20-%20Amanat%20Escrow%20Platform.md), especially **T16** (Telegram deep-link tampering).
## 2) Channel-specific controls
### 2.1 Telegram Web App identity
- Validate every Mini App session using Telegram `initData` with server-side signature verification.
- Never trust raw Telegram identifiers from client payload.
- Reject `initData` outside active bot context.
- Enforce short TTL and replay window for session and deep-link context tokens.
### 2.2 Bot callback and command surface
- Signed opaque callback payloads only; no mutable business state in query strings.
- Update callback handling is idempotent with dedupe key `(callbackId, telegramUpdateId, telegramUserId)`.
- Unknown callback payloads are rejected with audit logging and zero state mutation.
- Hard rate limits on `/api/telegram/webhook` per IP and per Telegram user.
### 2.3 Deep-link and redirect controls
- Signed link payloads with short expiry and bound context.
- Reject tampered/deprecated deep links by schema/nonce/version checks.
- No direct redirect to non-whitelisted hosts.
- Re-validate request ownership after deep-link state resolution.
### 2.4 Wallet and payment evidence controls
- Recipient, token, amount, and network checks must run before trust decisions.
- Payment proof replay rejected when payload hash differs from signed first-seen proof.
- Require provider-normalized reconciliation confirmation before marking funds complete.
### 2.5 Bot token and secrets
Secrets must never be in client-side code and must not be logged:
- `TELEGRAM_BOT_TOKEN`
- `TELEGRAM_WEBHOOK_SECRET`
- `TELEGRAM_WALLET_PAY_TOKEN` (if Wallet Pay enabled)
- `TON_CONNECT_MANIFEST_URL` / TON secret artifacts
- provider API secrets and webhook keys for request-network/SHKeeper paths
Apply Task 4 secret principles:
- separate sandbox and production tokens/environments,
- short rotation cadence,
- strict allowlist for secret-bearing hosts.
## 3) Compliance and secure configuration
### 3.1 CORS / CSP / framing
- Restrict Telegram endpoints to required app origins only.
- CSP in Telegram surfaces should avoid wildcard connect/script allowances.
- Add explicit `frame-ancestors` constraints for web entry surfaces that can be embedded.
### 3.2 Access and authorization
- Apply existing ownership rules from
[`Authorization Matrix - REST and Socket.IO`](Authorization%20Matrix%20-%20REST%20and%20Socket.IO.md).
- Telegram-originated actions use the same actor checks as web actions, including admin step-up and two-person control for high-value override actions.
- Dispute hold checks remain blocking for release/refund.
## 4) Abuse controls
1. **Spam / abuse**
- per-user command/chat throttle,
- per-IP flood detection on webhook.
2. **Account takeover**
- suspicious Telegram session behavior triggers notification disable and forced re-link.
3. **Wallet spoofing**
- signed wallet ownership checks + reconciliation proof checks.
4. **Fake proof**
- malformed proofs logged; never auto-complete payment with unverifiable proof.
5. **Support impersonation**
- support tools require signed admin context, with explicit role and actor logs.
## 5) Monitoring and alerting
Signals (and baseline targets):
- `telegram_update_failures_total` (high if sustained >0.5%)
- `telegram_initdata_invalid_total`
- `telegram_callback_replay_total`
- `telegram_payment_proof_mismatch_total`
- `telegram_notification_blocked_total`
- `telegram_payment_abnormal_status_jump_total`
Alert rules (initial):
- warning after 10 minutes, critical after 30 minutes or sustained spike above baseline.
- any sudden increase in payment mismatch ratio requires incident triage.
Where to emit:
- Sentry for exception/error trending,
- backend logs for structured events,
- existing dashboard metrics hooks and runbook scripts where available.
## 6) Operational controls required before launch
- Secrets rotation checklist completed (bot token and payment/provider keys).
- CORS/CSP reviewed and restricted to Telegram and known frontend origin.
- webhook secrets configured separately for sandbox and production.
- callback replay suppression validated in QA scenarios.
- support impersonation checks and audit trails validated.
## 7) Outstanding controls not yet implemented in this task layer
- Two-way admin alerting channel for Telegram-specific abuse currently depends on operations integration.
- Formal cryptographic replay store for deep-link one-time tokens (recommended before general rollout).
- Telegram payment-specific policy page and end-user risk consent copy.

View File

@@ -0,0 +1,156 @@
---
title: Task 5.9 QA, Rollout, Analytics, and Launch Runbooks
tags: [taskmaster, telegram, qa, rollout, analytics, runbook]
created: 2026-05-24
status: draft
---
# Task 5.9 QA, Rollout, Analytics, and Launch Runbooks
Source: `/.taskmaster/docs/prd-telegram-native-app-bot-wallet.md`
## 1) QA scope for launch readiness
### 1.1 Client matrix (required)
- Telegram iOS
- Telegram Android
- Telegram Desktop
- Telegram Web
- Light / dark themes
- Compact / fullscreen modes
- Normal and slow network
- Blocked bot scenario
- Expired / stale session scenario
- Payment cancellation and abort
- Unlinked user and re-link path
### 1.2 Functional QA checklist
1. Identity and linking
2. Request listing/detail in both bot and Mini App
3. Offer review flow
4. Payment initiation and cancel path
5. Delivery evidence upload
6. Dispute open/respond and status progression
7. Notification quiet/error state
8. Error and blocked-bot behavior
9. Support escalation handoff
### 1.3 Security/abuse QA
- forged/invalid `initData` rejection
- callback replay replayed twice: one success one no-op
- deep-link tampering
- wallet proof mismatch
- callback processing under invalid provider secrets
- admin override behavior and audit event capture
## 2) Environments and rollout
### 2.1 Environment separation
- `telegram-dev-bot` and `telegram-prod-bot` tokens and webhook endpoints must be distinct.
- No shared webhook secret between environments.
- QA and production payment fixtures remain isolated.
### 2.2 Feature flag sequence
1. **Development flag off**: no surface exposed
2. **Internal allowlist**: selected users only (buyer/seller/admin)
3. **Beta cohort**: controlled percentage and fixed org list
4. **Production enablement**: after runbook and KPI thresholds pass
### 2.3 Deployment safety
- If new surface increases payment mismatch or callback failure, immediately pause `TELEGRAM_SURFACE_ENABLED` and keep providers in read-only mode.
- Use existing rollback flow from incident operations and deployment runbooks.
## 3) Analytics and launch KPIs
Track these metrics daily for 14 days after stage advancement:
- activation rate (`activatedTelegramUsers / startedTelegramUsers`)
- link completion rate (`linkedUsers / startedLink`)
- request creation from Telegram (`telegramRequestsCreated`)
- offer response completion (`offerResponses / offersOpened`)
- payment started / payment completed (`telegramPaymentStart`, `telegramPaymentComplete`, `telegramPaymentFail`)
- dispute activity (`disputesOpened`, `disputesResolvedInTelegram`)
- release approvals from Telegram context (`telegramReleaseApprovals`)
- notification opt-outs (`notificationsOptOutRate`)
- callback duplicate ratio (`callbackReplay / callbackTotal`)
- average context resume latency (min and p95)
### Reporting destinations
- Sentry for exception and failure spikes
- application logs for workflow events
- existing monitoring dashboards for rate/latency anomalies
## 4) Launch runbooks
All runbooks are mandatory for Stage-1 rollout and post-launch incidents.
### 4.1 Bot outage
1. Validate webhook endpoint response health.
2. Switch status to notification-only mode where possible.
3. Confirm bot token and webhook URL.
4. Re-route urgent flows to web fallback.
5. Restore Telegram webhook + replay backlog after recovery.
### 4.2 Telegram API outage
1. Confirm external Telegram API status.
2. Temporarily disable deep-link / in-app actions that require Telegram callbacks.
3. Notify users of delayed updates.
4. Keep pending payment states in read-only mode until callback channel is restored.
### 4.3 Payment provider outage
1. Identify affected provider via provider mode and provider health flags.
2. Switch to read-only or alternative provider mode where configured.
3. Run reconciliation before re-enabling full writes.
4. Track stale pending payment age and contact support workflow.
### 4.4 Stuck payment
1. Check payment reconciliation queue and provider status.
2. Verify callback proof and on-chain confirmation.
3. Manually reconcile if allowed by protocol and policy.
4. Escalate if stale > 24h in funded or processing state.
### 4.5 Duplicate callback
1. Validate idempotency path executed correctly.
2. Confirm callback dedupe key retention window.
3. Compare event fingerprint for payload divergence.
4. Mark one path as duplicate no-op and keep audit trail.
### 4.6 Suspicious wallet proof
1. Block automated release/refund for the request.
2. Flag payment and mark for manual ops review.
3. Verify recipient, amount, and tx hash against chain/provider data.
4. Resume only after explicit approval.
### 4.7 Compromised bot token
1. Rotate bot token immediately.
2. Disable bot endpoints and clear webhook secret for 1 hour.
3. Validate callback signatures with new secret.
4. Resume in staged rollout mode with monitoring for 24h.
## 5) Stage exit criteria
- All required QA scenarios pass on iOS/Android/desktop/web.
- No critical webhook/payload mismatch regressions in 24h observation window.
- No unresolved payment stuck items > 24h after manual triage.
- Incident owners can execute all seven runbooks.
- Rollout metrics show non-degrading trend for the first two days.
## 6) Known rollout gaps
1. Fine-grained feature toggles for Telegram in existing observability dashboards are pending.
2. Admin analytics for Telegram-originated releases are schema-dependent and need implementation wiring.
3. Deep-link recovery behavior after prolonged Telegram link expiry still needs UX polishing.

View File

@@ -0,0 +1,67 @@
# PRD: Telegram Phone Number Authentication
> Source spec: `.taskmaster/docs/prd-telegram-phone-auth.md`
> Related task: **Task 5.10** — Implement Telegram as first-class authentication provider
---
## Problem
The current Telegram integration is a *secondary linking layer*: users must already have an Amanat email/password or Google account before they can connect Telegram. This breaks the natural Telegram Mini App user journey — someone who opens the Mini App from Telegram has already proven phone ownership to Telegram and reasonably expects to be authenticated with zero extra signup friction.
## Core idea
Telegram accounts are phone-number-verified. When the backend verifies `initData` from the Mini App (already implemented), it has cryptographic proof that the requester controls a specific Telegram identity. That proof is sufficient to create and sign in to an Amanat account — no email, no password required.
The same principle applies to the **Telegram Login Widget**, the standard web-based flow that lets users authenticate on any browser page by confirming from their Telegram app.
## Two auth paths covered
| Context | Mechanism | Already have? |
|---|---|---|
| Inside Telegram Mini App | `initData` HMAC-SHA-256 (already verified server-side) | Partial — used for linking only |
| Any browser (web login page) | Telegram Login Widget → signed callback | Missing |
Both paths converge at a new `POST /auth/telegram` endpoint that: verifies the signature → looks up or creates the Amanat user → issues a JWT session.
## New user provisioning
When a Telegram user arrives with no existing account:
- Account is auto-created (`authProvider: telegram`, `telegramVerified: true`)
- No email required — email column becomes nullable (`sparse` unique index)
- Name pre-filled from Telegram profile
- Role defaults to `buyer`; user can change
- A non-blocking onboarding screen offers (but does not require) email capture and preference setup
## What does NOT change
- Existing email/password and Google OAuth users are unaffected
- High-risk actions (release, refund, dispute resolution) still require step-up confirmation regardless of auth provider
- Telegram is not made the *only* auth method — it becomes an equal alternative
- Raw phone number is never exposed to Amanat; Telegram user ID is the stable identity anchor
## Key acceptance criteria
1. New Telegram user completes auth inside Mini App without entering email or password.
2. Returning Telegram user gets the same session whether using Mini App or web Login Widget.
3. Replay protection and max-age validation apply to this endpoint.
4. Blocked Telegram accounts receive 403 and cannot circumvent by re-linking.
5. Auto-provisioned users have `authProvider: telegram`; existing users are unaffected.
6. "Continue with Telegram" button visible on the web login page.
7. High-risk action step-up policy is unchanged.
## Collision and merge rules
| Scenario | Behavior |
|---|---|
| New Telegram user | Auto-provision + TelegramLink |
| Returning Telegram user | Auth as linked user |
| Email user opens Mini App (not yet linked) | Prompt to link (existing task 5.2 flow) |
| Same Telegram account on two app users | Rejected — TelegramLink.telegramUserId is unique |
## See also
- `.taskmaster/docs/prd-telegram-phone-auth.md` — full specification with endpoint contract, model changes, and frontend integration details
- `PRD - Platform Audit Remediation Plan (2026-05-24).md` — security baseline this feature must not weaken
- Task 5.2 (Identity linking), Task 5.8 (Security controls)

View File

@@ -0,0 +1,43 @@
---
taskmaster_id: "5.10"
status: "done"
priority: "high"
depends_on: ["5.2", "5.8"]
parent_id: "5"
source: "taskmaster"
generated_at: "2026-05-24T09:18:26.638Z"
completed_at: "2026-05-24"
---
# 5.10 - Implement Telegram as first-class authentication provider
- [x] 5.10 - Implement Telegram as first-class authentication provider #taskmaster #priority/high #status/done ⏫ 🆔 tm-5.10
## Metadata
| Field | Value |
| --- | --- |
| Taskmaster ID | 5.10 |
| Status | done |
| Priority | high |
| Dependencies | 5.2, 5.8 |
| Parent | 5 |
## Description
Add `POST /api/auth/telegram` and frontend login flow so users can authenticate with Amanat using only Telegram identity, without email or password.
## Details
Backend verifies Telegram Mini App `initData` and Telegram Login Widget payloads, checks/reuses `TelegramLink`, auto-provisions Telegram-only users with nullable email, `authProvider: "telegram"`, and `telegramVerified: true`, and returns the normal JWT/refresh-token pair plus `isNewUser`.
Frontend auto-authenticates Telegram Mini App launches from raw signed `initData`, adds a Telegram login action, and shows a lightweight onboarding dialog for new Telegram users.
## Verification
- Backend typecheck passed.
- Backend targeted Jest passed: `__tests__/telegram-auth.test.ts`, `__tests__/telegram-service.test.ts`.
- Frontend targeted Jest passed: `__tests__/auth/telegram-auth-action.test.ts`, `__tests__/sections/telegram/telegram-mini-app-shell.test.tsx`.
- Full frontend typecheck still has unrelated pre-existing payment UI errors outside this task.
See [[Task 5.10 Telegram First-Class Authentication]] for the audit report.