Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/PROJECT_STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@ Local `main` in the main clone is at `5693f7b`, an older Production commit that
| #17 Production release: free-only MVP | `main` ← `launch-mvp` | `83a30a7` | `lint, unit, e2e` ✅ (push + PR) · Vercel ✅ · Vercel Preview Comments ✅ | [41564501659](https://github.com/Jakuraa/StrangersMeetup/actions/runs/41564501659) (push) success |
| #30 M4.3 UPI Security Remediation | `launch-mvp` ← `feature/m4-3-upi-security-remediation` | `e4babe6` | `lint, unit, e2e` ✅ | PR Review Baseline for M4.3.1 |
| #31 M4.3.1 Review Remediation | `feature/m4-3-upi-security-remediation` ← `feature/m4-3-1-review-remediation` | `03bf518` | `lint, unit, e2e` ✅ | Independent Review Corrections for PR #30 |
| (Remediation) M4.3.2 Financial Integrity Remediation | `feature/m4-3-1-review-remediation` ← `feature/m4-3-2-financial-integrity` | (work branch) | `lint, unit, e2e` ✅ | Fail-Closed Merchant Identity, Secure Manual Reconciliation & State Transition Verification |
| #32 M4.3.2 Financial Integrity Remediation | `feature/m4-3-1-review-remediation` ← `feature/m4-3-2-financial-integrity` | `62cb0a3` | `lint, unit, e2e` ✅ · Vercel ✅ | Fail-Closed Merchant Identity & Secure Manual Refund Reconciliation |
| (Review) M4.3.3 Final Provider Verification Integrity | `feature/m4-3-2-financial-integrity` ← `feature/m4-3-3-provider-verification` | (work branch) | `lint, unit, e2e` ✅ | Removal of Currency Defaults, Elimination of Merchant Fallback, Authentic S2S Polling & Attestation |

**Owner gate:** PR #17 must not be merged without the owner's separate approval. `main` has no GitHub Actions runs on record. PR #30, PR #31, and PR #17 must not be merged automatically without owner review. Keep `ENABLE_LIVE_PAYMENTS=false` everywhere.
**Owner gate:** PR #17 must not be merged without the owner's separate approval. `main` has no GitHub Actions runs on record. PR #32, PR #31, PR #30, and PR #17 must not be merged without separate owner approval. Keep `ENABLE_LIVE_PAYMENTS=false` everywhere.

PR #23 is **merged** at `7c5079e` (P3 Location Intelligence + M2 Remediation).
PR #25 is **merged** at `83a30a7` (Owner Addendum Ops: dynamic DB block list, branch archiving, Phase 5.6/5.7 reports).
Expand All @@ -46,6 +47,7 @@ PR #28 is **merged** at `c3b4e83` (M4.1 UPI-First Checkout, Dynamic QR & Verifie
PR #29 is **merged** at `c71ec8d` (M4.2 Independent UPI Payment Security & Production Readiness Audit).
PR #30 is **open / preserved** (`feature/m4-3-upi-security-remediation`).
PR #31 is **open / preserved** (`feature/m4-3-1-review-remediation`).
PR #32 is **open / preserved** (`feature/m4-3-2-financial-integrity`).

## 4. Migrations per environment

Expand Down
9 changes: 6 additions & 3 deletions docs/UPI_PROVIDER_READINESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,14 @@ flowchart TD

### Readiness Status:

#### A. Code & Architectural Prerequisites (Engineering — Completed in M4.3, M4.3.1 & M4.3.2)
#### A. Code & Architectural Prerequisites (Engineering — Completed in M4.3, M4.3.1, M4.3.2 & M4.3.3)
- [x] **Fix Webhook Signature Calculation**: Updated `verifyWebhookSignature` in `lib/upi/adapter.js` to compute `SHA256(response + saltKey) + "###" + saltIndex` on the decoded payload object. Verified with PhonePe S2S test.
- [x] **Strict Webhook Amount Verification (REV-04)**: Implemented in `applySuccessfulUpiPayment` and `processUpiWebhook`; requires safe positive integer exact paise match; currency and merchantId match; underpayment or malformed amounts are marked `disputed` with 0 bookings created.
- [x] **Fail-Closed Merchant Identity & Currency Invariance (M4.3.2)**: Replaced permissive merchant logic with strict fail-closed verification across verified, stored, and server-configured merchant IDs. Rejects missing, null, empty string, or mismatched merchant IDs across PhonePe PG and mock adapters. Enforces non-empty `'INR'` currency matching.
- [x] **Secure Manual Refund Reconciliation (M4.3.2)**: Prohibited generated or placeholder `MANUAL-...` references. Implemented `verifyFinanceAuthorization` checking active database roles (`super_admin`, `finance`), `isValidExternalRefundRef` requiring verifiable external references (bank UTR, PG refund ARN), and `isValidEvidenceRef` requiring documented evidence references recorded in the immutable audit ledger. Documented operational CBS limitation.
- [x] **Fail-Closed Merchant Identity & Currency Invariance (M4.3.2 & M4.3.3)**: Replaced permissive merchant logic with strict fail-closed verification across verified, stored, and server-configured merchant IDs. Rejects missing, null, empty string, or mismatched merchant IDs across PhonePe PG and mock adapters. Enforces non-empty `'INR'` currency matching.
- [x] **Elimination of Currency Defaults & Synthetic Fallbacks (M4.3.3)**: Removed `verified.currency || 'INR'` in webhook processing and `gatewayStatus.currency || 'INR'` in polling. Missing currency remains missing (`null`) across all adapters. Never manufactures INR as proof of provider verification.
- [x] **Elimination of Configured Merchant Fallback in Status Check (M4.3.3)**: Removed `data.merchantId || this.merchantId` fallback in `PhonePeAdapter.getPaymentStatus`. Provider status responses missing `merchantId` remain `null`, preventing synthetic identity substitution. Three-way comparison in `applySuccessfulUpiPayment` fails closed with `missing_verified_merchant_id` or `merchant_mismatch`.
- [x] **Provider API Limitations Disclosed (M4.3.3)**: Documented that PhonePe domestic PG typically omits an explicit `currency` field in standard v1 responses; automated confirmation strictly requires explicit provider currency and merchant identity.
- [x] **Secure Manual Refund Reconciliation & Operator Attestation (M4.3.2 & M4.3.3)**: Prohibited generated or placeholder `MANUAL-...` references. Implemented `verifyFinanceAuthorization` checking active database roles (`super_admin`, `finance`), `isValidExternalRefundRef` requiring verifiable external references (bank UTR, PG refund ARN), and `isValidEvidenceRef` requiring documented evidence references recorded in the immutable audit ledger. Documented operational CBS limitation and labeled attestation as `operator_attested: true`, `bankVerified: false`.
- [x] **Enforce Authentication on `/checkout/:slug/upi`**: Gated unauthenticated users with redirect to `/signin`; guest checkout assigns guestToken and requires authenticated claim via `/api/checkout/upi/claim`.
- [x] **Duplicate Booking Protection (REV-02)**: Prevented duplicate active bookings during intent creation (409 `already_booked`), confirmation locked transaction (queued for refund in `refund_pending`), guest claims (409), and database constraint races (SQLSTATE `23505` savepoint recovery).
- [x] **Refund Operational Recovery & Reconciliation (REV-03, M4.3.2)**: Permitted refunding payments in both `succeeded` and `refund_pending` states; isolated external gateway calls outside holding transactions; caught provider failures without losing audit state; added authorized manual finance reconciliation mechanism; ensured booking revocation and chat removals fire only upon confirmed refund.
Expand Down
50 changes: 50 additions & 0 deletions docs/UPI_SECURITY_AUDIT.md
Original file line number Diff line number Diff line change
Expand Up @@ -349,3 +349,53 @@ Following independent owner review of PR #31 (`feature/m4-3-1-review-remediation
- **Migration Suite**: 8/8 passed (`test/migrations.test.js`).
- **Linter**: `140/140 files OK` (`npm run lint`).
- **Absolute Safety Boundary**: `ENABLE_LIVE_PAYMENTS=false` preserved across all files and environments. Real money transactions remain 100% disabled. Merges to PR #31, PR #30, and PR #17 strictly prohibited.

---

## 8. Milestone M4.3.3 Final Provider Verification Integrity Review

Milestone M4.3.3 implements a final zero-trust review and remediation of provider verification paths, eliminating implicit defaults, silent identity fallbacks, and unverified confirmations across PhonePe PG and mock adapters on branch `feature/m4-3-3-provider-verification`.

### 1. Removal of Currency Defaults Across Verification Paths
- **Eliminated Permissive Defaults**: Removed all instances of fallback currency defaults (`verified.currency || 'INR'` in `processUpiWebhook` and `gatewayStatus.currency || 'INR'` in `getUpiPaymentStatus`).
- **Faithful Provider Currency Representation**: Updated `PhonePeAdapter.getPaymentStatus`, `PhonePeAdapter.verifyWebhook`, `MockUpiAdapter.getPaymentStatus`, and `MockUpiAdapter.verifyWebhook` so that missing or omitted currency from the provider response strictly remains missing (`null`), and empty string remains empty string. Never manufactures `'INR'` as proof of provider verification.
- **Strict Currency Invariance in Confirmation**: `applySuccessfulUpiPayment` fail-closed currency verification requires that:
- Incoming provider currency is a non-empty string.
- Stored database payment currency is `'INR'`.
- Both values strictly equal `'INR'` (`safeEqual` / constant-time match).
- Missing or null currency triggers dispute reason `missing_currency`.
- Non-INR, lowercase, or mismatched currency triggers dispute reason `currency_mismatch`.
- In either failure case, the payment is moved to `status = 'disputed'`, failure reason records `Payment verification failed: Currency mismatch (<reason>)`, an immutable entry is logged in `payment_audits`, and 0 attendee bookings or Telegram invite links are created.

### 2. Removal of Merchant Identity Fallback in Provider Status Verification
- **Eliminated Fallback in `PhonePeAdapter.getPaymentStatus`**: Replaced `const statusMerchantId = (data.merchantId || this.merchantId || '').trim();` with strict extraction `data.merchantId !== undefined && data.merchantId !== null ? (typeof data.merchantId === 'string' ? data.merchantId.trim() : data.merchantId) : null;`. If the gateway response omits `merchantId`, it remains `null` and is never substituted with server configuration.
- **Fail-Closed Three-Way Merchant Verification**: When polling gateway status (`checkGateway: true`), the returned status is passed to `applySuccessfulUpiPayment`, which enforces constant-time equality across:
1. `verifiedMerchantId`: Merchant ID returned in the provider status response.
2. `storedMerchantId`: Merchant ID recorded during intent creation in `payments.merchant_id`.
3. `configuredMerchantId`: Merchant ID loaded from server adapter configuration (`adapter.merchantId`).
- Missing or empty provider merchant ID records `missing_verified_merchant_id`.
- Tampered or missing stored merchant ID records `missing_stored_merchant_id`.
- Mismatched provider merchant ID records `merchant_mismatch`.
- Any failure immediately disputes the payment with 0 confirmed bookings.
- **Provider API Limitation Disclosure**: Official PhonePe v1 Check Status API (`/pg/v1/status/{merchantId}/{orderId}`) and standard S2S webhook payloads are designed for domestic INR processing and typically omit an explicit `currency` field. If PhonePe omits `currency` or `merchantId` in standard callbacks, fail-closed enforcement prevents automatic confirmation. Authoritative verification relies on explicit provider fields; synthetic or manufactured fields are strictly disallowed.

### 3. Review of Provider Status Authenticity & Safety Invariants
- **Server-to-Server Authenticity**: Payment status polling executes strictly through authenticated server-to-server requests using PhonePe's HMAC/SHA256 `X-VERIFY` header (`SHA256(endpoint + saltKey)###saltIndex`) and `X-MERCHANT-ID`.
- **Cryptographic Signature Verification**: Incoming webhooks are verified against `X-VERIFY` (`SHA256(base64Response + saltKey)###saltIndex`) before reading payload contents.
- **Atomic Concurrency Guard**: Amount (positive safe integer paise match), order ID, merchant identity (three-way match), and currency (INR match) are checked atomically inside a locked PostgreSQL transaction (`SELECT ... FOR UPDATE`).
- **Client Return URL & Browser Callback Isolation**: Browser navigations to `/checkout/:slug/upi` or client polling endpoints (`/api/checkout/upi/status/:id`) never establish payment success. Spoofed query parameters (e.g. `?status=succeeded`) are completely ignored. Polling without `check=1` only reads existing database status.
- **Idempotency**: Repeated webhook deliveries or status poll queries on an already `succeeded` order return duplicate status without modifying database rows or creating duplicate bookings.
- **Booking & Invite Gating**: Pending, failed, and disputed payments never create confirmed bookings or Telegram invites.

### 4. Manual Refund Operational Limitation & Attestation
- **CBS Limitation Documented**: String validation in `isValidExternalRefundRef` enforces syntactical correctness (6–120 chars, alphanumeric syntax, anti-placeholder rules), but does **not** interface with Core Banking System (CBS) host-to-host settlement rails.
- **Operator-Attested Labeling**: All manual refund reconciliations are explicitly tagged with `attestation: 'operator_attested'` and `bankVerified: false` in `payments.raw_ref.reconciliation`, `payment_audits.details`, and audit logs.
- **Server-Side Authorization**: Re-verified against active `super_admin` or `finance` roles in `admin_users` table; caller strings are never trusted. Public users and support roles are rejected (401/403).
- **Anti-Fraud Rules**: Fake, placeholder, single-character repetition (`000000`, `XXXXXX`), and trivial sequences (`123456`, `abcdef`) are strictly rejected (400).

### M4.3.3 Verification Summary
- **Total Test Suite**: 472 tests total, 470 passed, 2 skipped (unconfigured Stars sandbox), 0 failed (`duration: ~51s`).
- **UPI Checkout Suite**: 37/37 passed (`test/upi-checkout.test.js`).
- **Migration Suite**: 8/8 passed (`test/migrations.test.js`).
- **Linter**: `140/140 files OK` (`npm run lint`).
- **Absolute Safety Boundary**: `ENABLE_LIVE_PAYMENTS=false` preserved across all files and environments. No real payments or refunds initiated. Merges to PR #32, PR #31, PR #30, and PR #17 strictly prohibited. Stopped for independent owner review.
28 changes: 21 additions & 7 deletions lib/services/upi-payments.js
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,8 @@ export async function getUpiPaymentStatus(orderId, { user = null, guestToken = n
if (!user) {
throw new HttpError(401, 'unauthorized', 'Authentication required to view payment status');
}
if (payment.user_id !== user.id && !isStaff) {
const currentUserId = user.id || user.userId;
if (payment.user_id !== currentUserId && !isStaff) {
throw new HttpError(403, 'forbidden', 'Access denied to this payment order');
}
} else {
Expand Down Expand Up @@ -402,7 +403,7 @@ export async function getUpiPaymentStatus(orderId, { user = null, guestToken = n
orderId,
providerPaymentId: gatewayStatus.providerPaymentId,
verifiedAmountPaise: gatewayStatus.amountPaise,
currency: gatewayStatus.currency || 'INR',
currency: gatewayStatus.currency,
merchantId: gatewayStatus.merchantId,
actor: 'poll:gateway'
});
Expand Down Expand Up @@ -508,13 +509,18 @@ export async function applySuccessfulUpiPayment({
disputeReason
});

let failureLabel = 'Verification mismatch';
if (disputeReason.includes('amount')) failureLabel = 'Amount mismatch';
else if (disputeReason.includes('currency')) failureLabel = 'Currency mismatch';
else if (disputeReason.includes('merchant')) failureLabel = 'Merchant mismatch';

await c.query(
`UPDATE payments SET
status = 'disputed',
failure_reason = 'Payment verification failed: Amount mismatch (' || $2 || ')',
failure_reason = 'Payment verification failed: ' || $3 || ' (' || $2 || ')',
webhook_verified_at = now()
WHERE id = $1`,
[payment.id, disputeReason]
[payment.id, disputeReason, failureLabel]
);

await c.query(
Expand Down Expand Up @@ -955,7 +961,7 @@ export async function processUpiWebhook({ headers, rawBody, clientIp }) {
providerPaymentId: verified.providerPaymentId,
verifiedAmountPaise: verified.amountPaise,
merchantId: verified.merchantId,
currency: verified.currency || 'INR',
currency: verified.currency,
actor: `webhook:${verified.provider}`
});
if (res.status === 'disputed') {
Expand Down Expand Up @@ -1149,6 +1155,8 @@ export async function refundUpiOrder(orderId, {
effectiveRef,
JSON.stringify({
manualReconciliation: true,
attestation: 'operator_attested',
bankVerified: false,
externalRefundRef: effectiveRef,
evidenceRef,
reason,
Expand All @@ -1168,6 +1176,8 @@ export async function refundUpiOrder(orderId, {
verifiedActor,
JSON.stringify({
reconciliationType: 'manual_finance_reconciliation',
attestation: 'operator_attested',
bankVerified: false,
externalRefundRef: effectiveRef,
evidenceRef,
reason,
Expand Down Expand Up @@ -1208,7 +1218,9 @@ export async function refundUpiOrder(orderId, {
reason,
externalRefundRef: effectiveRef,
evidenceRef,
manualReconciliation: true
manualReconciliation: true,
attestation: 'operator_attested',
bankVerified: false
},
c
);
Expand All @@ -1217,7 +1229,9 @@ export async function refundUpiOrder(orderId, {
orderId,
status: 'refunded',
refundId: effectiveRef,
manualReconciliation: true
manualReconciliation: true,
attestation: 'operator_attested',
bankVerified: false
};
});
}
Expand Down
Loading
Loading