import { createHash } from "node:crypto"; import { afterAll, beforeEach, describe, expect, it } from "vitest"; import { FeishuApplicationConnectionService, rewrapStoredFeishuApplicationEnvelopes, resolveActiveFeishuApplication, verifyStoredFeishuApplicationEnvelopes, } from "../../src/connections/feishuApplicationConnections.js"; import { DEFAULT_ORG_ID, prisma, resetDb, seedTestOrganization, testSecretEnvelope } from "./helpers.js"; describe("Organization Feishu Application Connections", () => { beforeEach(async () => { await resetDb(); }); afterAll(async () => { await prisma.$disconnect(); }); it("stores write-only app credentials and resolves them only from the explicit Organization", async () => { await seedAdmin("feishu-admin", DEFAULT_ORG_ID, "ADMIN"); const service = new FeishuApplicationConnectionService(prisma, testSecretEnvelope, async () => {}); const credential = { appId: "cli_shared_provider_local_id", appSecret: "feishu-secret-never-return", botOpenId: "ou_shared_bot", verificationToken: "verification-never-return", encryptKey: "encrypt-key-never-return", }; const created = await service.rotateCustomerApplication({ organizationId: DEFAULT_ORG_ID, actorUserId: "feishu-admin", ...credential, }); expect(created).toMatchObject({ created: true, status: "ACTIVE", activeVersion: 1, keyId: "test-active", }); const persisted = await prisma.$queryRaw>` SELECT "envelope" FROM "FeishuApplicationCredentialVersion" `; const serialized = JSON.stringify(persisted); for (const plaintext of Object.values(credential)) expect(serialized).not.toContain(plaintext); await expect(resolveActiveFeishuApplication(prisma, testSecretEnvelope, { organizationId: DEFAULT_ORG_ID, })).resolves.toMatchObject({ connectionId: created.id, organizationId: DEFAULT_ORG_ID, ...credential, }); }); it("allows colliding provider-local identifiers in two connection namespaces", async () => { await seedTestOrganization("org_feishu_other", "feishu-other"); await seedAdmin("feishu-admin-a", DEFAULT_ORG_ID, "OWNER"); await seedAdmin("feishu-admin-b", "org_feishu_other", "ADMIN"); const service = new FeishuApplicationConnectionService(prisma, testSecretEnvelope, async () => {}); const shared = { appSecret: "same-secret-is-still-separately-encrypted", botOpenId: "ou_same_bot", }; const first = await service.rotateCustomerApplication({ organizationId: DEFAULT_ORG_ID, actorUserId: "feishu-admin-a", appId: "cli_app_a", ...shared, }); const second = await service.rotateCustomerApplication({ organizationId: "org_feishu_other", actorUserId: "feishu-admin-b", appId: "cli_app_b", ...shared, }); expect(first.id).not.toBe(second.id); const resolved = await Promise.all([ resolveActiveFeishuApplication(prisma, testSecretEnvelope, { connectionId: first.id }), resolveActiveFeishuApplication(prisma, testSecretEnvelope, { connectionId: second.id }), ]); expect(resolved.map((item) => item.organizationId)).toEqual([DEFAULT_ORG_ID, "org_feishu_other"]); }); it("refuses to relabel a stable connection or share one app across Organizations", async () => { await seedTestOrganization("org_feishu_other", "feishu-other"); await seedAdmin("feishu-owner-a", DEFAULT_ORG_ID, "OWNER"); await seedAdmin("feishu-owner-b", "org_feishu_other", "OWNER"); const service = new FeishuApplicationConnectionService(prisma, testSecretEnvelope, async () => {}); await service.rotateCustomerApplication({ organizationId: DEFAULT_ORG_ID, actorUserId: "feishu-owner-a", appId: "cli_stable", appSecret: "secret-one", botOpenId: "ou_bot", }); await expect(service.rotateCustomerApplication({ organizationId: DEFAULT_ORG_ID, actorUserId: "feishu-owner-a", appId: "cli_replacement", appSecret: "replacement-secret", botOpenId: "ou_replacement_bot", })).rejects.toThrow("cannot change app identity"); await expect(service.rotateCustomerApplication({ organizationId: "org_feishu_other", actorUserId: "feishu-owner-b", appId: "cli_stable", appSecret: "secret-one", botOpenId: "ou_bot", })).rejects.toThrow("cannot be shared across Organizations"); await expect(prisma.organizationFeishuApplicationConnection.count()).resolves.toBe(1); await expect(prisma.feishuApplicationCredentialVersion.count()).resolves.toBe(1); }); it("rotates immutable versions and rejects a non-admin service caller", async () => { await seedAdmin("feishu-owner", DEFAULT_ORG_ID, "OWNER"); await seedAdmin("feishu-member", DEFAULT_ORG_ID, "MEMBER"); const service = new FeishuApplicationConnectionService(prisma, testSecretEnvelope, async () => {}); await service.rotateCustomerApplication({ organizationId: DEFAULT_ORG_ID, actorUserId: "feishu-owner", appId: "cli_one", appSecret: "secret-one", botOpenId: "ou_bot", }); const rotated = await service.rotateCustomerApplication({ organizationId: DEFAULT_ORG_ID, actorUserId: "feishu-owner", appId: "cli_one", appSecret: "secret-two", botOpenId: "ou_bot", }); expect(rotated).toMatchObject({ created: false, activeVersion: 2 }); await expect(prisma.feishuApplicationCredentialVersion.findMany({ orderBy: { version: "asc" }, select: { version: true, retiredAt: true }, })).resolves.toEqual([ { version: 1, retiredAt: expect.any(Date) }, { version: 2, retiredAt: null }, ]); await expect(service.rotateCustomerApplication({ organizationId: DEFAULT_ORG_ID, actorUserId: "feishu-member", appId: "cli_forbidden", appSecret: "forbidden-secret", botOpenId: "ou_bot", })).rejects.toThrow("requires an active Organization OWNER or ADMIN"); await expect(prisma.feishuApplicationCredentialVersion.count()).resolves.toBe(2); }); it("disables immediately through an authorized audited lifecycle write", async () => { await seedAdmin("feishu-owner", DEFAULT_ORG_ID, "OWNER"); const service = new FeishuApplicationConnectionService(prisma, testSecretEnvelope, async () => {}); const created = await service.rotateCustomerApplication({ organizationId: DEFAULT_ORG_ID, actorUserId: "feishu-owner", appId: "cli_disable", appSecret: "disable-secret", botOpenId: "ou_bot", }); await expect(service.disable({ organizationId: DEFAULT_ORG_ID, actorUserId: "feishu-owner", })).resolves.toMatchObject({ id: created.id, status: "DISABLED", activeVersion: 1 }); await expect(resolveActiveFeishuApplication(prisma, testSecretEnvelope, { connectionId: created.id, })).rejects.toThrow("active Feishu Application Connection not found"); await expect(prisma.auditEntry.count({ where: { organizationId: DEFAULT_ORG_ID, action: "feishu_application.disabled" }, })).resolves.toBe(1); }); it("fails closed when the stored app fingerprint is detached from the encrypted app id", async () => { await seedAdmin("feishu-owner", DEFAULT_ORG_ID, "OWNER"); const service = new FeishuApplicationConnectionService(prisma, testSecretEnvelope, async () => {}); const created = await service.rotateCustomerApplication({ organizationId: DEFAULT_ORG_ID, actorUserId: "feishu-owner", appId: "cli_original", appSecret: "original-secret", botOpenId: "ou_bot", }); const replacementFingerprint = createHash("sha256") .update("cph-feishu-app\0") .update("cli_replacement", "utf8") .digest("hex"); await prisma.organizationFeishuApplicationConnection.update({ where: { id: created.id }, data: { appIdentityFingerprint: replacementFingerprint }, }); await expect(resolveActiveFeishuApplication(prisma, testSecretEnvelope, { connectionId: created.id, })).rejects.toThrow("app identity fingerprint mismatch"); await expect(verifyStoredFeishuApplicationEnvelopes( prisma, testSecretEnvelope, )).rejects.toThrow("app identity fingerprint mismatch"); await expect(prisma.$transaction((tx) => rewrapStoredFeishuApplicationEnvelopes( tx, testSecretEnvelope, ))).rejects.toThrow("app identity fingerprint mismatch"); await expect(service.rotateCustomerApplication({ organizationId: DEFAULT_ORG_ID, actorUserId: "feishu-owner", appId: "cli_replacement", appSecret: "replacement-secret", botOpenId: "ou_replacement_bot", })).rejects.toThrow("app identity fingerprint mismatch"); await expect(prisma.feishuApplicationCredentialVersion.count()).resolves.toBe(1); }); }); async function seedAdmin( userId: string, organizationId: string, role: "OWNER" | "ADMIN" | "MEMBER", ): Promise { await prisma.user.create({ data: { id: userId, feishuOpenId: `legacy_${userId}`, displayName: userId }, }); await prisma.organizationMembership.create({ data: { organizationId, userId, role }, }); }