diff --git a/.gitignore b/.gitignore index aed438a..da328b7 100644 --- a/.gitignore +++ b/.gitignore @@ -107,3 +107,19 @@ playwright/.auth/ # Playwright screenshots (generated during e2e runs) **/e2e/screenshots/ + +# Miscellaneous temporary / backup files +*.orig +*.bak +*.rej +.node_repl_history + +# OS-specific +Thumbs.db:encryptable +ehthumbs.db +Desktop.ini + +# Python bytecode (scripts/) +__pycache__/ +*.py[cod] +*.pyo diff --git a/comebackhere-backend/src/routes/compliance.ts b/comebackhere-backend/src/routes/compliance.ts index 730ca6a..838643b 100644 --- a/comebackhere-backend/src/routes/compliance.ts +++ b/comebackhere-backend/src/routes/compliance.ts @@ -13,6 +13,7 @@ import { asyncHandler } from "../lib/errors.js" import { requireAdmin } from "../middleware/adminAuth.js" import { allowBodySchema, blockBodySchema, complianceAuditQuerySchema } from "../schemas/index.js" import { connectMongo, getComplianceAuditCollection } from "../db/mongo.js" +import { cacheDelete } from "../lib/cache.js" const router = Router() @@ -206,6 +207,8 @@ router.post("/allow", requireAdmin, validateBody(allowBodySchema), asyncHandler( env.signerSecret, env.networkPassphrase ) + // #893 — evict any stale compliance status cache entry for this address + await cacheDelete(`compliance:status:${address}`) res.status(200).json(result) })) @@ -243,6 +246,8 @@ router.post("/block", requireAdmin, validateBody(blockBodySchema), asyncHandler( env.signerSecret, env.networkPassphrase ) + // #893 — evict any stale compliance status cache entry for this address + await cacheDelete(`compliance:status:${address}`) res.status(200).json(result) })) diff --git a/comebackhere-backend/src/routes/invoices.ts b/comebackhere-backend/src/routes/invoices.ts index d728079..4c52280 100644 --- a/comebackhere-backend/src/routes/invoices.ts +++ b/comebackhere-backend/src/routes/invoices.ts @@ -1,11 +1,12 @@ import { Router, type Request, type Response } from "express" import type { FindCursor, WithId } from "mongodb" import { Keypair, TransactionBuilder, BASE_FEE, Contract, nativeToScVal, SorobanRpc, xdr } from "stellar-sdk" -import { connectMongo, getInvoiceEventsCollection, getInvoicesCollection, type InvoiceRecord, type InvoiceStatus } from "../db/mongo.js" +import { connectMongo, getComplianceAuditCollection, getInvoiceEventsCollection, getInvoicesCollection, type InvoiceRecord, type InvoiceStatus } from "../db/mongo.js" import { requireEnv } from "../lib/env.js" -import { asyncHandler, NotFoundError } from "../lib/errors.js" +import { asyncHandler, ForbiddenError, NotFoundError } from "../lib/errors.js" import { cacheGet, cacheSet } from "../lib/cache.js" import { validateBody, validateParams, validateQuery } from "../middleware/validate.js" +import { requireMerchantApiKey } from "../middleware/apiKey.js" import { createInvoiceSchema, invoiceIdParamSchema, invoiceListQuerySchema } from "../schemas/index.js" const router = Router() @@ -715,6 +716,17 @@ router.post("/", requireMerchantApiKey, validateBody(createInvoiceSchema), async const client = buildSorobanClient(env.rpcUrl) const body = { ...req.body, merchant_address: res.locals.merchantAddress } as CreateInvoiceBody + + // #892 — reject invoice creation for compliance-blocked addresses + const complianceDb = await connectMongo() + const latestAudit = await getComplianceAuditCollection(complianceDb).findOne( + { address: body.merchant_address }, + { sort: { ledger: -1, _id: -1 } }, + ) + if (latestAudit && latestAudit.event_type === "address_blocked") { + throw new ForbiddenError("Merchant address has been blocked by compliance") + } + const result = await createInvoice( body, client, diff --git a/comebackhere-backend/src/routes/treasury.ts b/comebackhere-backend/src/routes/treasury.ts index 5929ffe..7da3e0c 100644 --- a/comebackhere-backend/src/routes/treasury.ts +++ b/comebackhere-backend/src/routes/treasury.ts @@ -19,6 +19,7 @@ import { escalateHoldSchema, } from "../schemas/index.js" import { getBalanceCache, setBalanceCache, invalidateBalanceCache } from "../lib/cache.js" +import { logger } from "../lib/logger.js" const router = Router() @@ -211,7 +212,7 @@ export async function executeSettlementWithBalanceCheck( env.networkPassphrase, ) - res.locals.logger.info( + logger.info( { settlementId: body.settlement_id, required: settlement.amount.toString(), @@ -327,20 +328,22 @@ router.post("/execute-settlement", requireAdmin, validateBody(executeSettlementS const { settlement_id: settlementId, token_contract } = req.body as { settlement_id: number; token_contract?: string } - try { - const result = await executeSettlementWithBalanceCheck( - { settlement_id: settlementId, token_contract }, - env, - ) - // #212 — balance changed; evict the cache so the next GET /balances is fresh - invalidateBalanceCache(`execute-settlement id=${settlementId}`) - res.json(result) - } catch (err: unknown) { - const status = (err as { status?: number })?.status ?? 500 - const message = err instanceof Error ? err.message : String(err) - res.status(status).json({ error: message }) - } -}) + const result = await executeSettlementWithBalanceCheck( + { settlement_id: settlementId, token_contract }, + env, + ) + + // #891 — mark the settlement as Executed now that the ledger has confirmed it + const database = await connectMongo() + await getSettlementsCollection(database).updateOne( + { id: settlementId }, + { $set: { status: "Executed", executed_tx_hash: result.tx_hash, updated_at: new Date() } }, + ) + + // #212 — balance changed; evict the cache so the next GET /balances is fresh + invalidateBalanceCache(`execute-settlement id=${settlementId}`) + res.json(result) +})) export interface SimulateSettlementBody { settlement_id: number