import Fastify from "fastify"; import { registerAdminPlugin } from "./admin/plugin.js"; import { prisma } from "./db.js"; import { createLarkClient, startFeishuListenerWithClient } from "./feishu/client.js"; import { archiveFeishuBindingForLifecycleEvent } from "./feishu/bindingLifecycle.js"; import { makeTriggerHandler } from "./feishu/trigger.js"; import { removeAbandonedMessageResourceStages } from "./feishu/resourceStaging.js"; import { triggerQueue } from "./feishu/triggerQueue.js"; import { LocalSecretEnvelope, loadLocalSecretKeyring } from "./security/secretEnvelope.js"; import { createDatabaseRuntimeSettings } from "./settings/runtime.js"; import { verifyStoredProviderEnvelopes } from "./connections/providerConnections.js"; import { openProviderProxyLease } from "./connections/providerProxy.js"; import { verifyStoredFeishuApplicationEnvelopes } from "./connections/feishuApplicationConnections.js"; import { resolveActiveFeishuApplication } from "./connections/feishuApplicationConnections.js"; import { readServerBinding } from "./settings/server.js"; import { readSiloOrganizationId, requireSiloOrganization } from "./deployment/silo.js"; import { isSiloHttpRateLimitExempt, SiloFixedWindowRateLimiter } from "./deployment/siloRateLimit.js"; function requireEnv(name: string): string { const value = process.env[name]; if (value === undefined || value === "") { throw new Error(`missing required env: ${name}`); } return value; } function booleanEnv(name: string, fallback: boolean): boolean { const raw = process.env[name]; if (raw === undefined || raw === "") return fallback; return !["0", "false", "no", "off"].includes(raw.trim().toLowerCase()); } /** Start the Hub after every import and runtime dependency has loaded. */ export async function startHub(): Promise { requireEnv("DATABASE_URL"); const projectWorkspaceRoot = requireEnv("HUB_PROJECT_WORKSPACE_ROOT"); const httpBodyLimit = positiveIntegerEnv("HUB_HTTP_BODY_LIMIT_BYTES"); const maxFilesPerMessage = positiveIntegerEnv("HUB_MAX_FILES_PER_MESSAGE"); const maxBytesPerFile = positiveIntegerEnv("HUB_MAX_FILE_BYTES"); const httpRequestsPerMinute = positiveIntegerEnv("HUB_HTTP_REQUESTS_PER_MINUTE"); const feishuEventsPerMinute = positiveIntegerEnv("HUB_FEISHU_EVENTS_PER_MINUTE"); const app = Fastify({ logger: true, bodyLimit: httpBodyLimit }); const requestLimiter = new SiloFixedWindowRateLimiter(httpRequestsPerMinute, 60_000); app.addHook("onRequest", async (request, reply) => { // Static SPA assets / admin HTML shell / healthz do not count toward the // silo requestRate budget (a full admin load can pull dozens of chunks). if (isSiloHttpRateLimitExempt(request.url)) return; const decision = requestLimiter.consume(); if (!decision.allowed) { await reply .header("Retry-After", String(decision.retryAfterSeconds)) .status(429) .send({ error: { code: "rate_limited", message: "Silo request rate exceeded" } }); } }); const abandonedStages = await removeAbandonedMessageResourceStages(projectWorkspaceRoot); app.log.info({ removed: abandonedStages }, "startup: removed abandoned Feishu resource stages"); const secretEnvelope = new LocalSecretEnvelope(await loadLocalSecretKeyring()); const verifiedProviderEnvelopes = await verifyStoredProviderEnvelopes(prisma, secretEnvelope); const verifiedFeishuEnvelopes = await verifyStoredFeishuApplicationEnvelopes(prisma, secretEnvelope); app.log.info( { provider: verifiedProviderEnvelopes, feishu: verifiedFeishuEnvelopes }, "startup: authenticated stored connection envelopes", ); const runtimeSettings = createDatabaseRuntimeSettings( prisma, secretEnvelope, process.env, (credential, context) => openProviderProxyLease(credential, { onDiagnostic: (diagnostic) => { app.log.error({ runId: context.runId, projectId: context.projectId, providerId: context.providerId, errorCode: diagnostic.code, failureCategory: diagnostic.category, }, "provider proxy diagnostic"); }, }), ); const siloOrganizationId = readSiloOrganizationId(); const siloOrganization = await requireSiloOrganization(prisma, siloOrganizationId); const activeProvider = await prisma.organizationProviderConnection.findUnique({ where: { organizationId_providerId: { organizationId: siloOrganization.id, providerId: "openrouter", }, }, select: { status: true, activeSecretVersion: { select: { connectionId: true, retiredAt: true } }, id: true, }, }); if (activeProvider?.status !== "ACTIVE" || activeProvider.activeSecretVersion === null || activeProvider.activeSecretVersion.connectionId !== activeProvider.id || activeProvider.activeSecretVersion.retiredAt !== null) { throw new Error(`Silo Organization ${siloOrganization.id} has no ACTIVE openrouter Provider Connection`); } const feishuApplication = await resolveActiveFeishuApplication( prisma, secretEnvelope, { organizationId: siloOrganization.id }, ); app.log.info( { organizationId: siloOrganization.id, organizationSlug: siloOrganization.slug, feishuConnectionId: feishuApplication.connectionId, }, "startup: proved Silo Organization and resolved Feishu Application", ); const sessionSecret = requireEnv("HUB_SESSION_SECRET"); const publicBaseUrl = process.env["HUB_PUBLIC_BASE_URL"] ?? "http://127.0.0.1:8788"; const bind = readServerBinding(); // Startup reset: clear stale locks + mark dead runs as FAILED. await prisma.projectAgentLock.deleteMany({}); await prisma.agentRun.updateMany({ where: { status: "ACTIVE" }, data: { status: "FAILED", error: "process restart", finishedAt: new Date() }, }); app.log.info("startup: cleared stale locks + dead runs"); let feishuRuntime: { readonly isListenerReady?: () => boolean } | undefined; app.get("/api/healthz", async (_request, reply) => { const feishuReady = feishuRuntime?.isListenerReady?.() ?? !booleanEnv("HUB_FEISHU_LISTENER_ENABLED", true); if (!feishuReady) return reply.status(503).send({ ok: false, feishuReady, ts: Date.now() }); return { ok: true, feishuReady, ts: Date.now() }; }); await registerAdminPlugin(app, { prisma, sessionSecret, publicBaseUrl, feishuAppId: feishuApplication.appId, feishuAppSecret: feishuApplication.appSecret, projectWorkspaceRoot, secretEnvelope, }); const feishuListenerEnabled = booleanEnv("HUB_FEISHU_LISTENER_ENABLED", true); if (feishuListenerEnabled) { const feishuConfig = { appId: feishuApplication.appId, appSecret: feishuApplication.appSecret, botOpenId: feishuApplication.botOpenId, }; const larkClient = createLarkClient(feishuConfig); const triggerQueuePurgeTimer = setInterval(() => { const removed = triggerQueue.purgeExpired(); if (removed > 0) { app.log.info({ removed }, "feishu trigger queue: purged expired triggers"); } }, 60_000); triggerQueuePurgeTimer.unref(); const trigger = makeTriggerHandler({ prisma, settings: runtimeSettings, logger: app.log, secretEnvelope, projectWorkspaceRoot, publicBaseUrl, siloOrganizationId: siloOrganization.id, rejectWhenBusy: true, resourceLimits: { maxFilesPerMessage, maxBytesPerFile }, allowLegacyFeishuIdentity: false, maxFeishuEventsPerMinute: feishuEventsPerMinute, }); feishuRuntime = await startFeishuListenerWithClient( feishuConfig, larkClient, app.log, trigger, trigger.onCardAction, () => { process.exitCode = 1; void app.close().catch((error) => app.log.error({ err: error }, "Hub close after Feishu failure failed")); }, async (event) => { const result = await archiveFeishuBindingForLifecycleEvent(prisma, event); app.log.info({ ...event, archived: result.archived, projectId: result.projectId }, "feishu binding lifecycle event handled"); }, ); } else { app.log.info("feishu listener disabled by HUB_FEISHU_LISTENER_ENABLED"); } const address = await app.listen(bind); app.log.info({ address }, "hub listening"); } function positiveIntegerEnv(name: string): number { const raw = requireEnv(name); const value = Number(raw); if (!Number.isSafeInteger(value) || value <= 0) throw new Error(`${name} must be a positive safe integer`); return value; }