learning_ai_common_plat/services/platform-service/src/server.ts
saravanakumardb1 9d405952e2 fix(platform-service): TODO-4 \u2014 typed cast for request.auth augmentation
The DevOps admin preHandler read 'auth' as '(request as any).auth'.
The proper Fastify pattern is 'declare module' augmentation in
@bytelyst/fastify-auth, but the inline cast through 'unknown' is
sufficient for now and avoids touching the shared auth package.

Changed:
  - 'const auth = (request as any).auth;' \u2192
    'const auth = (request as unknown as { auth?: { role?: string } }).auth;'

Inline comment notes the cleaner 'declare module' alternative.

Final ecosystem state:
  scripts/check-rule-violations.sh: 0 findings across all rules \u2713

  web-hardcoded-hex:         0  \u2713
  b5-hardcoded-product-id:   0  \u2713
  b4-console-log:            0  \u2713
  b4-swift-print:            0  \u2713
  b4-python-print:           0  \u2713
  ts-any-type:               0  \u2713
  b7-emoji-in-code:          0  \u2713
2026-05-23 19:29:26 -07:00

361 lines
18 KiB
TypeScript

/**
* Platform Service — Fastify server entry point.
*
* Modules: auth, audit, notifications, feature flags, blob,
* invitations, referrals, promos (merged from growth-service),
* subscriptions, usage, plans, licenses, stripe (merged from billing-service),
* items, comments, votes, public (merged from tracker-service).
* Port: 4003 (configurable via PORT env var).
*/
// Resolve secrets from configured provider BEFORE config parsing
import { resolveSecrets, LYSNR_SECRETS } from '@bytelyst/config';
import {
collectDevopsInfo,
getBuildInfo,
httpDependencyCheck,
readServiceVersion,
} from '@bytelyst/devops/server';
await resolveSecrets([
LYSNR_SECRETS.COSMOS_KEY,
LYSNR_SECRETS.COSMOS_ENDPOINT,
LYSNR_SECRETS.JWT_SECRET,
LYSNR_SECRETS.STRIPE_SECRET_KEY,
LYSNR_SECRETS.STRIPE_WEBHOOK_SECRET,
LYSNR_SECRETS.AZURE_BLOB_CONNECTION_STRING,
LYSNR_SECRETS.AZURE_BLOB_ACCOUNT_KEY,
]);
import { createServiceApp, registerOptionalJwtContext, startService } from '@bytelyst/fastify-core';
import { productRoutes } from './modules/products/routes.js';
import { loadProductCache } from './modules/products/cache.js';
import { authRoutes } from './modules/auth/routes.js';
import { orgRoutes } from './modules/orgs/routes.js';
import { oauthRoutes } from './modules/auth/oauth/routes.js';
import { mfaRoutes } from './modules/auth/mfa/routes.js';
import { passkeyRoutes } from './modules/auth/passkeys/routes.js';
import { deviceRoutes } from './modules/auth/devices/routes.js';
import { agentRuntimeRoutes } from './modules/agent-runtime/routes.js';
import { loginEventRoutes } from './modules/auth/login-events/routes.js';
import { pushApprovalRoutes } from './modules/auth/push-approvals/routes.js';
import { qrAuthRoutes } from './modules/auth/qr-auth/routes.js';
import { enterpriseRoutes } from './modules/auth/enterprise/routes.js';
import { magicLinkRoutes } from './modules/auth/magic-link/routes.js';
import { auditRoutes } from './modules/audit/routes.js';
import { agentRoutes } from './modules/agents/routes.js';
import { agentEvalRoutes } from './modules/agent-evals/routes.js';
import { aiBudgetRoutes } from './modules/ai-budgets/routes.js';
import { knowledgeRoutes } from './modules/knowledge/routes.js';
import { scimRoutes } from './modules/scim/routes.js';
import { supportCaseRoutes } from './modules/support-cases/routes.js';
import { notificationRoutes } from './modules/notifications/routes.js';
import { flagRoutes } from './modules/flags/routes.js';
import { rateLimitRoutes } from './modules/ratelimit/routes.js';
import { blobRoutes } from './modules/blob/routes.js';
import { invitationRoutes } from './modules/invitations/routes.js';
import { referralRoutes } from './modules/referrals/routes.js';
import { referralMigrationAdminRoutes } from './modules/referrals/migration-admin-routes.js';
import { promoRoutes } from './modules/promos/routes.js';
import { subscriptionRoutes } from './modules/subscriptions/routes.js';
import { usageRoutes } from './modules/usage/routes.js';
import { planRoutes } from './modules/plans/routes.js';
import { licenseRoutes } from './modules/licenses/routes.js';
import { stripeRoutes } from './modules/stripe/routes.js';
import { settingsRoutes } from './modules/settings/routes.js';
import { itemRoutes } from './modules/items/routes.js';
import { commentRoutes } from './modules/comments/routes.js';
import { voteRoutes } from './modules/votes/routes.js';
import { publicRoutes } from './modules/public/routes.js';
import { tokenRoutes } from './modules/tokens/routes.js';
import { themeRoutes } from './modules/themes/routes.js';
import { waitlistRoutes } from './modules/waitlist/routes.js';
import { telemetryRoutes } from './modules/telemetry/routes.js';
import { diagnosticsRoutes } from './modules/diagnostics/routes.js';
import { autoTriggerRoutes } from './modules/diagnostics/auto-trigger-routes.js';
import { crashTriggerRoutes } from './modules/diagnostics/crash-trigger.js';
import { sessionReplayRoutes } from './modules/diagnostics/session-replay-routes.js';
import { performanceProfileRoutes } from './modules/diagnostics/performance-profile-routes.js';
import { startTriggerEvaluationJob } from './modules/diagnostics/trigger-job.js';
import { broadcastRoutes } from './modules/broadcasts/routes.js';
import { surveyRoutes } from './modules/surveys/routes.js';
import { jobRoutes } from './modules/jobs/routes.js';
import { runRoutes } from './modules/runs/routes.js';
import { statusRoutes } from './modules/status/routes.js';
import { deliveryRoutes } from './modules/delivery/routes.js';
import { sessionRoutes } from './modules/sessions/routes.js';
import { timelineRoutes } from './modules/timeline/routes.js';
import { maintenanceRoutes } from './modules/maintenance/routes.js';
import { exportRoutes } from './modules/exports/routes.js';
import { ipRuleRoutes } from './modules/ip-rules/routes.js';
import { experimentRoutes } from './modules/experiments/routes.js';
import { abTestingRoutes } from './modules/ab-testing/routes.js';
import { analyticsRoutes } from './modules/analytics/routes.js';
import { feedbackRoutes } from './modules/feedback/routes.js';
import { reviewRoutes } from './modules/reviews/routes.js';
import { impersonationRoutes } from './modules/impersonation/routes.js';
import { changelogRoutes } from './modules/changelog/routes.js';
import { webhookRoutes } from './modules/webhooks/routes.js';
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 { i18nRoutes } from './modules/i18n/routes.js';
import aiDiagnosticsRoutes from './modules/ai-diagnostics/routes.js';
import { initCosmosIfNeeded } from './lib/cosmos-init.js';
import { config } from './lib/config.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';
import { registerDeliverySubscribers } from './modules/delivery/subscribers.js';
import { verifyToken } from './modules/auth/jwt.js';
import { registerOptionalApiKeyContext } from './lib/api-key-auth.js';
import { eventSubscriptionRoutes } from './modules/event-subscriptions/routes.js';
import { agentExecutorRoutes } from './modules/agents/executor-routes.js';
import { startEventBus, stopEventBus } from './lib/event-bus.js';
import { wireDispatcherToBus } from './lib/event-dispatcher.js';
await initCosmosIfNeeded();
await loadProductCache();
// Run pending database migrations (idempotent, best-effort — failures don't block startup)
runPendingMigrations().catch(() => {});
// Seed default feature flags (idempotent, best-effort)
seedDefaultFlags({ info: (msg: string) => process.stdout.write(`[flags-seed] ${msg}\n`) }).catch(
() => {}
);
const app = await createServiceApp({
name: 'platform-service',
version: '0.1.0',
description:
'Auth, audit, notifications, feature flags, rate limiting, invitations, referrals, promos, subscriptions, usage, plans, licenses, stripe',
corsOrigin: config.CORS_ORIGIN,
swagger: {
title: 'Platform Service',
description:
'Auth, audit, notifications, feature flags, rate limiting, invitations, referrals, promos, subscriptions, usage, plans, licenses, stripe',
port: config.PORT,
},
metrics: true,
});
await registerOptionalJwtContext(app, {
verifyToken: async token => (await verifyToken(token)) as JwtPayload,
});
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' });
await app.register(orgRoutes, { prefix: '/api' });
await app.register(oauthRoutes, { prefix: '/api' });
await app.register(mfaRoutes, { prefix: '/api' });
await app.register(passkeyRoutes, { prefix: '/api' });
await app.register(deviceRoutes, { prefix: '/api' });
await app.register(agentRuntimeRoutes, { prefix: '/api' });
await app.register(loginEventRoutes, { prefix: '/api' });
await app.register(pushApprovalRoutes, { prefix: '/api' });
await app.register(qrAuthRoutes, { prefix: '/api' });
await app.register(enterpriseRoutes, { prefix: '/api' });
await app.register(magicLinkRoutes, { prefix: '/api' });
await app.register(auditRoutes, { prefix: '/api' });
await app.register(agentRoutes, { prefix: '/api' });
await app.register(agentEvalRoutes, { prefix: '/api' });
await app.register(aiBudgetRoutes, { prefix: '/api' });
await app.register(knowledgeRoutes, { prefix: '/api' });
await app.register(scimRoutes, { prefix: '/api' });
await app.register(supportCaseRoutes, { prefix: '/api' });
await app.register(notificationRoutes, { prefix: '/api' });
await app.register(flagRoutes, { prefix: '/api' });
await app.register(rateLimitRoutes, { prefix: '/api' });
await app.register(blobRoutes, { prefix: '/api' });
// Growth modules (merged from growth-service)
await app.register(invitationRoutes, { prefix: '/api' });
await app.register(referralRoutes, { prefix: '/api' });
await app.register(referralMigrationAdminRoutes, { prefix: '/api/admin' });
await app.register(promoRoutes, { prefix: '/api' });
// Billing modules (merged from billing-service)
await app.register(subscriptionRoutes, { prefix: '/api' });
await app.register(usageRoutes, { prefix: '/api' });
await app.register(planRoutes, { prefix: '/api' });
await app.register(licenseRoutes, { prefix: '/api' });
// Stripe routes outside billing scope (webhook has its own signature verification)
await app.register(stripeRoutes, { prefix: '/api' });
// Settings module (user-level global settings + per-device overrides)
await app.register(settingsRoutes, { prefix: '/api' });
// Tracker modules (merged from tracker-service)
await app.register(itemRoutes, { prefix: '/api' });
await app.register(commentRoutes, { prefix: '/api' });
await app.register(voteRoutes, { prefix: '/api' });
// API tokens module
await app.register(tokenRoutes, { prefix: '/api' });
// Themes module
await app.register(themeRoutes, { prefix: '/api' });
// Waitlist module (pre-launch signups — public + admin routes)
await app.register(waitlistRoutes, { prefix: '/api' });
// Telemetry module (client ingest + admin query + policies)
await app.register(telemetryRoutes, { prefix: '/api' });
// Diagnostics module (remote debug sessions — see docs/devops/REMOTE_DIAGNOSTICS_ROADMAP.md)
await app.register(diagnosticsRoutes, { prefix: '/api' });
// Auto-trigger routes for automated debug sessions (Phase 4)
await app.register(autoTriggerRoutes, { prefix: '/api' });
// Crash-trigger routes for crash-triggered auto-sessions (Phase 4)
await app.register(crashTriggerRoutes, { prefix: '/api' });
// Session replay routes (Phase 4)
await app.register(sessionReplayRoutes, { prefix: '/api' });
// Performance profiling routes (Phase 4)
await app.register(performanceProfileRoutes, { prefix: '/api' });
// Public routes — no auth, registered at top level
await app.register(publicRoutes, { prefix: '/api' });
// Scheduled jobs module (admin: list, trigger, view runs)
await app.register(jobRoutes, { prefix: '/api' });
await app.register(runRoutes, { prefix: '/api' });
// Public status page + incident management
await app.register(statusRoutes, { prefix: '/api' });
// Transactional email delivery
await app.register(deliveryRoutes, { prefix: '/api' });
// Session management
await app.register(sessionRoutes, { prefix: '/api' });
// Cross-product timeline ingest + query
await app.register(timelineRoutes, { prefix: '/api' });
// Maintenance mode
await app.register(maintenanceRoutes, { prefix: '/api' });
// Data exports
await app.register(exportRoutes, { prefix: '/api' });
// IP allow/deny rules
await app.register(ipRuleRoutes, { prefix: '/api' });
// P2 — Product Intelligence
await app.register(experimentRoutes, { prefix: '/api' });
await app.register(abTestingRoutes, { prefix: '/api' });
await app.register(analyticsRoutes, { prefix: '/api' });
await app.register(feedbackRoutes, { prefix: '/api' });
await app.register(reviewRoutes, { prefix: '/api' });
await app.register(impersonationRoutes, { prefix: '/api' });
await app.register(changelogRoutes, { prefix: '/api' });
// Webhook subscriptions (replaces lib/webhooks.ts fire-and-forget)
await app.register(webhookRoutes, { prefix: '/api' });
// Generic Marketplace module
await app.register(marketplaceRoutes, { prefix: '/api' });
// Predictive Analytics (Churn & Health Scoring)
await app.register(predictiveAnalyticsRoutes, { prefix: '/api' });
// Onboarding analytics (Phase 4.3)
await app.register(onboardingRoutes, { prefix: '/api' });
// Pre-built Stripe Checkout (Phase 4.4)
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' });
// i18n translations
await app.register(i18nRoutes, { prefix: '/api' });
// AI Diagnostics (NL query, LLM root-cause, error clustering)
await app.register(aiDiagnosticsRoutes, { prefix: '/api/ai-diagnostics' });
// Event subscriptions + DLQ + replay
await app.register(eventSubscriptionRoutes, { prefix: '/api' });
// Agent executor + tool registry + scheduling + metrics
await app.register(agentExecutorRoutes, { prefix: '/api' });
// DevOps endpoints
await app.register(
async function (fastify) {
// DevOps version endpoint (public - no auth required)
fastify.get('/devops/version', async (request, reply) => {
return reply.send(getBuildInfo());
});
// DevOps info endpoint (admin only)
fastify.get(
'/devops/info',
{
preHandler: async (request, reply) => {
// Require admin role. The `auth` property is decorated onto the
// Fastify request by the auth preHandler upstream; cast through
// `unknown` to read it without `any` (a `declare module` augmentation
// would be cleaner but requires touching @bytelyst/fastify-auth).
const auth = (request as unknown as { auth?: { role?: string } }).auth;
if (!auth || auth.role !== 'admin') {
return reply.code(403).send({ error: 'Admin access required' });
}
},
},
async (request, reply) => {
try {
// Two distinct config sources here — they were previously colliding
// because both were named `config` and the inner one shadowed the
// module-level env config imported at the top of this file:
// * productIdentity — loaded from shared/product.json. Has
// productId/displayName/etc., NOT environment values.
// * config (outer) — env-validated Zod object from ./lib/config.js.
// Has COSMOS_ENDPOINT, HOST, PORT, BACKEND_URL, etc.
// The original code tried to read config.cosmosEndpoint and
// config.platformServiceUrl off the ProductIdentity, which fails
// TS2339 because those keys don't exist there.
const productIdentity = (await import('@bytelyst/config')).loadProductIdentity();
const info = await collectDevopsInfo({
productId: productIdentity.productId || 'platform',
serviceName: 'platform-service',
serviceVersion: readServiceVersion(import.meta.url),
dependencyChecks: [
() => httpDependencyCheck('cosmos-db', config.COSMOS_ENDPOINT || 'unknown'),
],
extra: {
platformServiceUrl: `http://${config.HOST}:${config.PORT}`,
},
});
return reply.send(info);
} catch (error: any) {
fastify.log.error('Failed to collect devops info:', error);
return reply.code(500).send({ error: error.message });
}
}
);
},
{ prefix: '/api' }
);
// Register event bus subscribers
registerDiagnosticsSubscribers(app.log);
registerDeliverySubscribers(app.log);
wireDispatcherToBus();
startEventBus();
app.addHook('onClose', async () => {
await stopEventBus();
});
// Start diagnostic trigger evaluation job (Phase 4)
startTriggerEvaluationJob(app.log);
await startService(app, { port: config.PORT, host: config.HOST });