diff --git a/services/platform-service/src/lib/cosmos-init.ts b/services/platform-service/src/lib/cosmos-init.ts index 0e87c2b1..a49377bf 100644 --- a/services/platform-service/src/lib/cosmos-init.ts +++ b/services/platform-service/src/lib/cosmos-init.ts @@ -158,6 +158,30 @@ const CONTAINER_DEFS: Record = { surveys: { partitionKeyPath: '/productId' }, survey_responses: { partitionKeyPath: '/surveyId', defaultTtl: 365 * 86400 }, user_survey_states: { partitionKeyPath: '/userId', defaultTtl: 90 * 86400 }, + // CDN Pipeline (P2) + cdn_assets: { partitionKeyPath: '/productId' }, + cdn_purge_requests: { partitionKeyPath: '/productId', defaultTtl: 90 * 86400 }, + cdn_origin_configs: { partitionKeyPath: '/productId' }, + // Full-Text Search (P2) + search_index: { partitionKeyPath: '/productId' }, + search_suggestions: { partitionKeyPath: '/productId', defaultTtl: 180 * 86400 }, + // Billing Dunning (P2) + dunning_campaigns: { partitionKeyPath: '/productId' }, + dunning_policies: { partitionKeyPath: '/productId' }, + // Multi-Tenant (P3) + tenants: { partitionKeyPath: '/productId' }, + tenant_members: { partitionKeyPath: '/tenantId' }, + tenant_invites: { partitionKeyPath: '/tenantId', defaultTtl: 7 * 86400 }, + // Data Retention (P3) + retention_policies: { partitionKeyPath: '/productId' }, + retention_jobs: { partitionKeyPath: '/productId', defaultTtl: 365 * 86400 }, + // Backup/Restore (P3) + backups: { partitionKeyPath: '/productId' }, + restores: { partitionKeyPath: '/productId' }, + backup_configs: { partitionKeyPath: '/productId' }, + // API Versioning (P3) + api_versions: { partitionKeyPath: '/productId' }, + api_version_pins: { partitionKeyPath: '/productId' }, }; export async function initCosmosIfNeeded(): Promise { diff --git a/services/platform-service/src/server.ts b/services/platform-service/src/server.ts index 75f81d7e..004286b5 100644 --- a/services/platform-service/src/server.ts +++ b/services/platform-service/src/server.ts @@ -91,9 +91,16 @@ import { marketplaceRoutes } from './modules/marketplace/routes.js'; import { predictiveAnalyticsRoutes } from './modules/predictive-analytics/routes.js'; import { onboardingRoutes } from './modules/onboarding/routes.js'; import { billingCheckoutRoutes } from './modules/billing-checkout/routes.js'; +import { cdnRoutes } from './modules/cdn/routes.js'; +import { searchRoutes } from './modules/search/routes.js'; +import { dunningRoutes } from './modules/dunning/routes.js'; +import { tenantRoutes } from './modules/tenants/routes.js'; +import { retentionRoutes } from './modules/retention/routes.js'; +import { backupRoutes } from './modules/backups/routes.js'; +import { apiVersioningRoutes } from './modules/api-versioning/routes.js'; import { initCosmosIfNeeded } from './lib/cosmos-init.js'; import { config } from './lib/config.js'; -import type { JwtPayload } from './lib/request-context.js'; +import { type JwtPayload, extractProductIdAsync } from './lib/request-context.js'; import { seedDefaultFlags } from './modules/flags/seed.js'; import { runPendingMigrations } from './migrations/runner.js'; import { registerDiagnosticsSubscribers } from './modules/diagnostics/subscribers.js'; @@ -133,6 +140,19 @@ await registerOptionalJwtContext(app, { }); await registerOptionalApiKeyContext(app); +// Pre-resolve auto-registration for unknown products (Phase 4.1). +// Runs after JWT parsing so extractProductIdAsync can check jwtPayload. +// On success the product is in the cache before sync getRequestProductId runs. +app.addHook('onRequest', async req => { + // Only attempt on /api/ routes that carry a productId signal + if (!req.url.startsWith('/api/')) return; + try { + await extractProductIdAsync(req); + } catch { + // Swallow — the route's own getRequestProductId will throw a proper error + } +}); + // Register route modules await app.register(productRoutes, { prefix: '/api' }); await app.register(authRoutes, { prefix: '/api' }); @@ -231,6 +251,15 @@ await app.register(billingCheckoutRoutes, { prefix: '/api' }); // Broadcast Messaging & Surveys (see docs/roadmaps/not-started/platform_BROADCAST_SURVEY_ROADMAP.md) await app.register(broadcastRoutes, { prefix: '/api' }); await app.register(surveyRoutes, { prefix: '/api' }); +// P2 — CDN Pipeline, Full-Text Search, Billing Dunning +await app.register(cdnRoutes, { prefix: '/api' }); +await app.register(searchRoutes, { prefix: '/api' }); +await app.register(dunningRoutes, { prefix: '/api' }); +// P3 — Multi-Tenant, Data Retention, Backup/Restore, API Versioning +await app.register(tenantRoutes, { prefix: '/api' }); +await app.register(retentionRoutes, { prefix: '/api' }); +await app.register(backupRoutes, { prefix: '/api' }); +await app.register(apiVersioningRoutes, { prefix: '/api' }); // Register event bus subscribers registerDiagnosticsSubscribers(app.log);