forked from EduCraft/curriculum-project-hub
feat: add deployable alpha silo
This commit is contained in:
+96
-13
@@ -10,7 +10,10 @@ 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 { SiloFixedWindowRateLimiter } from "./deployment/siloRateLimit.js";
|
||||
|
||||
function requireEnv(name: string): string {
|
||||
const value = process.env[name];
|
||||
@@ -30,7 +33,23 @@ function booleanEnv(name: string, fallback: boolean): boolean {
|
||||
export async function startHub(): Promise<void> {
|
||||
requireEnv("DATABASE_URL");
|
||||
const projectWorkspaceRoot = requireEnv("HUB_PROJECT_WORKSPACE_ROOT");
|
||||
const app = Fastify({ logger: true });
|
||||
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) => {
|
||||
if (request.url === "/api/healthz") 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");
|
||||
@@ -59,9 +78,39 @@ export async function startHub(): Promise<void> {
|
||||
}),
|
||||
);
|
||||
|
||||
const feishuAppId = requireEnv("FEISHU_APP_ID");
|
||||
const feishuAppSecret = requireEnv("FEISHU_APP_SECRET");
|
||||
const feishuBotOpenId = requireEnv("FEISHU_BOT_OPEN_ID");
|
||||
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();
|
||||
@@ -74,24 +123,30 @@ export async function startHub(): Promise<void> {
|
||||
});
|
||||
app.log.info("startup: cleared stale locks + dead runs");
|
||||
|
||||
app.get("/api/healthz", async () => ({ ok: true, ts: Date.now() }));
|
||||
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,
|
||||
feishuAppSecret,
|
||||
feishuAppId: feishuApplication.appId,
|
||||
feishuAppSecret: feishuApplication.appSecret,
|
||||
projectWorkspaceRoot,
|
||||
secretEnvelope,
|
||||
});
|
||||
|
||||
const address = await app.listen(bind);
|
||||
app.log.info({ address }, "hub listening");
|
||||
|
||||
const feishuListenerEnabled = booleanEnv("HUB_FEISHU_LISTENER_ENABLED", true);
|
||||
if (feishuListenerEnabled) {
|
||||
const feishuConfig = { appId: feishuAppId, appSecret: feishuAppSecret, botOpenId: feishuBotOpenId };
|
||||
const feishuConfig = {
|
||||
appId: feishuApplication.appId,
|
||||
appSecret: feishuApplication.appSecret,
|
||||
botOpenId: feishuApplication.botOpenId,
|
||||
};
|
||||
const larkClient = createLarkClient(feishuConfig);
|
||||
const triggerQueuePurgeTimer = setInterval(() => {
|
||||
const removed = triggerQueue.purgeExpired();
|
||||
@@ -100,9 +155,37 @@ export async function startHub(): Promise<void> {
|
||||
}
|
||||
}, 60_000);
|
||||
triggerQueuePurgeTimer.unref();
|
||||
const trigger = makeTriggerHandler({ prisma, settings: runtimeSettings, logger: app.log, projectWorkspaceRoot });
|
||||
startFeishuListenerWithClient(feishuConfig, larkClient, app.log, trigger, trigger.onCardAction);
|
||||
const trigger = makeTriggerHandler({
|
||||
prisma,
|
||||
settings: runtimeSettings,
|
||||
logger: app.log,
|
||||
projectWorkspaceRoot,
|
||||
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"));
|
||||
},
|
||||
);
|
||||
} 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user