Skip to content
Merged
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
24 changes: 24 additions & 0 deletions packages/api-gateway/src/__tests__/merchant-suspension.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { describe, it, expect, vi } from "vitest";
import { checkMerchantSuspension } from "../middleware/merchantCheck";

describe("Merchant Suspension Early Lifecycle Checks", () => {
it("should reject suspended merchants before handling heavy payloads or route handlers", async () => {
const mockRequest = {
headers: { "x-merchant-id": "merch_suspended_99" },
params: {},
} as any;

const mockReply = {
status: vi.fn().mockReturnThis(),
send: vi.fn(),
} as any;

await checkMerchantSuspension(mockRequest, mockReply);

// Verify a consistent 403 Forbidden payload is fired upfront
expect(mockReply.status).toHaveBeenCalledWith(403);
expect(mockReply.send).toHaveBeenCalledWith(expect.objectContaining({
code: "MERCHANT_SUSPENDED"
}));
});
});
9 changes: 9 additions & 0 deletions packages/api-gateway/src/app/merchant/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { checkMerchantSuspension } from "../../middleware/merchantCheck";

// When defining your route blocks, register it as a preHandler:
fastify.register(async function (merchantRoutes) {
merchantRoutes.addHook("preHandler", checkMerchantSuspension);

// Your route definitions here run safely *after* the suspension verification
merchantRoutes.get("/listings", async (req, res) => { /* ... */ });
});
31 changes: 31 additions & 0 deletions packages/api-gateway/src/middleware/merchantCheck.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { FastifyRequest, FastifyReply } from "fastify";
import { getMerchantStatusFromDb } from "@/lib/db"; // Adjust matching database or cache layer string

/**
* Early preHandler hook that intercepts requests before body parsing occurs.
* Rejects suspended merchants immediately to prevent resource consumption and information leaks.
*/
export async function checkMerchantSuspension(request: FastifyRequest, reply: FastifyReply) {
// Extract merchant identity context safely from headers or route parameters
const merchantId = (request.headers["x-merchant-id"] || request.params?.["merchantId"]) as string;

if (!merchantId) {
return; // Pass through if not a merchant-scoped route context
}

try {
const merchant = await getMerchantStatusFromDb(merchantId);

if (merchant && merchant.status === "SUSPENDED") {
// Return a consistent suspension error explicitly before heavy work / body parsing begins
return reply.status(403).send({
error: "Forbidden",
message: "Merchant account is suspended.",
code: "MERCHANT_SUSPENDED"
});
}
} catch (error) {
// Fail closed for safety if the lookup fails
return reply.status(500).send({ error: "Internal validation failure." });
}
}