From 4f0e3d89f60e208f350ddbd76626046d617ef4de Mon Sep 17 00:00:00 2001 From: FragginWagon Date: Thu, 10 Sep 2026 01:46:58 -0400 Subject: [PATCH] feat: enforce production pricing provider config --- apps/api/src/app/server.js | 11 ++++++++- .../providers/http-pricing-provider.js | 18 ++++++++++++++ .../application/pricing-provider.test.js | 24 ++++++++++++++++++- apps/worker/src/worker-runtime.js | 11 ++++++++- docs/DEPLOYMENT.md | 5 +++- docs/IMPLEMENTATION-STATUS.md | 1 + 6 files changed, 66 insertions(+), 4 deletions(-) diff --git a/apps/api/src/app/server.js b/apps/api/src/app/server.js index 64148ae..958cf3a 100644 --- a/apps/api/src/app/server.js +++ b/apps/api/src/app/server.js @@ -51,7 +51,10 @@ import { createProviderMappingRepository } from "../modules/pricing/repositories import { createProviderMappingService } from "../modules/pricing/services/provider-mapping-service.js"; import { createPricingProviderRegistry } from "../modules/pricing/providers/pricing-provider.js"; import { createFixturePricingProvider } from "../modules/pricing/providers/fixture-pricing-provider.js"; -import { createHttpPricingProvider } from "../modules/pricing/providers/http-pricing-provider.js"; +import { + assertProductionPricingProviderConfig, + createHttpPricingProvider, +} from "../modules/pricing/providers/http-pricing-provider.js"; import { createAdminExportService } from "../modules/exports/services/admin-export-service.js"; import { createInventoryExportJobRepository } from "../modules/exports/repositories/inventory-export-job-repository.js"; import { createSellerSettingsRepository } from "../modules/settings/repositories/seller-settings-repository.js"; @@ -70,6 +73,12 @@ function parsePort(value) { } async function startServer() { + if (process.env.NODE_ENV === "production") { + assertProductionPricingProviderConfig({ + fixtureEnabled: process.env.PRICING_FIXTURE_PROVIDER_ENABLED === "true", + endpoint: process.env.PRICING_HTTP_PROVIDER_ENDPOINT, + }); + } const imageStorageRoot = resolveProjectImageStorageRoot( process.env.IMAGE_STORAGE_ROOT ?? ".local/image-storage", import.meta.url, diff --git a/apps/api/src/modules/pricing/providers/http-pricing-provider.js b/apps/api/src/modules/pricing/providers/http-pricing-provider.js index b61d916..a3910bb 100644 --- a/apps/api/src/modules/pricing/providers/http-pricing-provider.js +++ b/apps/api/src/modules/pricing/providers/http-pricing-provider.js @@ -1,5 +1,23 @@ import { createPricingProvider } from './pricing-provider.js'; +export function assertProductionPricingProviderConfig({ fixtureEnabled, endpoint }) { + if (fixtureEnabled) { + throw new Error('The fixture pricing provider must be disabled in production.'); + } + if (typeof endpoint !== 'string' || endpoint.trim().length === 0) { + throw new Error('A production pricing provider endpoint is required.'); + } + let parsedUrl; + try { + parsedUrl = new URL(endpoint); + } catch { + throw new Error('The production pricing provider endpoint must be a valid HTTPS URL.'); + } + if (parsedUrl.protocol !== 'https:') { + throw new Error('The production pricing provider endpoint must use HTTPS.'); + } +} + export function createHttpPricingProvider({ id, endpoint, diff --git a/apps/api/tests/application/pricing-provider.test.js b/apps/api/tests/application/pricing-provider.test.js index 48b4872..a0d36c8 100644 --- a/apps/api/tests/application/pricing-provider.test.js +++ b/apps/api/tests/application/pricing-provider.test.js @@ -6,7 +6,10 @@ import { createPricingProviderRegistry, } from "../../src/modules/pricing/providers/pricing-provider.js"; import { createFixturePricingProvider } from "../../src/modules/pricing/providers/fixture-pricing-provider.js"; -import { createHttpPricingProvider } from "../../src/modules/pricing/providers/http-pricing-provider.js"; +import { + assertProductionPricingProviderConfig, + createHttpPricingProvider, +} from "../../src/modules/pricing/providers/http-pricing-provider.js"; test("registers pricing providers behind a validated adapter contract", async () => { const provider = createPricingProvider({ @@ -78,6 +81,25 @@ test("http provider rejects insecure or malformed external provider URLs", () => ); }); +test("requires a secure external provider in production", () => { + assert.throws( + () => assertProductionPricingProviderConfig({ fixtureEnabled: true, endpoint: "https://provider.example/prices" }), + /fixture pricing provider must be disabled/i, + ); + assert.throws( + () => assertProductionPricingProviderConfig({ fixtureEnabled: false, endpoint: "" }), + /endpoint is required/i, + ); + assert.throws( + () => assertProductionPricingProviderConfig({ fixtureEnabled: false, endpoint: "http://provider.example/prices" }), + /must use HTTPS/i, + ); + assert.doesNotThrow(() => assertProductionPricingProviderConfig({ + fixtureEnabled: false, + endpoint: "https://provider.example/prices", + })); +}); + test("http provider sends mapping context and normalizes its response", async () => { let request; const provider = createHttpPricingProvider({ diff --git a/apps/worker/src/worker-runtime.js b/apps/worker/src/worker-runtime.js index fe66a60..918ef0f 100644 --- a/apps/worker/src/worker-runtime.js +++ b/apps/worker/src/worker-runtime.js @@ -18,7 +18,10 @@ import { createPricingRefreshJobRepository } from "../../api/src/modules/pricing import { createPricingService } from "../../api/src/modules/pricing/services/pricing-service.js"; import { createPricingProviderRegistry } from "../../api/src/modules/pricing/providers/pricing-provider.js"; import { createFixturePricingProvider } from "../../api/src/modules/pricing/providers/fixture-pricing-provider.js"; -import { createHttpPricingProvider } from "../../api/src/modules/pricing/providers/http-pricing-provider.js"; +import { + assertProductionPricingProviderConfig, + createHttpPricingProvider, +} from "../../api/src/modules/pricing/providers/http-pricing-provider.js"; import { createInventoryItemRepository } from "../../api/src/modules/inventory/repositories/inventory-item-repository.js"; import { createInventoryItemService } from "../../api/src/modules/inventory/services/inventory-item-service.js"; import { createInventoryExportJobRepository } from "../../api/src/modules/exports/repositories/inventory-export-job-repository.js"; @@ -45,6 +48,12 @@ const pricingJobCleanupIntervalMs = Number.parseInt( ); async function runWorker() { + if (process.env.NODE_ENV === "production") { + assertProductionPricingProviderConfig({ + fixtureEnabled: process.env.PRICING_FIXTURE_PROVIDER_ENABLED === "true", + endpoint: process.env.PRICING_HTTP_PROVIDER_ENDPOINT, + }); + } const imageStorageRoot = resolveProjectImageStorageRoot( process.env.IMAGE_STORAGE_ROOT ?? ".local/image-storage", import.meta.url, diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index d848bfc..c4b7583 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -30,7 +30,10 @@ keeps database and Redis ports internal to the Compose network. Before starting, create a production `.env` containing real values for `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD`, `DATABASE_URL`, `REDIS_URL`, `SMTP_URL`, `SMTP_FROM`, and a stable base64-encoded 32-byte -`MFA_ENCRYPTION_KEY`. Do not use the fixture pricing provider in production. +`MFA_ENCRYPTION_KEY`. Set `PRICING_HTTP_PROVIDER_ENDPOINT` to the approved +provider's HTTPS endpoint and provide its token through +`PRICING_HTTP_PROVIDER_TOKEN` when required. Production startup rejects the +fixture provider, missing endpoints, and non-HTTPS endpoints. Start the stack and apply migrations with: diff --git a/docs/IMPLEMENTATION-STATUS.md b/docs/IMPLEMENTATION-STATUS.md index 79fc334..6203753 100644 --- a/docs/IMPLEMENTATION-STATUS.md +++ b/docs/IMPLEMENTATION-STATUS.md @@ -143,6 +143,7 @@ Remaining roadmap areas are approved external provider credentials/adapters, bro - No initial User/Seller is provisioned in the development database yet. Run `npm run provision:admin` after setting its required local `.env` variables. - Listings are partially implemented: draft creation, seller-scoped reads, publication gating, lifecycle writes, archive behavior, sold/archived inventory checks, and item-owned image metadata are complete; storage-backed processing remains. - The provider abstraction, fixture provider, HTTP provider adapter, Product mapping workflow, durable refresh queue, worker execution, polling contract, retry policy, and completed-job purge operation are implemented. Production readiness still requires approved external provider credentials and provider-specific mapping data. +- API and worker production startup now reject fixture pricing, missing provider endpoints, and non-HTTPS provider endpoints before opening runtime connections. - Pricing refresh requests now enqueue seller-scoped jobs and expose a safe `GET /api/v1/admin/jobs/:id` polling contract instead of doing provider work inline. - Product-level provider mappings now persist seller-scoped `PRODUCT`/`SEARCH` relationships and support active listing and controlled disabling. - Pricing refresh enqueue now resolves its provider from an active Product mapping and rejects unmapped Products explicitly.