Merge remote-tracking branch 'origin/main'
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,4 +5,5 @@
|
|||||||
.obsidian/workspace.json
|
.obsidian/workspace.json
|
||||||
.obsidian/workspace-mobile.json
|
.obsidian/workspace-mobile.json
|
||||||
.obsidian/cache
|
.obsidian/cache
|
||||||
|
.obsidian/
|
||||||
.trash/
|
.trash/
|
||||||
|
|||||||
@@ -47,11 +47,13 @@ sequenceDiagram
|
|||||||
DB-->>BE: user doc (incl. hashed password)
|
DB-->>BE: user doc (incl. hashed password)
|
||||||
BE->>BE: bcrypt.compare(password, hash)
|
BE->>BE: bcrypt.compare(password, hash)
|
||||||
alt invalid
|
alt invalid
|
||||||
BE->>DB: increment loginAttempts; lock at N
|
BE->>DB: increment loginAttempts
|
||||||
|
BE->>DB: lock account at N
|
||||||
BE-->>FE: 401 / 423 locked
|
BE-->>FE: 401 / 423 locked
|
||||||
else valid
|
else valid
|
||||||
BE->>BE: sign JWT (7d), refresh (30d)
|
BE->>BE: sign JWT (7d), refresh (30d)
|
||||||
BE->>DB: store refresh-token id; clear attempts
|
BE->>DB: store refresh-token id
|
||||||
|
BE->>DB: clear attempts
|
||||||
BE-->>FE: 200 { user, token, refreshToken }
|
BE-->>FE: 200 { user, token, refreshToken }
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -79,7 +79,8 @@ sequenceDiagram
|
|||||||
FE-->>U: Redirect /auth/jwt/verify
|
FE-->>U: Redirect /auth/jwt/verify
|
||||||
else success
|
else success
|
||||||
BE->>R: rateLimitService.resetLoginAttempts(email)
|
BE->>R: rateLimitService.resetLoginAttempts(email)
|
||||||
BE->>DB: user.lastLoginAt = now; user.refreshTokens.push(refresh)
|
BE->>DB: set user.lastLoginAt = now
|
||||||
|
BE->>DB: save new refresh token
|
||||||
BE->>BE: generateToken(authUser) / generateRefreshToken(authUser)
|
BE->>BE: generateToken(authUser) / generateRefreshToken(authUser)
|
||||||
BE->>R: sessionService.createSession(accessToken, ...)
|
BE->>R: sessionService.createSession(accessToken, ...)
|
||||||
BE-->>FE: 200 { user, tokens: { accessToken, refreshToken } }
|
BE-->>FE: 200 { user, tokens: { accessToken, refreshToken } }
|
||||||
@@ -135,53 +136,5 @@ sequenceDiagram
|
|||||||
BE-->>FE: 401 TOKEN_INVALID
|
BE-->>FE: 401 TOKEN_INVALID
|
||||||
FE->>BE: POST /api/auth/refresh-token { refreshToken }
|
FE->>BE: POST /api/auth/refresh-token { refreshToken }
|
||||||
BE->>BE: verifyRefreshToken(refreshToken)
|
BE->>BE: verifyRefreshToken(refreshToken)
|
||||||
BE->>DB: User.findById(decoded.id); ensure refresh ∈ user.refreshTokens
|
BE->>DB: User.findById(decoded.id)
|
||||||
BE->>BE: Generate new access + refresh tokens
|
BE->>DB: ensure refresh token is in user.refreshTokens
|
||||||
BE->>DB: user.refreshTokens = [...minus old, new]
|
|
||||||
BE-->>FE: 200 { tokens: { accessToken, refreshToken } }
|
|
||||||
FE->>BE: GET /api/marketplace/... (Bearer new access) — retry
|
|
||||||
```
|
|
||||||
|
|
||||||
## Logout flow
|
|
||||||
|
|
||||||
1. Frontend `signOut()` (`action.ts:146-176`) reads `refreshToken` from `localStorage` and POSTs `/api/auth/logout` with a 10-second timeout.
|
|
||||||
2. Backend `authController.logout` (`:316-344`) removes the refresh token from `user.refreshTokens[]` and calls `sessionService.deleteSession(accessToken)`.
|
|
||||||
3. **Always-clear**: the frontend's `finally` block removes both `accessToken` and `refreshToken` from `localStorage` regardless of network success — meaning even an offline logout effectively signs the user out locally.
|
|
||||||
|
|
||||||
> [!tip] Force-logout an entire user
|
|
||||||
> Setting `user.refreshTokens = []` in MongoDB instantly invalidates all sessions on next refresh. `changePassword`, `resetPassword`, and `deleteAccount` all do this.
|
|
||||||
|
|
||||||
## Error / edge cases
|
|
||||||
|
|
||||||
- **Wrong password** → `401 Invalid credentials` (intentionally vague — no distinction between "unknown email" and "wrong password").
|
|
||||||
- **Email unverified** → `403 EMAIL_NOT_VERIFIED`; frontend auto-redirects to verify page.
|
|
||||||
- **5+ failures in 15 min** → `429 TOO_MANY_ATTEMPTS`; only an admin can manually clear via Redis.
|
|
||||||
- **Network timeout** → axios `AbortController` cancels at 60s; frontend shows a typed error and the user can retry.
|
|
||||||
- **Redis down** → login still succeeds (session creation is best-effort, wrapped in try/catch at `authController.ts:234-247`). Rate limiting falls back to the in-memory map in `authService.ts:113-145` if `rateLimitService` itself throws.
|
|
||||||
- **Stale refresh token** (rotated by another device) → `403 Invalid refresh token`. Frontend signs out and redirects to sign-in.
|
|
||||||
- **JWT signature mismatch** (secret rotated) → all sessions invalidated server-side; clients clear tokens on first 401.
|
|
||||||
- **Token issued for another audience/issuer** → `verifyToken` returns `null` (`authService.ts:60-79`), middleware returns `403 TOKEN_INVALID`.
|
|
||||||
- **Refresh token used as access token** → blocked by the `if (decoded.type === 'refresh') return null` check in `verifyToken` (`authService.ts:67`). This is critical: a leaked refresh token alone cannot read protected data.
|
|
||||||
- **Soft-deleted account** → `User.findOne({ status: "active" })` filter excludes deleted accounts; login fails as if the email did not exist.
|
|
||||||
|
|
||||||
> [!warning] Constant-time response is approximate
|
|
||||||
> Today we return `401` immediately when the user is missing, before running bcrypt. This is a timing oracle that lets an attacker enumerate registered emails by response-time analysis. Mitigation tracked separately — the recommendation is to always run a dummy bcrypt compare on missing users.
|
|
||||||
|
|
||||||
## Linked flows
|
|
||||||
|
|
||||||
- [[Registration Flow]] — produces the `User` document this flow consumes.
|
|
||||||
- [[Password Reset Flow]] — alternate entry into the account if credentials are lost.
|
|
||||||
- [[Google OAuth Flow]] — parallel auth path that produces equivalent tokens.
|
|
||||||
- [[Passkey (WebAuthn) Flow]] — passwordless alternative.
|
|
||||||
- [[Chat Flow]], [[Notification Flow]] — both consume the access token to authorise Socket.IO rooms.
|
|
||||||
|
|
||||||
## Source files
|
|
||||||
|
|
||||||
- Backend: `backend/src/services/auth/authController.ts:161-260`
|
|
||||||
- Backend: `backend/src/services/auth/authService.ts:24-99`
|
|
||||||
- Backend: `backend/src/services/auth/authRoutes.ts:22`
|
|
||||||
- Backend: `backend/src/services/redis/sessionService.ts`
|
|
||||||
- Backend: `backend/src/services/redis/rateLimitService.ts`
|
|
||||||
- Frontend: `frontend/src/auth/context/jwt/action.ts:32-176`
|
|
||||||
- Frontend: `frontend/src/auth/view/jwt/jwt-sign-in-view.tsx`
|
|
||||||
- Frontend: `frontend/src/lib/axios.ts` (interceptor + endpoints)
|
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ sequenceDiagram
|
|||||||
|
|
||||||
A->>FE_A: type & send
|
A->>FE_A: type & send
|
||||||
FE_A->>BE: POST /api/chat/{id}/messages {content}
|
FE_A->>BE: POST /api/chat/{id}/messages {content}
|
||||||
BE->>DB: chat.addMessage; metadata.lastActivity=now
|
BE->>DB: chat.addMessage and update metadata.lastActivity to now
|
||||||
BE->>IO: emit chat-{id} 'new-message'
|
BE->>IO: emit chat-{id} 'new-message'
|
||||||
IO-->>FE_A: 'new-message' (echo)
|
IO-->>FE_A: 'new-message' (echo)
|
||||||
IO-->>FE_B: 'new-message' (live)
|
IO-->>FE_B: 'new-message' (live)
|
||||||
|
|||||||
@@ -64,9 +64,11 @@ sequenceDiagram
|
|||||||
B->>FE: Enter code in dashboard
|
B->>FE: Enter code in dashboard
|
||||||
FE->>BE: POST /api/marketplace/purchase-requests/{id}/verify-delivery {code}
|
FE->>BE: POST /api/marketplace/purchase-requests/{id}/verify-delivery {code}
|
||||||
BE->>DB: match code, expires>now, !used
|
BE->>DB: match code, expires>now, !used
|
||||||
BE->>DB: deliveryCodeUsed=true; status="delivered"
|
BE->>DB: set deliveryCodeUsed = true
|
||||||
|
BE->>DB: set status = "delivered"
|
||||||
BE->>IO: emit request-{id} 'purchase-request-update' status-changed
|
BE->>IO: emit request-{id} 'purchase-request-update' status-changed
|
||||||
BE->>B,S: notifyDeliveryConfirmed
|
BE->>B: notifyDeliveryConfirmed
|
||||||
|
BE->>S: notifyDeliveryConfirmed
|
||||||
Note over BE: Auto-release timer (planned) → seller_paid → payout
|
Note over BE: Auto-release timer (planned) → seller_paid → payout
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -105,7 +105,8 @@ sequenceDiagram
|
|||||||
BE->>DB: Chat.create({type:"group", participants:[buyer, seller], system message})
|
BE->>DB: Chat.create({type:"group", participants:[buyer, seller], system message})
|
||||||
BE->>DB: dispute.chatId = chat._id
|
BE->>DB: dispute.chatId = chat._id
|
||||||
BE-->>FE: { dispute }
|
BE-->>FE: { dispute }
|
||||||
FE-->>B,S: chat opens (real-time via existing chat join)
|
FE-->>B: chat opens (real-time via existing chat join)
|
||||||
|
FE-->>S: chat opens (real-time via existing chat join)
|
||||||
|
|
||||||
A->>FE: Admin dashboard, click "Pick up"
|
A->>FE: Admin dashboard, click "Pick up"
|
||||||
FE->>BE: POST /api/disputes/{id}/assign
|
FE->>BE: POST /api/disputes/{id}/assign
|
||||||
@@ -130,7 +131,8 @@ sequenceDiagram
|
|||||||
A->>BE: split — refund X to buyer, release Y to seller
|
A->>BE: split — refund X to buyer, release Y to seller
|
||||||
end
|
end
|
||||||
BE-->>FE: { dispute }
|
BE-->>FE: { dispute }
|
||||||
IO-->>B,S: 'new-notification' dispute resolved (planned)
|
IO-->>B: 'new-notification' dispute resolved (planned)
|
||||||
|
IO-->>S: 'new-notification' dispute resolved (planned)
|
||||||
```
|
```
|
||||||
|
|
||||||
## API calls
|
## API calls
|
||||||
|
|||||||
@@ -87,9 +87,11 @@ sequenceDiagram
|
|||||||
else Sign-in: user missing
|
else Sign-in: user missing
|
||||||
BE-->>FE: 404 USER_NOT_FOUND
|
BE-->>FE: 404 USER_NOT_FOUND
|
||||||
else Sign-in: ok
|
else Sign-in: ok
|
||||||
BE->>DB: user.lastLoginAt = now; back-fill avatar if blank
|
BE->>DB: set user.lastLoginAt = now
|
||||||
|
BE->>DB: back-fill avatar if blank
|
||||||
end
|
end
|
||||||
BE->>BE: generate access + refresh; push refresh
|
BE->>BE: generate access and refresh tokens
|
||||||
|
BE->>BE: push refresh token
|
||||||
BE-->>FE: 200 { user, tokens }
|
BE-->>FE: 200 { user, tokens }
|
||||||
FE->>FE: localStorage.setItem(accessToken/refreshToken)
|
FE->>FE: localStorage.setItem(accessToken/refreshToken)
|
||||||
FE-->>U: Redirect /dashboard/{role}
|
FE-->>U: Redirect /dashboard/{role}
|
||||||
|
|||||||
@@ -118,7 +118,8 @@ sequenceDiagram
|
|||||||
B->>FE: Click "Publish"
|
B->>FE: Click "Publish"
|
||||||
FE->>BE: POST /api/marketplace/purchase-requests
|
FE->>BE: POST /api/marketplace/purchase-requests
|
||||||
BE->>DB: Duplicate check (same title+desc in 5m?)
|
BE->>DB: Duplicate check (same title+desc in 5m?)
|
||||||
BE->>BE: clean preferredSellerIds; compute isPublic
|
BE->>BE: clean preferredSellerIds
|
||||||
|
BE->>BE: compute isPublic
|
||||||
BE->>DB: PurchaseRequest.create({status: "pending"})
|
BE->>DB: PurchaseRequest.create({status: "pending"})
|
||||||
DB-->>BE: savedRequest
|
DB-->>BE: savedRequest
|
||||||
BE->>N: notifyPurchaseRequestCreated(buyer, requestId)
|
BE->>N: notifyPurchaseRequestCreated(buyer, requestId)
|
||||||
|
|||||||
@@ -93,7 +93,8 @@ sequenceDiagram
|
|||||||
|
|
||||||
Note over BE,DB: Later, when N completes a purchase
|
Note over BE,DB: Later, when N completes a purchase
|
||||||
BE->>BE: PointsService.addPoints(R, +X, 'referral', {referredUserId:N})
|
BE->>BE: PointsService.addPoints(R, +X, 'referral', {referredUserId:N})
|
||||||
BE->>DB: user.points += X; PointTransaction.create
|
BE->>DB: add X points to user balance
|
||||||
|
BE->>DB: create PointTransaction record
|
||||||
BE->>BE: updateUserLevel → maybe 'level-up'
|
BE->>BE: updateUserLevel → maybe 'level-up'
|
||||||
BE->>IO: emit user-{R} 'level-up'
|
BE->>IO: emit user-{R} 'level-up'
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -120,7 +120,8 @@ sequenceDiagram
|
|||||||
BE->>IO: emit user-{refId} 'referral-signup'
|
BE->>IO: emit user-{refId} 'referral-signup'
|
||||||
end
|
end
|
||||||
BE->>DB: TempVerification.findByIdAndDelete(...)
|
BE->>DB: TempVerification.findByIdAndDelete(...)
|
||||||
BE->>BE: generate tokens; push refresh
|
BE->>BE: generate tokens
|
||||||
|
BE->>BE: push refresh
|
||||||
BE-->>FE: 200 { user, tokens }
|
BE-->>FE: 200 { user, tokens }
|
||||||
FE->>FE: localStorage.setItem(accessToken, refreshToken)
|
FE->>FE: localStorage.setItem(accessToken, refreshToken)
|
||||||
FE-->>U: Redirect /dashboard/{role}
|
FE-->>U: Redirect /dashboard/{role}
|
||||||
|
|||||||
@@ -99,37 +99,37 @@ sequenceDiagram
|
|||||||
autonumber
|
autonumber
|
||||||
actor S as Seller
|
actor S as Seller
|
||||||
actor B as Buyer
|
actor B as Buyer
|
||||||
participant FE_S as Frontend (seller)
|
participant FE_S as Seller frontend
|
||||||
participant FE_B as Frontend (buyer)
|
participant FE_B as Buyer frontend
|
||||||
participant BE as Backend
|
participant BE as Backend
|
||||||
participant DB as MongoDB
|
participant DB as MongoDB
|
||||||
participant N as NotificationService
|
participant N as NotificationService
|
||||||
participant IO as Socket.IO
|
participant IO as SocketIO
|
||||||
|
|
||||||
S->>FE_S: Browse /dashboard/seller/marketplace
|
S->>FE_S: Browse marketplace
|
||||||
FE_S->>BE: GET /api/marketplace/purchase-requests?sellerId
|
FE_S->>BE: GET /api/marketplace/purchase-requests
|
||||||
BE-->>FE_S: filtered list
|
BE-->>FE_S: filtered request list
|
||||||
S->>FE_S: Open request, click "Send proposal"
|
S->>FE_S: Open request and send offer
|
||||||
S->>FE_S: Fill price, ETA, notes; submit
|
|
||||||
FE_S->>BE: POST /api/marketplace/offers
|
FE_S->>BE: POST /api/marketplace/offers
|
||||||
BE->>DB: ensure no existing offer; check status
|
BE->>DB: Validate offer not duplicate
|
||||||
BE->>DB: SellerOffer.create({status:"pending"})
|
BE->>DB: Validate request status
|
||||||
opt first offer on the request
|
BE->>DB: Create offer with status pending
|
||||||
BE->>DB: PurchaseRequest.status = "received_offers"
|
opt request has no offers yet
|
||||||
|
BE->>DB: Set request status to received_offers
|
||||||
end
|
end
|
||||||
BE->>N: notifyNewOfferReceived(buyer, requestId, sellerName)
|
BE->>N: notifyNewOfferReceived
|
||||||
N->>IO: emit user-{buyer} new-notification
|
N->>IO: emit notification to buyer
|
||||||
BE->>IO: emit seller-{sellerId} 'new-offer'
|
BE->>IO: emit seller new-offer
|
||||||
BE-->>FE_S: 200 { offer }
|
BE-->>FE_S: 200 { offer }
|
||||||
IO-->>FE_B: new-notification (buyer's bell icon)
|
IO-->>FE_B: notify buyer bell icon
|
||||||
B->>FE_B: Open request detail
|
B->>FE_B: Open request detail
|
||||||
FE_B->>BE: GET /api/marketplace/offers/request/{id}
|
FE_B->>BE: GET /api/marketplace/offers/request/{id}
|
||||||
BE-->>FE_B: offers[]
|
BE-->>FE_B: offers
|
||||||
alt Buyer accepts via payment
|
alt
|
||||||
B->>FE_B: Click "Pay" → starts [[Payment Flow - SHKeeper]]
|
B->>FE_B: Click pay to finish selected offer
|
||||||
Note over BE,DB: SHKeeper webhook PAID arrives later;<br/>winning offer → accepted, others → rejected
|
B->>FE_B: SHKeeper webhook handles payment result
|
||||||
else Buyer negotiates
|
else
|
||||||
B->>FE_B: Open chat → [[Negotiation Flow]]
|
B->>FE_B: Open chat to negotiate
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
435
09 - Audits/Platform Logical Audit - 2026-05-24.md
Normal file
435
09 - Audits/Platform Logical Audit - 2026-05-24.md
Normal file
@@ -0,0 +1,435 @@
|
|||||||
|
# Platform Logical Audit
|
||||||
|
|
||||||
|
**Date:** 2026-05-24
|
||||||
|
**Scope:** Data Models, API Reference, Architecture, Flows, Security
|
||||||
|
**Method:** Cross-document consistency check, state-machine validation, authorization gap analysis, dependency verification
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
This audit identifies critical logical contradictions, security holes, and cross-document inconsistencies across the Amn platform documentation. The most severe findings are:
|
||||||
|
|
||||||
|
1. **Dispute/escrow race condition** allowing fund release while a dispute is active.
|
||||||
|
2. **Three mutually incompatible `Dispute` status/action enums** across the Data Model, API Reference, and Flow documents — the documented API cannot be implemented against the documented schema.
|
||||||
|
3. **Passkey (WebAuthn) implementation is cryptographically broken** and unusable in production.
|
||||||
|
4. **Multiple financial endpoints require no authentication**, allowing unauthenticated payment record injection and private data exfiltration.
|
||||||
|
5. **Web3 payment verification only checks `receipt.status`**, not recipient address or amount, making payment fraud trivial.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔴 Critical Issues
|
||||||
|
|
||||||
|
### 1. Dispute Does Not Auto-Pause Escrow (Funds-at-Risk)
|
||||||
|
|
||||||
|
**Files:** `04 - Flows/Dispute Flow.md`, `03 - API Reference/Dispute API.md`
|
||||||
|
|
||||||
|
**Finding:** The Dispute Flow explicitly states: *"Today, opening a dispute does **not** flip `Payment.escrowState` away from `funded`. An admin could theoretically still release the escrow before resolving the dispute."*
|
||||||
|
|
||||||
|
At the same time, the Dispute API claims dispute creation *"Pauses any in-flight payout (sets a hold flag)."*
|
||||||
|
|
||||||
|
**Impact:** A buyer opens a dispute; an admin (or malicious/compromised admin) releases the escrow before resolution. The seller receives funds while the dispute is still open.
|
||||||
|
|
||||||
|
**Required Fix:** Introduce a `disputed` escrow state (or `disputeHold` flag) that blocks all release/refund operations until the dispute is resolved. The hold must be enforced in `PaymentCoordinator`, not just in controller logic.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Passkey Authentication Is Completely Broken
|
||||||
|
|
||||||
|
**Files:** `04 - Flows/Passkey (WebAuthn) Flow.md`, `02 - Data Models/User.md`
|
||||||
|
|
||||||
|
**Findings:**
|
||||||
|
- **Attestation is stubbed:** The `publicKey` field is stored as the literal string `'simulated-public-key'`. A malicious client can register any credential ID under any user account.
|
||||||
|
- **Refresh tokens are not persisted:** Passkey-issued refresh tokens are never appended to `user.refreshTokens[]`. Standard `/api/auth/refresh-token` will reject them, breaking session continuity.
|
||||||
|
- **In-memory challenge store:** `storedChallenges` is a `Map` in process memory. In a horizontally scaled deployment, the challenge created on instance A can only be verified on instance A. Load-balancer round-robin breaks authentication entirely.
|
||||||
|
|
||||||
|
**Impact:** Passkey auth is trivially bypassable and non-functional at scale.
|
||||||
|
|
||||||
|
**Required Fix:** Replace stub with `@simplewebauthn/server` (or equivalent), persist real public keys, store refresh tokens in the user record, and move challenges to Redis with TTL.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Unauthenticated State-Mutation Endpoints
|
||||||
|
|
||||||
|
**Files:** `03 - API Reference/Payment API.md`, `03 - API Reference/AI API.md`, `03 - API Reference/Notification API.md`
|
||||||
|
|
||||||
|
**Findings:**
|
||||||
|
|
||||||
|
| Endpoint | Risk |
|
||||||
|
|----------|------|
|
||||||
|
| `POST /api/payment/decentralized/save` | Anyone can persist a Web3 payment record |
|
||||||
|
| `POST /api/payment/decentralized/update` | Anyone can update decentralized payment status/confirmations |
|
||||||
|
| `GET /api/payment/decentralized/history/:userId` | Anyone can read any user's payment history (privacy breach) |
|
||||||
|
| `POST /api/payment/shkeeper/create-test-payment` | Anyone can inject test payment records into production data |
|
||||||
|
| `POST /api/payment/decentralized/verify/:paymentId` | Anyone can trigger chain re-verification |
|
||||||
|
| `POST /api/payment/decentralized/verify-all-pending` | Anyone can trigger a global batch verification job |
|
||||||
|
| All AI endpoints (`/generate`, `/analyze`, `/translate`, `/assist`) | No caller identity; unlimited OpenAI cost abuse |
|
||||||
|
| Legacy notification router (`notification/routes.ts`) | Mounted without auth; accepts `?userId=` query parameter allowing notification read/modify for any user |
|
||||||
|
|
||||||
|
**Impact:** Data poisoning, privacy violations, and unbounded cost exposure.
|
||||||
|
|
||||||
|
**Required Fix:** Add Bearer JWT middleware to all endpoints above. Enforce ownership or admin-role checks on `:userId` parameterized endpoints.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Web3 Payment Verification Trusts the Wrong Data
|
||||||
|
|
||||||
|
**Files:** `04 - Flows/Payment Flow - DePay & Web3.md`
|
||||||
|
|
||||||
|
**Finding:** The verifier checks only `receipt.status === '0x1'`. It **does not** decode the `Transfer` event to verify:
|
||||||
|
- `to` address == `ESCROW_WALLET_ADDRESS`
|
||||||
|
- `value` >= expectedAmount (accounting for decimals)
|
||||||
|
|
||||||
|
**Impact:** A malicious actor can submit the hash of any successful transaction (e.g., a 0.01 USDT transfer to their own wallet) and the system will mark the payment as completed.
|
||||||
|
|
||||||
|
**Required Fix:** Decode the event logs; verify recipient, token contract address, and amount match the intent.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Three Mutually Incompatible Dispute Enum Sets
|
||||||
|
|
||||||
|
**Files:** `02 - Data Models/Dispute.md`, `03 - API Reference/Dispute API.md`, `04 - Flows/Dispute Flow.md`
|
||||||
|
|
||||||
|
**Finding:** The three documents describe three different `Dispute` systems:
|
||||||
|
|
||||||
|
| Source | Status Enum | Resolution Action Enum |
|
||||||
|
|--------|-------------|------------------------|
|
||||||
|
| **Data Model** | `pending`, `in_progress`, `waiting_response`, `resolved`, `rejected`, `closed` | `refund`, `replacement`, `compensation`, `warning_seller`, `ban_seller`, `no_action` |
|
||||||
|
| **API Reference** | `open`, `under_review`, `resolved_buyer`, `resolved_seller`, `closed` | `buyer`, `seller`, `split` |
|
||||||
|
| **Flow** | `pending`, `in_progress`, `resolved`, `closed` | `refund`, `partial`, `release`, `reject` |
|
||||||
|
|
||||||
|
**Impact:** The API cannot be implemented as documented against the data model. The request/response schemas for `POST /api/disputes/:id/resolve` are completely different between API (uses `decision`) and Flow (uses `action`).
|
||||||
|
|
||||||
|
**Required Fix:** Create a single source-of-truth enum set. Align all three documents (or generate API docs from the model). `ban_seller` also has no corresponding `User.status` value (`User.status` is `active | suspended | deleted`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟠 High Severity Issues
|
||||||
|
|
||||||
|
### 6. Payment Model Uses `Mixed` for Core Foreign Keys
|
||||||
|
|
||||||
|
**File:** `02 - Data Models/Payment.md`
|
||||||
|
|
||||||
|
`purchaseRequestId`, `sellerOfferId`, and `sellerId` are `Schema.Types.Mixed` (ObjectId *or* String) to support the template flow. This sacrifices all type safety, referential integrity, and indexing efficiency for the normal (non-template) payment path, which is the vast majority of transactions.
|
||||||
|
|
||||||
|
**Fix:** Use strict `ObjectId` refs for the standard path; store template linkage in a separate `metadata` field.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. Broken Foreign Key References
|
||||||
|
|
||||||
|
**Files:** `02 - Data Models/PointTransaction.md`, `02 - Data Models/Chat.md`
|
||||||
|
|
||||||
|
- `PointTransaction.order` references an **`Order` model that does not exist** in any documented schema.
|
||||||
|
- `Chat.relatedTo.type` includes `Transaction`, but no `Transaction` model exists.
|
||||||
|
|
||||||
|
**Fix:** Remove or rename orphaned references. If `Order` is an internal alias, document it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. Delivery Confirmation Code Is Brute-Forceable
|
||||||
|
|
||||||
|
**Files:** `04 - Flows/Delivery Confirmation Flow.md`
|
||||||
|
|
||||||
|
The 6-digit code (`Math.floor(100000 + Math.random()*900000)`) has only 900,000 combinations. There is **no rate limiting** documented on verification attempts.
|
||||||
|
|
||||||
|
**Fix:** Add Redis-backed rate limiting (e.g., 5 attempts per 15 minutes per request). Consider increasing entropy or adding time-based invalidation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9. Purchase Request Status Machine Is Inconsistent
|
||||||
|
|
||||||
|
**Files:** `02 - Data Models/PurchaseRequest.md`, `03 - API Reference/Marketplace API.md`, `04 - Flows/Purchase Request Flow.md`
|
||||||
|
|
||||||
|
Three different status enums:
|
||||||
|
|
||||||
|
| Source | Unique Values |
|
||||||
|
|--------|---------------|
|
||||||
|
| **Data Model** | `pending_payment`, `active`, `seller_paid` |
|
||||||
|
| **API** | `draft` |
|
||||||
|
| **Flow** | `finalized`, `archived` |
|
||||||
|
|
||||||
|
The flow also claims a backward transition `in_negotiation → received_offers` is valid, but states elsewhere that "progression is forward-only except into terminal status."
|
||||||
|
|
||||||
|
**Fix:** One canonical enum. Remove ghost states (`draft`, `finalized`, `archived`, `pending_payment`, `active`) or implement them consistently.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10. JWTs in `localStorage` with 7-Day Expiry
|
||||||
|
|
||||||
|
**Files:** `01 - Architecture/Security Architecture.md`
|
||||||
|
|
||||||
|
Access tokens are stored in `localStorage` (XSS theft vector) and expire in **7 days**. For a financial escrow platform, this is far too long. The docs admit the risk but only list secondary mitigations (CSP, audits).
|
||||||
|
|
||||||
|
**Fix:** Move tokens to `httpOnly` cookies with 15–60 minute expiry and implement a secure refresh-token rotation mechanism.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11. SHKeeper Webhook Swallows All Errors
|
||||||
|
|
||||||
|
**File:** `04 - Flows/Payment Flow - SHKeeper.md`
|
||||||
|
|
||||||
|
The webhook returns `202 Accepted` even on signature failures, DB errors, and unknown payments. Failures are silently swallowed with no dead-letter queue, retry mechanism, or alerting.
|
||||||
|
|
||||||
|
**Fix:** Differentiate `400` (client error, do not retry) from `500` (server error, retry). Log structured webhook failures to a DLQ or alerting channel.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 12. Socket.IO Room Joining Is Client-Controlled
|
||||||
|
|
||||||
|
**Files:** `01 - Architecture/Real-time Layer.md`
|
||||||
|
|
||||||
|
Clients explicitly emit `join-user-room {userId}` with their own ID. The docs include a warning that `userId` arguments "are NOT trusted blindly" and add "(cite: needs verification in `socketService.ts`)." This is an explicit admission the authorization check may not be implemented. If missing, any authenticated user can subscribe to another user's private notifications.
|
||||||
|
|
||||||
|
**Fix:** Remove client-driven `join-user-room` events. The server should automatically join the socket to `user-{decoded.id}` immediately after handshake auth.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 13. Rate Limiting Is Effectively Disabled
|
||||||
|
|
||||||
|
**Files:** `01 - Architecture/Backend Architecture.md`, `03 - API Reference/API Overview.md`
|
||||||
|
|
||||||
|
`express-rate-limit` is disabled by default. Protected paths are limited to auth OTP/resend and AI endpoints. Marketplace, payment, chat, file upload, and notification endpoints have **no documented rate limiting**.
|
||||||
|
|
||||||
|
**Fix:** Enable global rate limiting with tiered limits (stricter for auth/financial, moderate for chat/marketplace).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟡 Medium Severity Issues
|
||||||
|
|
||||||
|
### 14. Architecture Claims Stateless, But Isn't
|
||||||
|
|
||||||
|
**Files:** `01 - Architecture/System Architecture.md`, `01 - Architecture/Backend Architecture.md`, `01 - Architecture/Real-time Layer.md`
|
||||||
|
|
||||||
|
- Claims backend is **stateless** (JWT-only, no sessions).
|
||||||
|
- Admits Redis stores **login-attempt lockout counters** (session state).
|
||||||
|
- Socket.IO requires **sticky sessions** for multi-node, but current infra is a single Docker host. Running `N=2` backend replicas would break real-time events.
|
||||||
|
|
||||||
|
**Fix:** Remove the "stateless" claim until auth state and Socket.IO are node-agnostic (Redis adapter + cookie-based sticky sessions).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 15. Fictitious Dependencies
|
||||||
|
|
||||||
|
**Files:** `01 - Architecture/Frontend Architecture.md`, `01 - Architecture/Infrastructure.md`
|
||||||
|
|
||||||
|
- **Next.js 16** does not exist (latest stable is 15). Using an unverified framework version for a financial platform is high-risk.
|
||||||
|
- **Redis 8** (`redis:8-alpine`) does not exist (latest stable is 7.x).
|
||||||
|
|
||||||
|
**Fix:** Update docs to reflect actual tested versions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 16. Blog Post `viewsCount` Incremented on GET
|
||||||
|
|
||||||
|
**File:** `03 - API Reference/Blog API.md`
|
||||||
|
|
||||||
|
`GET /api/blog/posts/:slug` atomically increments `viewsCount`. GET requests should be idempotent and safe.
|
||||||
|
|
||||||
|
**Fix:** Move view counting to a `POST /api/blog/posts/:slug/view` beacon endpoint, or use an async analytics pipeline.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 17. API Request/Response Fields Do Not Match Data Models
|
||||||
|
|
||||||
|
**File:** `03 - API Reference/*`
|
||||||
|
|
||||||
|
| API | Uses | Model Has |
|
||||||
|
|-----|------|-----------|
|
||||||
|
| Address (`POST /api/addresses`) | `fullName`, `street`, `postalCode`, `isPrimary` | `name`, `fullAddress`, `zipCode`, `primary` |
|
||||||
|
| Blog (`POST /api/blog/posts`) | `excerpt`, `isFeatured`, `videoUrl`, `metadata` | `description`, `featured`, `videos[]`, no `metadata` |
|
||||||
|
| Chat (`POST /api/chat`) | `title` | `name` |
|
||||||
|
| Chat message (`POST /api/chat/:id/messages`) | `type`, `replyToMessageId` | `messageType`, `replyTo` |
|
||||||
|
| Payment (multiple) | flat `amount: number` | nested `amount: { amount, currency }` |
|
||||||
|
| Notification (`POST /api/notifications`) | `body`, `type` (domain) | `message`, `type` (severity), `category` (domain) |
|
||||||
|
| User profile (`PUT /api/user/profile`) | `name` (alias for `profile.name`) | No `profile.name` field exists |
|
||||||
|
| Points (`POST /api/points/admin/add`) | `reason` | `description` (required) |
|
||||||
|
|
||||||
|
**Fix:** Align API specs to the data models, or vice versa, with a single source of truth.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 18. Seller Can Update Price After Offer Acceptance
|
||||||
|
|
||||||
|
**File:** `04 - Flows/Negotiation Flow.md`
|
||||||
|
|
||||||
|
The flow admits: *"`updateOffer` does not enforce status... Current code allows the price change, which is dangerous post-payment."*
|
||||||
|
|
||||||
|
**Fix:** Reject `updateOffer` if `status !== 'pending'`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 19. Buyer Can Confirm Delivery Before Seller Ships
|
||||||
|
|
||||||
|
**File:** `04 - Flows/Delivery Confirmation Flow.md`
|
||||||
|
|
||||||
|
Preconditions list `payment`, `processing`, or `delivery`. The "manual fast-track" lets the buyer skip the code and confirm receipt at any time — including immediately after payment, before the seller acknowledges.
|
||||||
|
|
||||||
|
**Fix:** Restrict fast-track confirmation to status `delivery` only, or require admin override.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 20. Payment `confirmed` Status Is a Ghost State
|
||||||
|
|
||||||
|
**Files:** `02 - Data Models/Payment.md`, `04 - Flows/Payment Flow - SHKeeper.md`
|
||||||
|
|
||||||
|
The model includes `confirmed` in `status`. The SHKeeper webhook maps `PAID` directly to `completed`, skipping `confirmed`. No automated flow appears to set it.
|
||||||
|
|
||||||
|
**Fix:** Remove `confirmed` from the enum, or document the transition that sets it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 21. `partial` Escrow State Does Not Exist in Model
|
||||||
|
|
||||||
|
**Files:** `04 - Flows/Escrow Flow.md`, `04 - Flows/Payment Flow - SHKeeper.md`, `02 - Data Models/Payment.md`
|
||||||
|
|
||||||
|
Both flows use `escrowState: "partial"` for overpayments. The `Payment` model enum is `funded | releasable | released | refunded | releasing | failed` — `partial` is absent.
|
||||||
|
|
||||||
|
**Fix:** Add `partial` to the model enum, or map overpayments to `funded` with a `overpaidAmount` metadata field.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 22. `BlogPost.comments` Is Just a Number
|
||||||
|
|
||||||
|
**File:** `02 - Data Models/BlogPost.md`
|
||||||
|
|
||||||
|
There is no `Comment` model. The system counts comments but cannot store or retrieve actual comment content.
|
||||||
|
|
||||||
|
**Fix:** Implement a `Comment` collection, or remove the `comments` counter.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 23. No Pagination on List-Oriented Endpoints
|
||||||
|
|
||||||
|
**File:** `03 - API Reference/*`
|
||||||
|
|
||||||
|
- `GET /api/marketplace/sellers`
|
||||||
|
- `GET /api/marketplace/purchase-requests/my`
|
||||||
|
- `GET /api/marketplace/purchase-requests/:id/offers`
|
||||||
|
- `GET /api/blog/posts/featured`
|
||||||
|
- `GET /api/points/leaderboard`
|
||||||
|
- `GET /api/users/contacts`
|
||||||
|
|
||||||
|
**Fix:** Add cursor- or offset-based pagination to all unbounded list endpoints.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 24. Missing Array Defaults
|
||||||
|
|
||||||
|
**Files:** `02 - Data Models/*`
|
||||||
|
|
||||||
|
Across 5+ models, array fields (`offers[]`, `tags[]`, `attachments[]`, `evidence[]`, `timeline[]`, `participants[]`, etc.) have no default value. In Mongoose they default to `undefined`, causing runtime errors on `.push()` or `.length`.
|
||||||
|
|
||||||
|
**Fix:** Add `default: []` to all array fields.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 25. Inconsistent Soft-Delete Patterns
|
||||||
|
|
||||||
|
**Files:** `02 - Data Models/*`
|
||||||
|
|
||||||
|
| Model | Pattern |
|
||||||
|
|-------|---------|
|
||||||
|
| User | `status: active / suspended / deleted` |
|
||||||
|
| BlogPost | `status: draft / published / archived` |
|
||||||
|
| RequestTemplate, Category, LevelConfig | `isActive` boolean |
|
||||||
|
| Dispute | `status` enum (no deleted state) |
|
||||||
|
| SellerOffer | `status` enum |
|
||||||
|
|
||||||
|
**Fix:** Standardize on one pattern (e.g., `status` enum with `deleted` or `archived`, plus `deletedAt` timestamp).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 26. Frontend Docker Image Cannot Be Promoted Across Environments
|
||||||
|
|
||||||
|
**File:** `01 - Architecture/Frontend Architecture.md`
|
||||||
|
|
||||||
|
The frontend uses `process.env.NEXT_PUBLIC_API_URL`, which is baked at **build time**. The same Docker image cannot be promoted from dev → staging → prod without rebuilding.
|
||||||
|
|
||||||
|
**Fix:** Inject the API URL at runtime via a server-side config endpoint or an env-replacement script in the container entrypoint.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 27. Watchtower Auto-Deploys with Zero Staging Gate
|
||||||
|
|
||||||
|
**File:** `01 - Architecture/Infrastructure.md`
|
||||||
|
|
||||||
|
Production auto-updates every 5 minutes on `latest` tag change with no health-check gate, blue/green rollout, or smoke test.
|
||||||
|
|
||||||
|
**Fix:** Implement a staging promotion pipeline: build → deploy to staging → smoke test → tag promote to `stable` → Watchtower watches `stable`, not `latest`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟢 Lower Severity / Polish Issues
|
||||||
|
|
||||||
|
### 28. Naming Inconsistencies
|
||||||
|
|
||||||
|
**Files:** `02 - Data Models/*`
|
||||||
|
|
||||||
|
- `PointTransaction.user` vs `*Id` suffix convention used everywhere else
|
||||||
|
- `User.referredBy` vs `referredById`
|
||||||
|
- `User.profile.phone` vs `phoneNumber` used in `Address` and `PurchaseRequest`
|
||||||
|
- `Chat.messages[].senderType` defaults to `User` (PascalCase) while 95% of enums use lowercase
|
||||||
|
- `Address.addressType` uses `Home` / `Office` / `Other` (PascalCase)
|
||||||
|
- `Chat.metadata.createdBy` lacks `Id` suffix
|
||||||
|
- `Dispute.resolution.resolvedBy` lacks `Id` suffix
|
||||||
|
|
||||||
|
### 29. Redundant / Derived Fields
|
||||||
|
|
||||||
|
**Files:** `02 - Data Models/PurchaseRequest.md`, `02 - Data Models/User.md`
|
||||||
|
|
||||||
|
- `PurchaseRequest.deliveryConfirmed` boolean + `deliveryConfirmedAt` — boolean is derivable
|
||||||
|
- `PurchaseRequest.deliveryInfo.deliveryCodeUsed` boolean — derivable from `deliveryCodeUsedAt`
|
||||||
|
- `User.points.total` — derivable from `available + used`; risks arithmetic drift
|
||||||
|
- `PurchaseRequest.rating` + `feedback` — duplicate the dedicated `Review` model (two sources of truth)
|
||||||
|
|
||||||
|
### 30. ER Diagram Errors
|
||||||
|
|
||||||
|
**File:** `02 - Data Models/Data Model Overview.md`
|
||||||
|
|
||||||
|
- `PURCHASE_REQUEST ||--o| REVIEW` implies max one review; the model allows many
|
||||||
|
- `CHAT ||--o{ DISPUTE` direction is reversed; a chat belongs to at most one dispute
|
||||||
|
|
||||||
|
### 31. Missing Audit Timestamps
|
||||||
|
|
||||||
|
**Files:** `02 - Data Models/*`
|
||||||
|
|
||||||
|
- `User` has `status: deleted` but no `deletedAt`
|
||||||
|
- `PurchaseRequest` has terminal states (`completed`, `seller_paid`, `cancelled`) but no `completedAt` / `cancelledAt`
|
||||||
|
- `Review` has `pending → published → rejected` workflow but no transition timestamps
|
||||||
|
- `Dispute` has `adminId` assignment but no `assignedAt`
|
||||||
|
|
||||||
|
### 32. Missing Constraints
|
||||||
|
|
||||||
|
**Files:** `02 - Data Models/*`
|
||||||
|
|
||||||
|
- `Dispute.purchaseRequestId` has no unique constraint (allows duplicate active disputes per request)
|
||||||
|
- `PurchaseRequest.deliveryInfo.deliveryCode` is not unique or sparse-unique across requests
|
||||||
|
- `Category.parentId` has no circular-reference guard
|
||||||
|
- `User.referredBy` can self-reference with no prevention
|
||||||
|
- `LevelConfig.discountPercent` has no upper bound (`max: 100`)
|
||||||
|
- `RequestTemplate.expiresAt` can be created in the past
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-Cutting Themes
|
||||||
|
|
||||||
|
1. **Documentation Drift:** The four primary document layers (Models, API, Flows, Architecture) describe different versions of the same system. The most egregious example is the `Dispute` entity, which has three incompatible definitions.
|
||||||
|
2. **Security Theater:** Several sections describe security measures (CSP, audits, future ClamAV) while fundamental flaws (localStorage tokens, unauthenticated endpoints, stubbed crypto) remain unaddressed.
|
||||||
|
3. **Ghost States:** Multiple status enums contain values that no automated flow sets (`confirmed`, `partial`, `finalized`, `archived`, `active` for SellerOffer).
|
||||||
|
4. **Operational Fragility:** The production topology is a single Docker host with automatic `latest` deployment. There is no HA, no staging gate, and no upload storage that can survive horizontal scaling (uploads are host bind-mounts).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommendations (Priority Order)
|
||||||
|
|
||||||
|
1. **Fix the Dispute ↔ Escrow race condition** immediately. A `disputed` state must block all releases.
|
||||||
|
2. **Align all status enums** across Models, API, and Flows. Create a single source-of-truth document.
|
||||||
|
3. **Harden Passkey authentication** before any production use.
|
||||||
|
4. **Add authentication and ownership checks** to all financial endpoints.
|
||||||
|
5. **Fix Web3 verification** to decode and validate `Transfer` event parameters.
|
||||||
|
6. **Enable rate limiting globally**, with stricter tiers for auth and financial paths.
|
||||||
|
7. **Implement idempotency keys** for dispute creation, payment verify, and payout creation.
|
||||||
|
8. **Protect `/uploads`** with signed URLs or session-based auth for sensitive attachments.
|
||||||
|
9. **Fix SHKeeper webhook error handling** — return proper status codes and log to a DLQ.
|
||||||
|
10. **Sanitize logs** to remove plaintext verification/reset codes.
|
||||||
118
PRD - Mermaid Diagram Fixes.md
Normal file
118
PRD - Mermaid Diagram Fixes.md
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
# PRD: Mermaid Diagram Rendering Stabilization
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
- Scope: Documentation Mermaid diagrams in this vault.
|
||||||
|
- Discovery date: 2026-05-24.
|
||||||
|
- Total Mermaid blocks checked: `57`.
|
||||||
|
- Total failing blocks: `11`.
|
||||||
|
- Tooling used: `@mermaid-js/mermaid-cli` parse validation for each extracted block.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
Create a pass-ready task queue of Mermaid syntax/render issues so each diagram can be corrected one-by-one without guessing.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
- Every Mermaid block parses successfully with the same mmdc-based syntax validation.
|
||||||
|
- Diagrams render in Obsidian/markdown previews without parser errors.
|
||||||
|
- Each corrected block retains the same intent and participant names where possible.
|
||||||
|
|
||||||
|
## Task 1 - Security Architecture: Email + password sequence
|
||||||
|
- **File:** `01 - Architecture/Security Architecture.md`
|
||||||
|
- **Diagram range:** `38-57` (Authentication layers / 2.1)
|
||||||
|
- **Error:** Parse error line 12: `...nAttempts; lock at N BE-->>FE: 4`
|
||||||
|
- **Likely issue:** Sequence parser is choking around a message containing inline punctuation and line continuation (`;` and `...`), likely interpreted as malformed sequence text.
|
||||||
|
- **Fix:** Keep the message text parser-safe (single simple `:` message per line; avoid special separators like unescaped semicolons) and rerun parser.
|
||||||
|
|
||||||
|
## Task 2 - Authentication Flow: Login sequence
|
||||||
|
- **File:** `04 - Flows/Authentication Flow.md`
|
||||||
|
- **Diagram range:** `57-90` (`## Sequence diagram`)
|
||||||
|
- **Error:** Parse error line 48: `...Tokens.push(refresh) BE->>BE: ge`
|
||||||
|
- **Likely issue:** Line includes inline method-like token with parentheses-like/space patterns and tokenization ambiguity.
|
||||||
|
- **Fix:** Replace the problematic statement with plain text in message, e.g. `BE->>DB: User.refreshTokens.push(refreshToken)` as one line.
|
||||||
|
|
||||||
|
## Task 3 - Authentication Flow: Refresh token flow
|
||||||
|
- **File:** `04 - Flows/Authentication Flow.md`
|
||||||
|
- **Diagram range:** `127-143` (`## Refresh flow`)
|
||||||
|
- **Error:** Parse error line 11: `... is in user.refreshTokens BE->>BE: Genera`
|
||||||
|
- **Likely issue:** Special math-style characters and expression-like message text interrupting parser tokenization.
|
||||||
|
- **Fix:** Replace special symbols in message text with plain ASCII and split to simple message line(s).
|
||||||
|
|
||||||
|
## Task 4 - Chat Flow: typing/chat notification sequence
|
||||||
|
- **File:** `04 - Flows/Chat Flow.md`
|
||||||
|
- **Diagram range:** `91-125` (`## Sequence diagram`)
|
||||||
|
- **Error:** Parse error line 20: `...ata.lastActivity=now BE->>IO: emit c`
|
||||||
|
- **Likely issue:** Assignment-style text (`...=now`) in message and possible bare punctuation in one-line payload.
|
||||||
|
- **Fix:** Simplify message bodies around state updates to readable text only.
|
||||||
|
|
||||||
|
## Task 5 - Delivery Confirmation Flow: code verification sequence
|
||||||
|
- **File:** `04 - Flows/Delivery Confirmation Flow.md`
|
||||||
|
- **Diagram range:** `45-71` (`## Sequence diagram`)
|
||||||
|
- **Error:** Parse error line 22: `...; status="delivered" BE->>IO: emit r`
|
||||||
|
- **Likely issue:** Double status expression in message (`deliveryCodeUsed=true; status="delivered"`).
|
||||||
|
- **Fix:** Split into two explicit sequence lines or rephrase with punctuation-safe text.
|
||||||
|
|
||||||
|
## Task 6 - Dispute Flow: admin resolve sequence
|
||||||
|
- **File:** `04 - Flows/Dispute Flow.md`
|
||||||
|
- **Diagram range:** `90-134` (`## Sequence diagram`)
|
||||||
|
- **Error:** Parse error line 18: `... is dispute } FE-->>B,S: chat opens (real`
|
||||||
|
- **Likely issue:** Invalid multi-recipient send syntax `B,S` on one arrow line.
|
||||||
|
- **Fix:** Either split into two lines (`FE-->>B:` and `FE-->>S:`) or route via a shared frontend/chat intermediary.
|
||||||
|
|
||||||
|
## Task 7 - Google OAuth Flow: completion sequence
|
||||||
|
- **File:** `04 - Flows/Google OAuth Flow.md`
|
||||||
|
- **Diagram range:** `56-96` (`## Sequence diagram`)
|
||||||
|
- **Error:** Parse error line 34: `...fill avatar if blank end BE->>BE`
|
||||||
|
- **Likely issue:** `end`/branch block combined with following arrow in parser context likely due missing separation in message-heavy block.
|
||||||
|
- **Fix:** Ensure `opt ... end` block boundaries and arrows are on clean, standalone lines.
|
||||||
|
|
||||||
|
## Task 8 - Purchase Request Flow: publish sequence
|
||||||
|
- **File:** `04 - Flows/Purchase Request Flow.md`
|
||||||
|
- **Diagram range:** `94-129` (`## Sequence diagram`)
|
||||||
|
- **Error:** Parse error line 27: `...ds; compute isPublic BE->>DB: Purcha`
|
||||||
|
- **Likely issue:** Message text includes expressions (`compute isPublic`) and punctuation (`;`) that are breaking the sequence parser.
|
||||||
|
- **Fix:** Rephrase backend processing lines into simple text without `;`/embedded expressions.
|
||||||
|
|
||||||
|
## Task 9 - Referral Flow: conversion and payout sequence
|
||||||
|
- **File:** `04 - Flows/Referral Flow.md`
|
||||||
|
- **Diagram range:** `70-99` (`## Sequence diagram`)
|
||||||
|
- **Error:** Parse error line 26: `...Transaction.create; update`
|
||||||
|
- **Likely issue:** Message text around chained commands (`updateUserLevel`) appears parser-conflicting.
|
||||||
|
- **Fix:** Break chained method-like messages into one action-per-line statements.
|
||||||
|
|
||||||
|
## Task 10 - Registration Flow: email verification sequence
|
||||||
|
- **File:** `04 - Flows/Registration Flow.md`
|
||||||
|
- **Diagram range:** `87-127` (`## Sequence diagram`)
|
||||||
|
- **Error:** Parse error line 36: `...tokens.push(refresh) BE->>BE: 200`
|
||||||
|
- **Likely issue:** Message line includes token-manipulation call style in plain text causing parser confusion.
|
||||||
|
- **Fix:** Reword to plain textual action; keep method calls minimal.
|
||||||
|
|
||||||
|
## Task 11 - Seller Offer Flow: submit offer sequence
|
||||||
|
- **File:** `04 - Flows/Seller Offer Flow.md`
|
||||||
|
- **Diagram range:** `97-134` (`## Sequence diagram`)
|
||||||
|
- **Error:** Parse error line 16: `...ETA, notes; submit FE_S->>BE: POST`
|
||||||
|
- **Likely issue:** Message with `; submit` and inline comma-separated action text.
|
||||||
|
- **Fix:** Split into cleaner action lines before POST step.
|
||||||
|
|
||||||
|
## Execution Order
|
||||||
|
1. Address Task 6 first (obvious syntax violation) to quickly cut down parse noise.
|
||||||
|
2. Tackle Tasks 2, 3, 5, 8, 9, 10, 11 (message-format cleanup).
|
||||||
|
3. Validate with a full parser sweep after each batch.
|
||||||
|
4. Final sweep: re-run mmdc parse validation across all `57` Mermaid blocks and close this PRD once no errors remain.
|
||||||
|
|
||||||
|
## Current Run Status
|
||||||
|
- Execution date: 2026-05-24
|
||||||
|
- Parallel passes launched: 3
|
||||||
|
- Result: All targeted task files pass `@mermaid-js/mermaid-cli` parsing.
|
||||||
|
- Full vault sweep (all markdown Mermaid blocks): **pass**.
|
||||||
|
|
||||||
|
### Task Completion
|
||||||
|
- Task 1: Complete (Security Architecture sequence lines simplified)
|
||||||
|
- Task 2: Complete (Authentication login refresh token line split/rephrased)
|
||||||
|
- Task 3: Complete (Authentication refresh-flow syntax normalized)
|
||||||
|
- Task 4: Complete (Chat flow update-message line rewritten)
|
||||||
|
- Task 5: Complete (Delivery confirmation message split into safe lines)
|
||||||
|
- Task 6: Complete (Dispute flow multi-recipient send split into separate arrows)
|
||||||
|
- Task 7: Complete (Google OAuth flow statement ordering fixed)
|
||||||
|
- Task 8: Complete (Purchase Request preference/public logic split into separate action lines)
|
||||||
|
- Task 9: Complete (Referral flow user-points action split)
|
||||||
|
- Task 10: Complete (Registration token update line split)
|
||||||
|
- Task 11: Complete (Seller Offer flow rewritten into parser-safe sequence diagram)
|
||||||
232
PRD - Platform Audit Remediation Plan (2026-05-24).md
Normal file
232
PRD - Platform Audit Remediation Plan (2026-05-24).md
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
# PRD: Platform Audit Remediation Plan (2026-05-24)
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
- Source audit: `09 - Audits/Platform Logical Audit - 2026-05-24.md`
|
||||||
|
- Target: backend hardening and consistency fixes first, then doc/runtime alignment.
|
||||||
|
- Assessed date: 2026-05-24
|
||||||
|
|
||||||
|
## Audit validity check (code-backed vs docs-only)
|
||||||
|
|
||||||
|
- **Code-confirmed valid findings**:
|
||||||
|
- Unauthenticated financial/auth-sensitive routes are exposed (`/api/payment/*`, `/api/ai/*`, legacy notification routes).
|
||||||
|
- Passkey flow uses stubbed credential handling and in-memory challenge storage.
|
||||||
|
- `PaymentCoordinator` does not currently enforce dispute holds.
|
||||||
|
- Web3 verifier relies primarily on `receipt.status`.
|
||||||
|
- Socket.IO user-room joins are client-controlled.
|
||||||
|
- Rate limiting is explicitly disabled in `backend/src/app.ts`.
|
||||||
|
|
||||||
|
- **Partially validated / inconsistent findings**:
|
||||||
|
- Dispute module appears absent in the checked backend tree (`backend/src` does not contain `services/dispute`/`routes/disputeRoutes`/`controllers/disputeController`), so many dispute-specific audit checks cannot be confirmed against code but indicate a documentation-runtime mismatch.
|
||||||
|
- The REST/API/flow docs mention endpoints that differ from available implementation in some places; these should be normalized as part of a separate alignment pass.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PRD 1: Secure unauthenticated endpoints and data-owner enforcement (P0)
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
- `backend/src/services/payment/decentralizedPaymentRoutes.ts` exposes mutation endpoints (`/save`, `/update`, `/verify`, `/verify-all-pending`) without auth.
|
||||||
|
- `backend/src/services/ai/aiRoutes.ts` has public generation endpoints (`/generate`, `/analyze`, `/translate`, `/assist`) with no auth.
|
||||||
|
- `backend/src/services/notification/routes.ts` lets clients specify `userId` and mutate/read without ownership checks, and routes are mounted publicly via `app.use("/api", notificationRoutes)` in `backend/src/app.ts`.
|
||||||
|
|
||||||
|
### Acceptance criteria
|
||||||
|
- Every endpoint in `decentralizedPaymentRoutes.ts`, `aiRoutes.ts`, and legacy `notificationRoutes.ts` requires:
|
||||||
|
- `authenticateToken`
|
||||||
|
- owner/admin role check where applicable
|
||||||
|
- Notification endpoints do not accept arbitrary `userId`; derive `userId` from authenticated principal.
|
||||||
|
- Payment history and mutation endpoints enforce requestor ownership or admin/authorized role.
|
||||||
|
|
||||||
|
### Risk assessment
|
||||||
|
- **Threat**: Financial/private data tampering or privacy breach by unauthenticated callers.
|
||||||
|
- **Likelihood**: High.
|
||||||
|
- **Impact**: Critical (funding fraud, PII leakage, cost abuse).
|
||||||
|
- **Mitigation**: Introduce router-level middleware and standardized auth helper.
|
||||||
|
- **Residual risk**: Medium if any integration jobs call these endpoints (document required with service tokens or service role).
|
||||||
|
|
||||||
|
### Delivery plan
|
||||||
|
1. Add route-level `authenticateToken` to all routes in files above.
|
||||||
|
2. Introduce ownership guard utilities for:
|
||||||
|
- `history/:userId` and payment listing/history filters
|
||||||
|
- notification queries/updates
|
||||||
|
3. Restrict AI calls to authenticated users with per-user rate budget.
|
||||||
|
4. Add audit logs for denied access and suspicious payloads.
|
||||||
|
5. Update API docs for auth requirements.
|
||||||
|
|
||||||
|
### Owner / dependencies
|
||||||
|
- Owner: Backend security + payments teams.
|
||||||
|
- Depends on: token standards in `backend/src/shared/middleware/auth`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PRD 2: Production-grade Passkey/WebAuthn flow (P0)
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
- `backend/src/services/auth/passkeyService.ts` stores passkey challenge data in an in-memory `Map` and inserts `'simulated-public-key'` during registration.
|
||||||
|
- Refresh token is generated on authentication but never persisted to `user.refreshTokens[]`, making token rotation behavior inconsistent with the rest of auth flows.
|
||||||
|
|
||||||
|
### Acceptance criteria
|
||||||
|
- Replace stubbed registration/verification with real WebAuthn verification.
|
||||||
|
- Back challenges by shared storage (Redis or equivalent), not in-process memory.
|
||||||
|
- Persist verified refresh tokens and rotate according to normal auth policy.
|
||||||
|
- Reject reused/expired challenges and malformed assertions with deterministic errors.
|
||||||
|
|
||||||
|
### Risk assessment
|
||||||
|
- **Threat**: Credential spoofing, replay attacks, and broken session continuity.
|
||||||
|
- **Likelihood**: High.
|
||||||
|
- **Impact**: Critical (account takeover / authentication bypass).
|
||||||
|
- **Mitigation**: Add `@simplewebauthn/server` (or equivalent) + Redis-backed nonce store + strict verification.
|
||||||
|
- **Residual risk**: Medium (device/browser compatibility edge-cases).
|
||||||
|
|
||||||
|
### Delivery plan
|
||||||
|
1. Replace `verifyRegistration` and `verifyAuthentication` with attestation/assertion verification.
|
||||||
|
2. Swap `storedChallenges` map for distributed storage with TTL.
|
||||||
|
3. Add persistent refresh token append in passkey login flow.
|
||||||
|
4. Add regression tests for sign-in/sign-up and challenge expiry.
|
||||||
|
|
||||||
|
### Owner / dependencies
|
||||||
|
- Owner: Auth team.
|
||||||
|
- Depends on: Redis infra, passkey frontend challenge handling.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PRD 3: Enforce dispute hold before payout/release operations (P1)
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
- No dispute implementation files are present in the checked backend for enforcement paths, yet escrow release can still proceed in payment logic without a blocking hold.
|
||||||
|
- `backend/src/services/marketplace/routes.ts` release endpoint (`/purchase-requests/:id/release-payment`) executes without a dispute gate.
|
||||||
|
- `backend/src/services/payment/paymentCoordinator.ts` applies sequencing and dedupe logic but has no dispute state guard.
|
||||||
|
|
||||||
|
### Acceptance criteria
|
||||||
|
- Introduce explicit hold mechanism on `Payment` (or new `disputeStatus`/`holdReason` field).
|
||||||
|
- All release/refund flows check hold state before state mutation.
|
||||||
|
- Release endpoint returns clear 409/423 response when disputed.
|
||||||
|
- Backfill/reporting distinguishes dispute-blocked payments.
|
||||||
|
|
||||||
|
### Risk assessment
|
||||||
|
- **Threat**: Funds released before dispute resolution.
|
||||||
|
- **Likelihood**: Medium (documented behavior gap).
|
||||||
|
- **Impact**: Critical (financial loss / trust erosion).
|
||||||
|
- **Mitigation**: Centralize payout/refund state checks in coordinator and services, not UI-only logic.
|
||||||
|
- **Residual risk**: Medium (existing open disputes may need manual reconciliation).
|
||||||
|
|
||||||
|
### Delivery plan
|
||||||
|
1. Add `disputed`/`disputeHoldReason` fields (and migration plan) to `Payment`.
|
||||||
|
2. Add hold checks in:
|
||||||
|
- `backend/src/services/marketplace/routes.ts` admin release flow
|
||||||
|
- `backend/src/services/payment/shkeeper/shkeeperService.ts` and payout flows
|
||||||
|
- `backend/src/services/payment/paymentCoordinator.ts` before completion transitions.
|
||||||
|
3. Create an internal service contract for setting/releasing hold after dispute open/resolve.
|
||||||
|
4. Add guard tests for blocked release and allowed release post-override.
|
||||||
|
|
||||||
|
### Owner / dependencies
|
||||||
|
- Owner: Escrow/payment platform team.
|
||||||
|
- Depends on: dispute subsystem availability / status source-of-truth.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PRD 4: Strengthen DePay/Web3 verification semantics (P1)
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
- `backend/src/services/payment/decentralizedPaymentService.ts` verifies only `receipt.status === '0x1'`, without validating destination token, recipient, and amount mapping.
|
||||||
|
|
||||||
|
### Acceptance criteria
|
||||||
|
- Verification loads the transaction receipt + logs + token-decoding and validates:
|
||||||
|
- correct recipient address (`toAddress`)
|
||||||
|
- expected token contract (`token`)
|
||||||
|
- minimum amount with decimals.
|
||||||
|
- Reject/flag ambiguous payloads and store verifier evidence in `Payment.metadata`.
|
||||||
|
- Add idempotency/duplicate handling for repeated verification requests.
|
||||||
|
|
||||||
|
### Risk assessment
|
||||||
|
- **Threat**: Fraudulent transaction proofing and false-positive confirmations.
|
||||||
|
- **Likelihood**: Medium.
|
||||||
|
- **Impact**: High (unauthorized balance movement/false completion).
|
||||||
|
- **Mitigation**: Strict recipient/token/amount checks + explicit failure reason mapping.
|
||||||
|
- **Residual risk**: Medium (chain/protocol variance).
|
||||||
|
|
||||||
|
### Delivery plan
|
||||||
|
1. Extend verifier schema for expected transfer contract/address/decimals.
|
||||||
|
2. Decode ERC-20 `Transfer` event and compare against expected values.
|
||||||
|
3. Mark and persist `verificationFingerprint` to prevent replay/double-processing.
|
||||||
|
4. Return canonical failure reasons for observability.
|
||||||
|
|
||||||
|
### Owner / dependencies
|
||||||
|
- Owner: Payments + blockchain service team.
|
||||||
|
- Depends on: RPC/provider reliability and token ABI metadata.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PRD 5: Lock Socket.IO user room joins to authenticated context (P2)
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
- `backend/src/app.ts` allows `join-user-room`, `join-buyer-room`, and `join-seller-room` events from arbitrary input IDs with no identity binding.
|
||||||
|
|
||||||
|
### Acceptance criteria
|
||||||
|
- Server derives room membership from validated `socket` identity after token handshake.
|
||||||
|
- Reject mismatched room join attempts with server-side authorization checks.
|
||||||
|
- Preserve real-time event behavior for legitimate users.
|
||||||
|
|
||||||
|
### Risk assessment
|
||||||
|
- **Threat**: Event channel interception and privacy leakage.
|
||||||
|
- **Likelihood**: Medium.
|
||||||
|
- **Impact**: High (notifications/messages for wrong user).
|
||||||
|
- **Mitigation**: Server-authorized socket-to-user mapping and middleware verification.
|
||||||
|
- **Residual risk**: Low (requires active socket auth and client migration).
|
||||||
|
|
||||||
|
### Delivery plan
|
||||||
|
1. Add token validation middleware on socket connection.
|
||||||
|
2. Replace client-sent ID room joins with authenticated identity joins.
|
||||||
|
3. Add per-room join/emit checks in critical publishers.
|
||||||
|
4. Add monitoring for room-join failures and suspicious activity.
|
||||||
|
|
||||||
|
### Owner / dependencies
|
||||||
|
- Owner: Real-time platform team.
|
||||||
|
- Depends on: frontend socket auth handshake updates.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PRD 6: Re-enable and scope rate limiting for public-sensitive paths (P2)
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
- `backend/src/app.ts` has comments and code indicating rate limiting is disabled globally.
|
||||||
|
|
||||||
|
### Acceptance criteria
|
||||||
|
- Re-enable limiter at edge level with sensible defaults.
|
||||||
|
- Use stricter quotas for auth/financial/AI endpoints.
|
||||||
|
- Keep lightweight paths (public listings, static read APIs) at higher budgets.
|
||||||
|
|
||||||
|
### Risk assessment
|
||||||
|
- **Threat**: Brute-force/log abuse, AI cost exhaustion, webhook abuse.
|
||||||
|
- **Likelihood**: High.
|
||||||
|
- **Impact**: Medium-High (operational availability and cost).
|
||||||
|
- **Mitigation**: Endpoint-tiered limiter with trusted network exceptions and observability.
|
||||||
|
- **Residual risk**: Medium (false positives on legitimate spikes).
|
||||||
|
|
||||||
|
### Delivery plan
|
||||||
|
1. Restore global limiter with default relaxed values.
|
||||||
|
2. Add stricter route-level windows for `/api/auth/*`, `/api/payment/*`, `/api/ai/*`.
|
||||||
|
3. Add alerts on repeated 429s and limit misconfigurations.
|
||||||
|
|
||||||
|
### Owner / dependencies
|
||||||
|
- Owner: Platform infrastructure/security team.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documentation and API alignment follow-up (P2)
|
||||||
|
|
||||||
|
- Where docs refer to `backend/src/services/dispute/*` and `backend/src/services/dispute/disputeRoutes.ts`, either implement that module or update architecture/API/docs to current repository reality.
|
||||||
|
- Normalize enum/action naming across:
|
||||||
|
- `02 - Data Models/*`
|
||||||
|
- `03 - API Reference/*`
|
||||||
|
- `04 - Flows/*`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Delivery sequencing suggestion
|
||||||
|
1. PRD 1 (security/auth)
|
||||||
|
2. PRD 6 (rate limiting)
|
||||||
|
3. PRD 2 (passkeys)
|
||||||
|
4. PRD 4 (verification trust)
|
||||||
|
5. PRD 5 (socket hardening)
|
||||||
|
6. PRD 3 (dispute hold controls)
|
||||||
|
7. Documentation/API alignment
|
||||||
|
|
||||||
333
PRD - Request Network Migration and Funds Management.md
Normal file
333
PRD - Request Network Migration and Funds Management.md
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
# PRD - Request Network Migration and Funds Management
|
||||||
|
|
||||||
|
Date: 2026-05-24
|
||||||
|
Status: Draft for architecture review
|
||||||
|
Owner: Platform / Payments
|
||||||
|
Codebase reviewed: `/Users/manwe/CascadeProjects/escrow`
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
Replacing SHKeeper with Request Network is not a provider swap. SHKeeper currently acts like a deposit-wallet and payout-task provider: the backend creates a payment intent, receives a generated wallet address, watches SHKeeper webhooks and BSC wallet balances, then optionally creates an outgoing payout task.
|
||||||
|
|
||||||
|
Request Network is request/payment-reference based. The platform creates a Request Network payment request or secure payment page, the payer pays through Request Network contracts or hosted flow, and reconciliation happens through Request Network payment events. Payouts are API-created payment requests/calldata that still require wallet execution. This means Amanat needs a first-class funds ledger and a release/refund orchestration layer, not just renamed `/payment/shkeeper/*` endpoints.
|
||||||
|
|
||||||
|
Primary recommendation: run a phased migration behind a provider adapter. Introduce Request Network for new pay-ins first using Secure Payment Pages, keep SHKeeper read-only for existing payments, then add funds ledger, release/refund gates, and Request Network payout flows.
|
||||||
|
|
||||||
|
## Source Findings
|
||||||
|
|
||||||
|
Current code:
|
||||||
|
|
||||||
|
- Backend mounts SHKeeper at `/api/payment/shkeeper` through `backend/src/services/payment/shkeeper/shkeeperRoutes.ts` and `shkeeperPayoutRoutes.ts`.
|
||||||
|
- Frontend calls SHKeeper through `frontend/src/actions/payment.ts`, `frontend/src/web3/components/shkeeper-payment.tsx`, `shkeeper-widget.tsx`, and `shkeeper-payout.tsx`.
|
||||||
|
- `Payment.provider` only allows `shkeeper` or `other`; SHKeeper-specific fields live directly under `Payment.metadata`.
|
||||||
|
- Pay-in completion is accepted from SHKeeper webhook, manual confirmation, or wallet monitor, all coordinated through `PaymentCoordinator`.
|
||||||
|
- Current payout paths mix SHKeeper payout tasks, simulated marketplace release, and direct admin wallet payout.
|
||||||
|
- There is no durable internal funds ledger that tracks available, held, releasable, disputed, released, refunded, and fee amounts per purchase request.
|
||||||
|
|
||||||
|
Request Network documentation checked:
|
||||||
|
|
||||||
|
- API v2 supports programmatic request creation, secure payment pages, payouts, webhooks, payment search, platform fees, and payee destinations: <https://docs.request.network/llms.txt>
|
||||||
|
- Secure Payment Pages are hosted payment URLs returned by `POST /v2/secure-payments`, returning `requestIds`, `token`, and `securePaymentUrl`: <https://beta.docs.request.network/api-features/secure-payment-pages>
|
||||||
|
- Secure Payment Pages validate official Request Network contracts before payer signing: <https://beta.docs.request.network/api-features/secure-payment-pages>
|
||||||
|
- Webhooks are scoped by Client ID and include `x-request-network-signature`, delivery ID, retry count, and optional test header: <https://requestnetwork.mintlify.app/api-reference/webhooks>
|
||||||
|
- Request Network webhooks retry failed deliveries up to 3 retries with short backoff windows: <https://requestnetwork.mintlify.app/api-reference/webhooks>
|
||||||
|
- Payout endpoints return unsigned transaction data that the platform/wallet must execute: <https://docs.request.network/request-network-api/recurring-payments>
|
||||||
|
- Batch payouts can return approval and batch payment transactions; the application sends those wallet transactions: <https://docs.request.network/request-network-api/batch-payments>
|
||||||
|
- Request Network protocol fee is 5 bps with stablecoin caps, and platform fees can be included through `feePercentage` and `feeAddress`: <https://docs.request.network/request-network-api/fees>
|
||||||
|
- Payment networks use payment references for detection, rather than unique deposit wallets per invoice: <https://docs.request.network/advanced/protocol-overview/how-payment-networks-work>
|
||||||
|
|
||||||
|
## Target Architecture
|
||||||
|
|
||||||
|
### Payment Provider Adapter
|
||||||
|
|
||||||
|
Create a provider-neutral payment interface:
|
||||||
|
|
||||||
|
- `createPayInIntent`
|
||||||
|
- `getPayInStatus`
|
||||||
|
- `handleProviderWebhook`
|
||||||
|
- `createHostedPaymentLink`
|
||||||
|
- `createReleaseInstruction`
|
||||||
|
- `createRefundInstruction`
|
||||||
|
- `getPayoutStatus`
|
||||||
|
- `searchProviderPayments`
|
||||||
|
|
||||||
|
Implement:
|
||||||
|
|
||||||
|
- `ShkeeperProvider` for legacy compatibility.
|
||||||
|
- `RequestNetworkProvider` for new flows.
|
||||||
|
|
||||||
|
### Funds Ledger
|
||||||
|
|
||||||
|
Add an internal ledger separate from provider metadata:
|
||||||
|
|
||||||
|
- `fundsAccount`: one per purchase request or order.
|
||||||
|
- `ledgerEntry`: immutable entries for authorization, payment detected, platform fee, provider fee, hold, release, refund, chargeback/dispute hold, adjustment.
|
||||||
|
- `fundsBalance`: derived or cached view containing `expected`, `paid`, `held`, `releasable`, `released`, `refunded`, `disputed`, `fees`, and `available`.
|
||||||
|
|
||||||
|
Ledger invariants:
|
||||||
|
|
||||||
|
- Seller payout cannot exceed paid amount minus refunds, fees, and active dispute holds.
|
||||||
|
- Release requires payment status `completed`, escrow state `releasable`, no open dispute, and a verified seller destination.
|
||||||
|
- Refund requires available held balance and must mark the purchase request/payment state consistently.
|
||||||
|
- Provider webhooks never directly mark funds as released; they create/reconcile ledger entries.
|
||||||
|
|
||||||
|
### Payment Flow
|
||||||
|
|
||||||
|
Pay-in:
|
||||||
|
|
||||||
|
1. Buyer accepts seller offer.
|
||||||
|
2. Backend creates internal payment + funds account.
|
||||||
|
3. Backend calls Request Network `POST /v2/secure-payments` or `POST /v2/request`.
|
||||||
|
4. Frontend redirects to `securePaymentUrl` or executes returned calldata in wallet.
|
||||||
|
5. Request Network webhook confirms payment.
|
||||||
|
6. Backend verifies signature, idempotency, amount, request ID, payment reference, currency, and merchant reference.
|
||||||
|
7. Ledger moves from `expected` to `held`.
|
||||||
|
8. Purchase request enters `payment` or `processing`.
|
||||||
|
|
||||||
|
Release:
|
||||||
|
|
||||||
|
1. Buyer delivery confirmation or admin release marks funds `releasable`.
|
||||||
|
2. Backend creates Request Network payout/release instruction, or admin wallet transaction instruction.
|
||||||
|
3. Admin/operator wallet signs and broadcasts transaction.
|
||||||
|
4. Backend records transaction hash as `release_pending`.
|
||||||
|
5. Webhook/search confirms settlement.
|
||||||
|
6. Ledger moves `held -> released`.
|
||||||
|
|
||||||
|
Refund:
|
||||||
|
|
||||||
|
1. Refund request validates available held balance.
|
||||||
|
2. Backend creates refund transaction instruction.
|
||||||
|
3. Admin/operator wallet executes.
|
||||||
|
4. Confirmation updates ledger `held -> refunded`.
|
||||||
|
|
||||||
|
## PRD 1 - Provider-Neutral Payment Adapter
|
||||||
|
|
||||||
|
Priority: P0
|
||||||
|
|
||||||
|
Goal: decouple checkout, webhook, and payout flows from SHKeeper-specific routes and metadata.
|
||||||
|
|
||||||
|
Scope:
|
||||||
|
|
||||||
|
- Add `paymentProvider` abstraction in backend.
|
||||||
|
- Add `provider: 'shkeeper' | 'request_network' | 'manual' | 'admin_wallet'` to payment model.
|
||||||
|
- Keep `/api/payment/shkeeper/*` operational for legacy records.
|
||||||
|
- Add `/api/payment/providers/request-network/*` or `/api/payment/request-network/*` for new Request Network flows.
|
||||||
|
- Add a feature flag: `PAYMENT_PROVIDER=request_network|shkeeper`.
|
||||||
|
|
||||||
|
Acceptance Criteria:
|
||||||
|
|
||||||
|
- New checkout can create a Request Network payment link without touching SHKeeper service code.
|
||||||
|
- Existing SHKeeper payments remain readable and can still process late webhooks.
|
||||||
|
- Frontend does not import provider-specific action names outside provider-specific components.
|
||||||
|
- Provider metadata is namespaced under `metadata.providers.requestNetwork` or a dedicated provider table.
|
||||||
|
|
||||||
|
Risk Assessment:
|
||||||
|
|
||||||
|
- Impact: High. Payment creation is core revenue flow.
|
||||||
|
- Likelihood: Medium. Existing code has many direct `shkeeper` references.
|
||||||
|
- Main risks: broken checkout, duplicate payment records, orphaned webhooks, mixed provider states.
|
||||||
|
- Mitigation: use provider feature flag, preserve SHKeeper routes, add idempotency keys, run Request Network only for new payments during rollout.
|
||||||
|
- Rollback: set `PAYMENT_PROVIDER=shkeeper`; keep Request Network records read-only.
|
||||||
|
|
||||||
|
## PRD 2 - Request Network Pay-In Integration
|
||||||
|
|
||||||
|
Priority: P0
|
||||||
|
|
||||||
|
Goal: replace generated SHKeeper wallet addresses with Request Network request/payment-reference based payment collection.
|
||||||
|
|
||||||
|
Scope:
|
||||||
|
|
||||||
|
- Store Request Network `requestId`, `paymentReference`, `securePaymentUrl`, `token`, `merchantReference`, `network`, `invoiceCurrency`, and `paymentCurrency`.
|
||||||
|
- Use Secure Payment Pages for first launch to reduce frontend wallet/calldata complexity.
|
||||||
|
- Include Amanat purchase request ID and payment ID as merchant reference or metadata.
|
||||||
|
- Validate supported networks/currencies before creating payment links.
|
||||||
|
- Remove frontend dependence on SHKeeper-generated `walletAddress` for new provider flow.
|
||||||
|
|
||||||
|
Acceptance Criteria:
|
||||||
|
|
||||||
|
- Buyer can start payment and receive a Request Network hosted payment URL.
|
||||||
|
- Backend records internal payment as `pending` with Request Network identifiers.
|
||||||
|
- Webhook `payment.confirmed` reconciles only the matching internal payment.
|
||||||
|
- Amount, currency, provider request ID, and merchant reference are verified before setting `escrowState='funded'`.
|
||||||
|
- Late or duplicate webhook deliveries are idempotent.
|
||||||
|
|
||||||
|
Risk Assessment:
|
||||||
|
|
||||||
|
- Impact: High.
|
||||||
|
- Likelihood: Medium.
|
||||||
|
- Main risks: currency/network mismatch, hosted redirect UX breakage, buyer pays wrong amount, missing webhook.
|
||||||
|
- Mitigation: backend-only creation, strict metadata correlation, periodic fallback reconciliation using Request Network payment search, idempotency by delivery ID and request ID.
|
||||||
|
- Rollback: disable Request Network provider for new payments; keep pending Request Network payments visible with manual support workflow.
|
||||||
|
|
||||||
|
## PRD 3 - Funds Ledger and Escrow State Machine
|
||||||
|
|
||||||
|
Priority: P0
|
||||||
|
|
||||||
|
Goal: introduce internal funds management so payment, hold, release, refund, dispute, and fee states are auditable and provider-independent.
|
||||||
|
|
||||||
|
Scope:
|
||||||
|
|
||||||
|
- Add `FundsAccount` model keyed by purchase request/payment.
|
||||||
|
- Add immutable `LedgerEntry` model with provider references and actor/source.
|
||||||
|
- Add derived `FundsBalance` query/service.
|
||||||
|
- Define state transitions: `expected -> held -> releasable -> releasing -> released`, plus `refunded`, `partially_refunded`, `disputed`, `failed`.
|
||||||
|
- Prevent release/refund from reading raw `Payment.status` alone.
|
||||||
|
|
||||||
|
Acceptance Criteria:
|
||||||
|
|
||||||
|
- Every successful pay-in creates a ledger entry for gross paid amount.
|
||||||
|
- Platform fee, Request Network protocol fee, and net seller amount are represented explicitly.
|
||||||
|
- Release API checks ledger invariants before creating payout/refund instructions.
|
||||||
|
- Dispute hold blocks payout until resolved.
|
||||||
|
- Admin UI/payment detail view can show gross paid, fees, held, releasable, released, refunded.
|
||||||
|
|
||||||
|
Risk Assessment:
|
||||||
|
|
||||||
|
- Impact: Very High.
|
||||||
|
- Likelihood: Medium.
|
||||||
|
- Main risks: double-release, incorrect fee accounting, drift between Payment and ledger state.
|
||||||
|
- Mitigation: immutable entries, unique idempotency keys, transaction/session writes where Mongo supports them, reconciliation job, read-only migration report before enforcing ledger.
|
||||||
|
- Rollback: keep old payment fields as source of truth until ledger backfill passes; feature flag release enforcement.
|
||||||
|
|
||||||
|
## PRD 4 - Request Network Webhook and Reconciliation Service
|
||||||
|
|
||||||
|
Priority: P1
|
||||||
|
|
||||||
|
Goal: replace SHKeeper webhook and wallet monitor semantics with signed Request Network event processing and periodic reconciliation.
|
||||||
|
|
||||||
|
Scope:
|
||||||
|
|
||||||
|
- Add `/api/payment/request-network/webhook`.
|
||||||
|
- Verify raw body using `x-request-network-signature`.
|
||||||
|
- Store delivery ID, retry count, event type, request ID, payment reference, payload hash, and processed status.
|
||||||
|
- Support test webhooks via `x-request-network-test`.
|
||||||
|
- Add scheduled reconciliation using Request Network payment search/status APIs.
|
||||||
|
- Retire SHKeeper wallet monitor for new Request Network payments.
|
||||||
|
|
||||||
|
Acceptance Criteria:
|
||||||
|
|
||||||
|
- Invalid signatures are rejected in all environments except explicit local test mode.
|
||||||
|
- Duplicate delivery IDs are acknowledged without duplicating ledger entries.
|
||||||
|
- `payment.confirmed` and partial/over/failed states map to internal status consistently.
|
||||||
|
- Reconciliation job can repair missed webhook state without relying on buyer/frontend callbacks.
|
||||||
|
|
||||||
|
Risk Assessment:
|
||||||
|
|
||||||
|
- Impact: High.
|
||||||
|
- Likelihood: Medium.
|
||||||
|
- Main risks: raw body parsing mismatch, webhook downtime, event mapping mistakes.
|
||||||
|
- Mitigation: raw-body middleware scoped to webhook route, event fixture tests, dead-letter queue/table, operator replay endpoint.
|
||||||
|
- Rollback: pause webhook processor and rely on manual/admin reconciliation for Request Network records.
|
||||||
|
|
||||||
|
## PRD 5 - Release, Refund, and Payout Orchestration
|
||||||
|
|
||||||
|
Priority: P1
|
||||||
|
|
||||||
|
Goal: replace SHKeeper payout tasks and simulated marketplace release with auditable transaction instruction and confirmation flows.
|
||||||
|
|
||||||
|
Scope:
|
||||||
|
|
||||||
|
- Define release/refund service that consumes ledger balances, not raw request status.
|
||||||
|
- Generate Request Network payout instructions or direct admin wallet transaction instructions.
|
||||||
|
- Store unsigned transaction payloads, signer, submitted tx hash, confirmation status, and provider status.
|
||||||
|
- Add batch payout support as a later optimization after single release is stable.
|
||||||
|
- Require admin/operator authorization and dispute checks before instruction creation.
|
||||||
|
|
||||||
|
Acceptance Criteria:
|
||||||
|
|
||||||
|
- Seller payout requires verified seller wallet/payee destination.
|
||||||
|
- Release cannot occur if funds are unpaid, already released, refunded, or disputed.
|
||||||
|
- Transaction hash confirmation updates ledger only once.
|
||||||
|
- Admin can see pending release instructions and retry/cancel safely.
|
||||||
|
- Existing `/purchase-requests/:id/release-payment` simulated path is removed or hidden behind dev-only mode.
|
||||||
|
|
||||||
|
Risk Assessment:
|
||||||
|
|
||||||
|
- Impact: Very High.
|
||||||
|
- Likelihood: High.
|
||||||
|
- Main risks: Request Network payouts are not custodial magic; operator wallet still signs transactions, so process failure can leave funds pending.
|
||||||
|
- Mitigation: explicit release queue, two-step admin approval, signer audit trail, reconciliation against tx hash/provider status.
|
||||||
|
- Rollback: keep direct admin wallet payout as emergency fallback with mandatory ledger entry and reason code.
|
||||||
|
|
||||||
|
## PRD 6 - Frontend Checkout and Admin Migration
|
||||||
|
|
||||||
|
Priority: P1
|
||||||
|
|
||||||
|
Goal: update buyer payment, admin release, seller payout, and payment details UI for Request Network flows.
|
||||||
|
|
||||||
|
Scope:
|
||||||
|
|
||||||
|
- Replace `ShkeeperPayment` with provider-neutral `CryptoPayment`.
|
||||||
|
- Add `RequestNetworkPayment` redirect flow for secure payment pages.
|
||||||
|
- Keep `ShkeeperPayment` available only for legacy/unpaid SHKeeper records if needed.
|
||||||
|
- Replace `ShkeeperPayout` with release queue/admin payout UI.
|
||||||
|
- Payment detail page shows provider, request ID, payment reference, hosted link, transaction hashes, ledger balances, webhook/reconciliation status.
|
||||||
|
|
||||||
|
Acceptance Criteria:
|
||||||
|
|
||||||
|
- Buyer checkout no longer expects provider-generated `walletAddress` for Request Network payments.
|
||||||
|
- Admin release UI displays available/releasable funds and blocks unsafe release states.
|
||||||
|
- Seller payout status comes from internal release instruction/ledger, not SHKeeper task polling.
|
||||||
|
- Legacy SHKeeper labels are not shown for Request Network payments.
|
||||||
|
|
||||||
|
Risk Assessment:
|
||||||
|
|
||||||
|
- Impact: Medium-High.
|
||||||
|
- Likelihood: Medium.
|
||||||
|
- Main risks: confusing redirect UX, old Persian labels referencing SHKeeper, admin accidentally using old payout button.
|
||||||
|
- Mitigation: provider-specific copy, feature flag UI sections, legacy badge, clear payment detail diagnostics.
|
||||||
|
- Rollback: route checkout back to SHKeeper component via provider flag.
|
||||||
|
|
||||||
|
## PRD 7 - Data Migration and Legacy Decommission
|
||||||
|
|
||||||
|
Priority: P2
|
||||||
|
|
||||||
|
Goal: safely migrate historical SHKeeper payment records and phase out provider-specific code.
|
||||||
|
|
||||||
|
Scope:
|
||||||
|
|
||||||
|
- Backfill provider metadata namespace for existing SHKeeper payments.
|
||||||
|
- Create ledger entries for completed SHKeeper payments from trusted records.
|
||||||
|
- Mark historical records as `legacyProvider='shkeeper'`.
|
||||||
|
- Keep SHKeeper webhook active during webhook tail period.
|
||||||
|
- Remove SHKeeper wallet monitor, simple auto webhook, test callback routes, and payout routes after cutoff.
|
||||||
|
|
||||||
|
Acceptance Criteria:
|
||||||
|
|
||||||
|
- Migration produces counts for total, migrated, skipped, ambiguous, and failed records.
|
||||||
|
- No historical completed payment loses transaction hash/provider invoice/task metadata.
|
||||||
|
- Legacy SHKeeper webhook can still reconcile existing pending records until cutoff date.
|
||||||
|
- Decommission checklist includes env vars, docs, frontend labels, backend routes, and runbooks.
|
||||||
|
|
||||||
|
Risk Assessment:
|
||||||
|
|
||||||
|
- Impact: Medium.
|
||||||
|
- Likelihood: Medium.
|
||||||
|
- Main risks: ambiguous historical records, template checkout string IDs, mixed status meanings.
|
||||||
|
- Mitigation: dry-run migration, manual review bucket, no destructive migration, keep original metadata.
|
||||||
|
- Rollback: ledger backfill is additive; old records remain intact.
|
||||||
|
|
||||||
|
## Implementation Sequence
|
||||||
|
|
||||||
|
1. Add provider adapter and Request Network config without changing default provider.
|
||||||
|
2. Add funds ledger models/services and dry-run backfill report.
|
||||||
|
3. Implement Request Network secure pay-in flow behind feature flag.
|
||||||
|
4. Implement signed webhook receiver and reconciliation job.
|
||||||
|
5. Enable Request Network for limited new checkout cohort.
|
||||||
|
6. Add release/refund orchestration using ledger gates.
|
||||||
|
7. Migrate admin/frontend views.
|
||||||
|
8. Backfill legacy records.
|
||||||
|
9. Decommission SHKeeper once no active records depend on it.
|
||||||
|
|
||||||
|
## Open Decisions
|
||||||
|
|
||||||
|
- Use Request Network Secure Payment Pages first, or execute Request Network calldata directly in the existing wallet modal?
|
||||||
|
- Keep funds in a platform-controlled escrow wallet, pay sellers via admin/operator release, or route pay-ins directly to seller with only platform fee collection?
|
||||||
|
- Which chains/currencies are required for launch: BSC USDT parity with today, or Request Network supported stablecoin routes first?
|
||||||
|
- Should platform fee be paid by buyer, seller, or absorbed by Amanat?
|
||||||
|
- Does Amanat need crypto-to-fiat/offramp later, which adds KYC/payment detail requirements?
|
||||||
|
|
||||||
|
## Recommendation
|
||||||
|
|
||||||
|
Start with Secure Payment Pages and a platform escrow/payee destination controlled by Amanat. This best matches the current escrow mental model while reducing frontend transaction-building risk. Do not route pay-ins directly to sellers until dispute handling, refund logic, and service fee economics are fully redesigned.
|
||||||
|
|
||||||
Reference in New Issue
Block a user