diff --git a/services/platform-service/src/lib/request-context.ts b/services/platform-service/src/lib/request-context.ts new file mode 100644 index 00000000..8cad4ed7 --- /dev/null +++ b/services/platform-service/src/lib/request-context.ts @@ -0,0 +1,71 @@ +/** + * Request-level product context helpers. + * + * Extract and validate productId from every request: + * 1. JWT token payload (authenticated requests) + * 2. X-Product-Id header (unauthenticated requests) + * 3. PRODUCT_ID env var fallback (backward compat during migration) + * + * Validates against the products registry — rejects unknown or disabled products. + */ + +import type { FastifyRequest } from 'fastify'; +import { BadRequestError } from './errors.js'; +import { isValidProduct, getProduct } from '../modules/products/cache.js'; +import type { ProductDoc } from '../modules/products/types.js'; + +/** JWT payload shape attached to req by the onRequest hook (see Commit 3). */ +export interface JwtPayload { + sub: string; + email?: string; + role?: string; + productId?: string; + type?: string; +} + +// Augment Fastify request to include parsed JWT payload +declare module 'fastify' { + interface FastifyRequest { + jwtPayload?: JwtPayload; + } +} + +/** + * Extract and validate productId from the request. + * Priority: JWT token > X-Product-Id header > env fallback (dev only). + * Rejects unknown or disabled products. + */ +export function getRequestProductId(req: FastifyRequest): string { + // 1. From JWT (set during login/register, attached by onRequest hook) + let id = req.jwtPayload?.productId; + + // 2. From header (unauthenticated requests like license activation) + if (!id) { + const header = req.headers['x-product-id']; + if (typeof header === 'string' && header.length > 0) id = header; + } + + // 3. Fallback to env var (backward compat during migration, dev only) + if (!id) { + const envFallback = process.env.PRODUCT_ID; + if (envFallback) id = envFallback; + } + + if (!id) throw new BadRequestError('productId is required (via JWT or X-Product-Id header)'); + + // Validate against product registry + if (!isValidProduct(id)) throw new BadRequestError(`Unknown product: ${id}`); + const product = getProduct(id)!; + if (product.status === 'disabled') throw new BadRequestError(`Product ${id} is disabled`); + + return id; +} + +/** + * Get the full product config for the current request's productId. + * Useful when modules need product-specific values (trial days, device limits, etc.). + */ +export function getRequestProductConfig(req: FastifyRequest): ProductDoc { + const id = getRequestProductId(req); + return getProduct(id)!; +}