import type { PrismaClient } from "@prisma/client"; import type { FastifyInstance } from "fastify"; import { FeishuApplicationConnectionService } from "../../connections/feishuApplicationConnections.js"; import { FeishuReadinessError, type FeishuReadinessProbe } from "../../connections/feishuReadiness.js"; import type { LocalSecretEnvelope } from "../../security/secretEnvelope.js"; import { requireOrgRole, type GuardDeps } from "../auth/guards.js"; import { handleRouteError } from "../errors.js"; export interface FeishuApplicationConnectionRouteConfig { readonly prisma: PrismaClient; readonly sessionSecret: string; readonly secretEnvelope: LocalSecretEnvelope; readonly readinessProbe?: FeishuReadinessProbe; } export async function registerFeishuApplicationConnectionRoutes( app: FastifyInstance, config: FeishuApplicationConnectionRouteConfig, ): Promise { const guardDeps: GuardDeps = { prisma: config.prisma, sessionSecret: config.sessionSecret }; const connections = new FeishuApplicationConnectionService( config.prisma, config.secretEnvelope, config.readinessProbe, ); app.get("/api/org/:orgSlug/feishu-application-connection", async (request, reply) => { try { const { orgSlug } = request.params as { orgSlug: string }; const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug }); if (auth === null) return; return { connection: await connections.readMetadata(auth.organization.id) }; } catch (error) { request.log.error({ requestId: request.id, operation: "feishu_application_connection.read", errorCode: "feishu_application_connection_read_failed", }, "Feishu Application Connection read failed"); return handleRouteError(reply, error); } }); app.put("/api/org/:orgSlug/feishu-application-connection", async (request, reply) => { try { const { orgSlug } = request.params as { orgSlug: string }; const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug }); if (auth === null) return; const body = parseBody(request.body); const result = await connections.rotateCustomerApplication({ organizationId: auth.organization.id, actorUserId: auth.user.id, ...body, }); request.log.info({ organizationId: auth.organization.id, connectionId: result.id, status: result.status, secretVersion: result.activeVersion, keyId: result.keyId, }, result.created ? "Feishu Application Connection created" : "Feishu Application Connection rotated"); const { created, ...metadata } = result; return reply.status(created ? 201 : 200).send(metadata); } catch (error) { const facts = error instanceof FeishuReadinessError ? { errorCode: error.code, failureCategory: error.category, ...(error.upstreamStatus !== undefined ? { upstreamStatus: error.upstreamStatus } : {}), } : { errorCode: "feishu_application_connection_write_failed" }; request.log.error({ requestId: request.id, operation: "feishu_application_connection.rotate", ...facts, }, "Feishu Application Connection mutation failed"); return handleRouteError(reply, error); } }); app.delete("/api/org/:orgSlug/feishu-application-connection", async (request, reply) => { try { const { orgSlug } = request.params as { orgSlug: string }; const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug }); if (auth === null) return; const result = await connections.disable({ organizationId: auth.organization.id, actorUserId: auth.user.id, }); request.log.info({ organizationId: auth.organization.id, connectionId: result.id, status: result.status, secretVersion: result.activeVersion, keyId: result.keyId, }, "Feishu Application Connection disabled"); return reply.send(result); } catch (error) { request.log.error({ requestId: request.id, operation: "feishu_application_connection.disable", errorCode: "feishu_application_connection_disable_failed", }, "Feishu Application Connection disable failed"); return handleRouteError(reply, error); } }); } function parseBody(value: unknown): { readonly appId: string; readonly appSecret: string; readonly botOpenId: string; readonly verificationToken?: string; readonly encryptKey?: string; } { if (typeof value !== "object" || value === null || Array.isArray(value)) { throw new Error("invalid Feishu Application credential body"); } const body = value as Record; for (const name of ["appId", "appSecret", "botOpenId"] as const) { if (typeof body[name] !== "string") throw new Error(`${name} is required`); } for (const name of ["verificationToken", "encryptKey"] as const) { if (body[name] !== undefined && typeof body[name] !== "string") { throw new Error(`${name} must be a string`); } } return { appId: body["appId"] as string, appSecret: body["appSecret"] as string, botOpenId: body["botOpenId"] as string, ...(body["verificationToken"] !== undefined ? { verificationToken: body["verificationToken"] as string } : {}), ...(body["encryptKey"] !== undefined ? { encryptKey: body["encryptKey"] as string } : {}), }; }