feat: secure organization provider credentials

This commit is contained in:
2026-07-10 22:04:43 +08:00
parent e049cb6880
commit 848f913597
51 changed files with 3280 additions and 230 deletions
+2 -1
View File
@@ -9,7 +9,7 @@ import {
sessionCookieHeader,
} from "../../src/admin/routes/authRoutes.js";
import { OAUTH_STATE_COOKIE_NAME, signOAuthState } from "../../src/admin/auth/session.js";
import { DEFAULT_ORG_ID, prisma, resetDb } from "./helpers.js";
import { DEFAULT_ORG_ID, prisma, resetDb, testSecretEnvelope } from "./helpers.js";
const SESSION_SECRET = "integration-test-session-secret";
const PUBLIC_BASE = "http://127.0.0.1:8788";
@@ -23,6 +23,7 @@ async function buildApp(fetchImpl?: typeof fetch) {
feishuAppId: "cli_test",
feishuAppSecret: "secret_test",
projectWorkspaceRoot: "/tmp/cph-test-workspaces",
secretEnvelope: testSecretEnvelope,
cookieSecure: false,
...(fetchImpl !== undefined ? { fetchImpl } : {}),
});
+2 -1
View File
@@ -18,7 +18,7 @@ import {
mintSessionToken,
sessionCookieHeader,
} from "../../src/admin/routes/authRoutes.js";
import { DEFAULT_ORG_ID, prisma, resetDb } from "./helpers.js";
import { DEFAULT_ORG_ID, prisma, resetDb, testSecretEnvelope } from "./helpers.js";
const SESSION_SECRET = "integration-test-session-secret";
const workspaceRoots: string[] = [];
@@ -32,6 +32,7 @@ async function buildApp(workspaceRoot: string) {
feishuAppId: "cli_test",
feishuAppSecret: "secret_test",
projectWorkspaceRoot: workspaceRoot,
secretEnvelope: testSecretEnvelope,
cookieSecure: false,
});
await app.ready();
@@ -8,7 +8,7 @@ import {
mintSessionToken,
sessionCookieHeader,
} from "../../src/admin/routes/authRoutes.js";
import { DEFAULT_ORG_ID, prisma, resetDb } from "./helpers.js";
import { DEFAULT_ORG_ID, prisma, resetDb, testSecretEnvelope } from "./helpers.js";
import { addOrgMember } from "../../src/org/members.js";
import { addTeamMember, archiveTeam, createTeam, updateTeam } from "../../src/org/teams.js";
@@ -23,6 +23,7 @@ async function buildApp() {
feishuAppId: "cli_test",
feishuAppSecret: "secret_test",
projectWorkspaceRoot: "/tmp/cph-test-ws",
secretEnvelope: testSecretEnvelope,
cookieSecure: false,
});
await app.ready();
@@ -0,0 +1,214 @@
import Fastify from "fastify";
import { afterAll, beforeEach, describe, expect, it } from "vitest";
import { registerAdminPlugin } from "../../src/admin/plugin.js";
import { ProviderConnectionService } from "../../src/connections/providerConnections.js";
import { mintSessionToken, sessionCookieHeader } from "../../src/admin/routes/authRoutes.js";
import { DEFAULT_ORG_ID, prisma, resetDb, testSecretEnvelope } from "./helpers.js";
const SESSION_SECRET = "integration-test-session-secret";
const secrets = testSecretEnvelope;
async function buildApp() {
const app = Fastify({ logger: false });
await registerAdminPlugin(app, {
prisma,
sessionSecret: SESSION_SECRET,
publicBaseUrl: "http://127.0.0.1:8788",
feishuAppId: "cli_test",
feishuAppSecret: "secret_test",
projectWorkspaceRoot: "/tmp/cph-test-workspaces",
secretEnvelope: secrets,
providerReadinessProbe: async () => {},
cookieSecure: false,
});
await app.ready();
return app;
}
async function seedUser(role: "OWNER" | "ADMIN" | "MEMBER"): Promise<string> {
const id = `u-${role.toLowerCase()}`;
const openId = `ou_${role.toLowerCase()}`;
await prisma.user.create({ data: { id, feishuOpenId: openId, displayName: id } });
await prisma.organizationMembership.create({
data: { organizationId: DEFAULT_ORG_ID, userId: id, role },
});
return mintSessionToken({ userId: id, feishuOpenId: openId }, SESSION_SECRET);
}
describe("Organization BYOK provider connections", () => {
beforeEach(async () => {
await resetDb();
});
afterAll(async () => {
await prisma.$disconnect();
});
it("stores write-only credentials in an encrypted immutable version", async () => {
const token = await seedUser("OWNER");
const app = await buildApp();
const credential = {
baseUrl: "https://provider.example/api",
authToken: "org-a-auth-token-never-return",
anthropicApiKey: "org-a-api-key-never-return",
};
try {
const response = await app.inject({
method: "PUT",
url: "/api/org/test-default/provider-connections/openrouter",
headers: {
cookie: sessionCookieHeader(token),
"content-type": "application/json",
},
payload: credential,
});
expect(response.statusCode).toBe(201);
expect(response.json()).toMatchObject({
providerId: "openrouter",
mode: "BYOK",
status: "ACTIVE",
activeVersion: 1,
keyId: "test-active",
});
expect(response.body).not.toContain(credential.authToken);
expect(response.body).not.toContain(credential.anthropicApiKey);
expect(response.body).not.toContain(credential.baseUrl);
const persisted = await prisma.$queryRaw<Array<{
envelope: unknown;
activeSecretVersionId: string;
}>>`
SELECT v."envelope", c."activeSecretVersionId"
FROM "ProviderCredentialVersion" v
JOIN "OrganizationProviderConnection" c ON c."id" = v."connectionId"
WHERE c."organizationId" = ${DEFAULT_ORG_ID} AND c."providerId" = 'openrouter'
`;
expect(persisted).toHaveLength(1);
const serialized = JSON.stringify(persisted);
expect(serialized).not.toContain(credential.authToken);
expect(serialized).not.toContain(credential.anthropicApiKey);
expect(serialized).not.toContain(credential.baseUrl);
expect(persisted[0]?.activeSecretVersionId).toBeTruthy();
} finally {
await app.close();
}
});
it("rotates by appending a version and never exposes old or new plaintext", async () => {
const token = await seedUser("ADMIN");
const app = await buildApp();
const cookie = sessionCookieHeader(token);
try {
const first = await app.inject({
method: "PUT",
url: "/api/org/test-default/provider-connections/openrouter",
headers: { cookie, "content-type": "application/json" },
payload: { baseUrl: "https://one.example", authToken: "first-secret" },
});
const second = await app.inject({
method: "PUT",
url: "/api/org/test-default/provider-connections/openrouter",
headers: { cookie, "content-type": "application/json" },
payload: { baseUrl: "https://two.example", authToken: "second-secret" },
});
expect(first.statusCode).toBe(201);
expect(second.statusCode).toBe(200);
expect(second.json()).toMatchObject({ activeVersion: 2, status: "ACTIVE" });
const versions = await prisma.$queryRaw<Array<{ version: number; retiredAt: Date | null }>>`
SELECT v."version", v."retiredAt"
FROM "ProviderCredentialVersion" v
JOIN "OrganizationProviderConnection" c ON c."id" = v."connectionId"
WHERE c."organizationId" = ${DEFAULT_ORG_ID}
ORDER BY v."version"
`;
expect(versions).toEqual([
{ version: 1, retiredAt: expect.any(Date) },
{ version: 2, retiredAt: null },
]);
const read = await app.inject({
method: "GET",
url: "/api/org/test-default/provider-connections",
headers: { cookie },
});
expect(read.statusCode).toBe(200);
expect(read.json()).toEqual({ connections: [expect.objectContaining({ activeVersion: 2 })] });
expect(read.body).not.toContain("first-secret");
expect(read.body).not.toContain("second-secret");
} finally {
await app.close();
}
});
it("rejects members and refuses to overwrite a platform-managed connection", async () => {
const memberToken = await seedUser("MEMBER");
const app = await buildApp();
try {
const denied = await app.inject({
method: "PUT",
url: "/api/org/test-default/provider-connections/openrouter",
headers: {
cookie: sessionCookieHeader(memberToken),
"content-type": "application/json",
},
payload: { baseUrl: "https://provider.example", authToken: "denied-secret" },
});
expect(denied.statusCode).toBe(403);
const service = new ProviderConnectionService(prisma, secrets, async () => {});
await expect(service.rotateByok({
organizationId: DEFAULT_ORG_ID,
providerId: "another-provider",
actorUserId: "u-member",
baseUrl: "https://provider.example",
authToken: "service-bypass-secret",
})).rejects.toThrow("requires an active Organization OWNER or ADMIN");
await expect(prisma.organizationProviderConnection.count({
where: { organizationId: DEFAULT_ORG_ID, providerId: "another-provider" },
})).resolves.toBe(0);
await prisma.$executeRaw`
INSERT INTO "OrganizationProviderConnection"
("id", "organizationId", "providerId", "mode", "status", "createdAt", "updatedAt")
VALUES
('platform-connection', ${DEFAULT_ORG_ID}, 'openrouter', 'PLATFORM_MANAGED', 'DRAFT', NOW(), NOW())
`;
const ownerToken = await seedUser("OWNER");
const refused = await app.inject({
method: "PUT",
url: "/api/org/test-default/provider-connections/openrouter",
headers: {
cookie: sessionCookieHeader(ownerToken),
"content-type": "application/json",
},
payload: { baseUrl: "https://provider.example", authToken: "takeover-secret" },
});
expect(refused.statusCode).toBe(403);
expect(refused.json()).toMatchObject({
error: { code: "forbidden" },
});
} finally {
await app.close();
}
});
it("does not activate or persist a credential that fails readiness", async () => {
await seedUser("ADMIN");
const service = new ProviderConnectionService(prisma, secrets, async () => {
throw new Error("provider credential readiness check failed: status 401");
});
await expect(service.rotateByok({
organizationId: DEFAULT_ORG_ID,
providerId: "openrouter",
actorUserId: "u-admin",
baseUrl: "https://provider.example/api",
authToken: "rejected-secret",
})).rejects.toThrow("readiness check failed");
await expect(prisma.organizationProviderConnection.count({
where: { organizationId: DEFAULT_ORG_ID, providerId: "openrouter" },
})).resolves.toBe(0);
await expect(prisma.providerCredentialVersion.count()).resolves.toBe(0);
});
});
@@ -125,10 +125,10 @@ describe("real Claude SDK sandbox boundary", () => {
workspaceDir: workspace,
},
systemPrompt: "Use the Bash tool exactly once, then report completion.",
providerEnv: {
providerProxyEnv: {
ANTHROPIC_BASE_URL: stub.baseUrl,
ANTHROPIC_AUTH_TOKEN: "provider-secret",
ANTHROPIC_API_KEY: "provider-api-secret",
ANTHROPIC_AUTH_TOKEN: "run-proxy-capability",
ANTHROPIC_API_KEY: "",
},
tools: ["bash"],
maxTurns: 3,
@@ -4,6 +4,15 @@ import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { promisify } from "node:util";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import {
DEFAULT_ORG_ID,
prisma,
resetDb,
testSecretEnvelope,
TEST_SECRET_KEY,
TEST_SECRET_KEY_ID,
} from "./helpers.js";
import { ProviderConnectionService } from "../../src/connections/providerConnections.js";
const execFileAsync = promisify(execFile);
const TEST_DATABASE_URL = "postgresql://paradigm:paradigm@127.0.0.1:5432/cph_hub_test";
@@ -16,6 +25,7 @@ describe("deployment preflight CLI", () => {
let marker: string;
beforeEach(async () => {
await resetDb();
root = await mkdtemp(join(tmpdir(), "cph-deployment-preflight-"));
baseDir = join(root, "deploy");
hubDir = join(baseDir, "current", "hub");
@@ -93,6 +103,23 @@ describe("deployment preflight CLI", () => {
expect(result.stderr).toContain("PostgreSQL authenticated query failed for DATABASE_URL");
});
it("rejects a structurally valid keyring that cannot authenticate stored envelopes", async () => {
await prismaActorForEnvelope();
const workspaceRoot = join(persistentDir, "workspaces");
const fixture = await createFixture({
workspaceRoot,
marker,
keyringKey: Buffer.alloc(32, "wrong"),
});
const result = await runCli(fixture.args);
expect(result.exitCode).toBe(1);
expect(result.stderr).toContain("stored provider envelope verification failed");
expect(result.stderr).toContain("secret envelope authentication failed");
expect(result.stderr).not.toContain("stored-envelope-secret");
});
it("loads the actual Hub dependency graph and queries PostgreSQL in the runtime probe", async () => {
const result = await execFileAsync(
resolve("node_modules/.bin/tsx"),
@@ -138,6 +165,7 @@ describe("deployment preflight CLI", () => {
marker: string;
pgIsReadyScript?: string;
databaseUrl?: string;
keyringKey?: Buffer;
}): Promise<{ args: string[] }> {
const binDir = join(root, "bin");
const cphBin = join(binDir, "cph");
@@ -147,6 +175,7 @@ describe("deployment preflight CLI", () => {
const runuserBin = join(binDir, "runuser");
const setprivBin = join(binDir, "setpriv");
const envFile = join(root, "platform.env");
const keyringFile = join(root, "secret-keyring.json");
await Promise.all([
mkdir(binDir, { recursive: true }),
mkdir(join(hubDir, "dist"), { recursive: true }),
@@ -180,15 +209,17 @@ describe("deployment preflight CLI", () => {
`const { appendFileSync } = require("node:fs");\nappendFileSync(${JSON.stringify(options.marker)}, "service-prisma\\n");\n`,
),
writeFile(join(hubDir, "prisma", "schema.prisma"), "// fixture\n"),
writeFile(keyringFile, JSON.stringify({
version: 1,
activeKeyId: TEST_SECRET_KEY_ID,
keys: { [TEST_SECRET_KEY_ID]: (options.keyringKey ?? TEST_SECRET_KEY).toString("base64") },
}), { mode: 0o600 }),
]);
await writeFile(
envFile,
[
"NODE_ENV=production",
`DATABASE_URL=${options.databaseUrl ?? TEST_DATABASE_URL}`,
"ANTHROPIC_BASE_URL=https://openrouter.ai/api",
"ANTHROPIC_AUTH_TOKEN=provider-token",
"ANTHROPIC_API_KEY=",
"FEISHU_APP_ID=cli_app_id",
"FEISHU_APP_SECRET=feishu-app-secret",
"FEISHU_BOT_OPEN_ID=ou_bot",
@@ -207,6 +238,8 @@ describe("deployment preflight CLI", () => {
args: [
"--env-file",
envFile,
"--keyring-file",
keyringFile,
"--node-bin",
process.execPath,
"--runuser-bin",
@@ -236,6 +269,20 @@ describe("deployment preflight CLI", () => {
],
};
}
async function prismaActorForEnvelope(): Promise<void> {
await prisma.user.create({ data: { id: "preflight-admin", feishuOpenId: "ou_preflight", displayName: "Preflight" } });
await prisma.organizationMembership.create({
data: { organizationId: DEFAULT_ORG_ID, userId: "preflight-admin", role: "ADMIN" },
});
await new ProviderConnectionService(prisma, testSecretEnvelope, async () => {}).rotateByok({
organizationId: DEFAULT_ORG_ID,
providerId: "openrouter",
actorUserId: "preflight-admin",
baseUrl: "https://provider.example/api",
authToken: "stored-envelope-secret",
});
}
});
async function executable(path: string, contents: string): Promise<void> {
+9
View File
@@ -17,9 +17,16 @@ import type {
} from "@ai-sdk/provider";
import type { FeishuRuntime } from "../../src/feishu/client.js";
import type { ModelFactory } from "../../src/agent/runner.js";
import { LocalSecretEnvelope } from "../../src/security/secretEnvelope.js";
export const TEST_DATABASE_URL = "postgresql://paradigm:paradigm@127.0.0.1:5432/cph_hub_test";
export const DEFAULT_ORG_ID = "org_test_default";
export const TEST_SECRET_KEY_ID = "test-active";
export const TEST_SECRET_KEY = Buffer.alloc(32, "k");
export const testSecretEnvelope = new LocalSecretEnvelope({
activeKeyId: TEST_SECRET_KEY_ID,
keys: new Map([[TEST_SECRET_KEY_ID, TEST_SECRET_KEY]]),
});
export const prisma = new PrismaClient({
datasources: { db: { url: TEST_DATABASE_URL } },
@@ -29,6 +36,8 @@ export const prisma = new PrismaClient({
export async function resetDb(): Promise<void> {
const tables = [
"FeishuEventReceipt",
"ProviderCredentialVersion",
"OrganizationProviderConnection",
"AgentFileChange",
"AgentMessage",
"AuditEntry",
@@ -0,0 +1,127 @@
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterAll, beforeEach, describe, expect, it } from "vitest";
import { ProviderConnectionService, verifyStoredProviderEnvelopes } from "../../src/connections/providerConnections.js";
import { LocalSecretEnvelope, loadLocalSecretKeyringFile, type SecretEnvelopeV1 } from "../../src/security/secretEnvelope.js";
import { LOCAL_KEK_ROTATION_ADVISORY_LOCK, rotateLocalKek } from "../../src/security/localKekRotation.js";
import { DEFAULT_ORG_ID, prisma, resetDb } from "./helpers.js";
const temporaryDirectories: string[] = [];
describe("local KEK rotation", () => {
beforeEach(async () => {
await resetDb();
});
afterAll(async () => {
await Promise.all(temporaryDirectories.splice(0).map((directory) => rm(directory, { recursive: true, force: true })));
await prisma.$disconnect();
});
it("atomically retains the old KEK, rewraps DEKs, and authenticates every envelope", async () => {
const oldKey = Buffer.alloc(32, "o");
const oldSecrets = new LocalSecretEnvelope({
activeKeyId: "local-old",
keys: new Map([["local-old", oldKey]]),
});
await prisma.user.create({ data: { id: "rotation-admin", feishuOpenId: "ou_rotation", displayName: "Rotation" } });
await prisma.organizationMembership.create({
data: { organizationId: DEFAULT_ORG_ID, userId: "rotation-admin", role: "OWNER" },
});
await new ProviderConnectionService(prisma, oldSecrets, async () => {}).rotateByok({
organizationId: DEFAULT_ORG_ID,
providerId: "openrouter",
actorUserId: "rotation-admin",
baseUrl: "https://provider.example/api",
authToken: "credential-stays-encrypted",
});
const before = await prisma.providerCredentialVersion.findFirstOrThrow({
include: { connection: true },
});
const beforeEnvelope = before.envelope as unknown as SecretEnvelopeV1;
const directory = await mkdtemp(join(tmpdir(), "cph-kek-rotation-"));
temporaryDirectories.push(directory);
const keyringFile = join(directory, "keyring.json");
await writeFile(keyringFile, JSON.stringify({
version: 1,
activeKeyId: "local-old",
keys: { "local-old": oldKey.toString("base64") },
}), { mode: 0o600 });
const expectedOwner = {
uid: process.getuid?.() ?? 0,
gid: process.getgid?.() ?? 0,
};
if (expectedOwner.uid !== 0 || expectedOwner.gid !== 0) {
await expect(rotateLocalKek({
keyringFile,
prisma,
generatedKey: Buffer.alloc(32, "x"),
})).rejects.toThrow("must be owned by 0:0");
}
let releaseLock!: () => void;
const releaseLockPromise = new Promise<void>((resolve) => {
releaseLock = resolve;
});
let lockAcquired!: () => void;
const lockAcquiredPromise = new Promise<void>((resolve) => {
lockAcquired = resolve;
});
const lockHolder = prisma.$transaction(async (tx) => {
await tx.$queryRaw<Array<{ readonly locked: string }>>`
SELECT pg_advisory_xact_lock(${LOCAL_KEK_ROTATION_ADVISORY_LOCK})::text AS "locked"
`;
lockAcquired();
await releaseLockPromise;
}, { timeout: 30_000 });
await lockAcquiredPromise;
try {
await expect(rotateLocalKek({
keyringFile,
prisma,
generatedKey: Buffer.alloc(32, "x"),
expectedOwner,
})).rejects.toThrow("rotation lock is already held");
} finally {
releaseLock();
await lockHolder;
}
const result = await rotateLocalKek({
keyringFile,
prisma,
now: new Date("2026-07-10T12:34:56.000Z"),
generatedKey: Buffer.alloc(32, "n"),
expectedOwner,
});
expect(result).toMatchObject({
previousActiveKeyId: "local-old",
activeKeyId: expect.stringMatching(/^local-20260710T123456Z-/),
rewrapped: 1,
verified: 1,
});
const after = await prisma.providerCredentialVersion.findUniqueOrThrow({ where: { id: before.id } });
const afterEnvelope = after.envelope as unknown as SecretEnvelopeV1;
expect(after.keyId).toBe(result.activeKeyId);
expect(afterEnvelope.payload).toEqual(beforeEnvelope.payload);
expect(afterEnvelope.wrappedDek).not.toEqual(beforeEnvelope.wrappedDek);
const keyringSource = await readFile(keyringFile, "utf8");
expect(keyringSource).toContain("local-old");
expect(keyringSource).toContain(result.activeKeyId);
expect(keyringSource).not.toContain("credential-stays-encrypted");
const rotatedSecrets = new LocalSecretEnvelope(await loadLocalSecretKeyringFile(keyringFile));
await expect(verifyStoredProviderEnvelopes(prisma, rotatedSecrets)).resolves.toBe(1);
expect(() => oldSecrets.decryptJson({
purpose: "provider-connection",
organizationId: before.connection.organizationId,
connectionId: before.connection.id,
secretVersionId: before.id,
}, afterEnvelope)).toThrow("secret envelope key is unavailable");
await expect(prisma.auditEntry.count({
where: { action: "provider_secret.kek_rewrapped", organizationId: DEFAULT_ORG_ID },
})).resolves.toBe(1);
});
});
@@ -0,0 +1,170 @@
import { afterAll, beforeEach, describe, expect, it } from "vitest";
import { ProviderConnectionService } from "../../src/connections/providerConnections.js";
import { DatabaseRuntimeSettings } from "../../src/settings/runtime.js";
import type { ProviderUpstreamCredential } from "../../src/connections/providerProxy.js";
import { DEFAULT_ORG_ID, prisma, resetDb, seedTestOrganization, testSecretEnvelope } from "./helpers.js";
const secrets = testSecretEnvelope;
describe("project-scoped provider runtime settings", () => {
beforeEach(async () => {
await resetDb();
});
afterAll(async () => {
await prisma.$disconnect();
});
it("resolves two projects only through their own Organization connection", async () => {
await seedTestOrganization("org_other", "other");
await Promise.all([
prisma.user.create({ data: { id: "user-a", feishuOpenId: "ou_a", displayName: "A" } }),
prisma.user.create({ data: { id: "user-b", feishuOpenId: "ou_b", displayName: "B" } }),
prisma.project.create({
data: {
id: "project-a",
organizationId: DEFAULT_ORG_ID,
name: "A",
workspaceDir: "/tmp/project-a",
},
}),
prisma.project.create({
data: {
id: "project-b",
organizationId: "org_other",
name: "B",
workspaceDir: "/tmp/project-b",
},
}),
]);
await Promise.all([
prisma.organizationMembership.create({
data: { organizationId: DEFAULT_ORG_ID, userId: "user-a", role: "ADMIN" },
}),
prisma.organizationMembership.create({
data: { organizationId: "org_other", userId: "user-b", role: "OWNER" },
}),
]);
const service = new ProviderConnectionService(prisma, secrets, async () => {});
await service.rotateByok({
organizationId: DEFAULT_ORG_ID,
providerId: "openrouter",
actorUserId: "user-a",
baseUrl: "https://provider-a.example/api",
authToken: "provider-a-token",
anthropicApiKey: "provider-a-api-key",
});
await service.rotateByok({
organizationId: "org_other",
providerId: "openrouter",
actorUserId: "user-b",
baseUrl: "https://provider-b.example/api",
authToken: "provider-b-token",
});
const leasedCredentials: ProviderUpstreamCredential[] = [];
const settings = new DatabaseRuntimeSettings(prisma, secrets, {
ANTHROPIC_AUTH_TOKEN: "forbidden-global-token",
ANTHROPIC_BASE_URL: "https://forbidden-global.example",
}, async (credential) => {
leasedCredentials.push(credential);
return {
sdkEnv: {
ANTHROPIC_BASE_URL: "http://127.0.0.1:12345",
ANTHROPIC_AUTH_TOKEN: `run-capability-${leasedCredentials.length}`,
ANTHROPIC_API_KEY: "",
},
sensitiveValues: [`run-capability-${leasedCredentials.length}`],
async close() {},
};
});
const providerA = await settings.provider("openrouter", { projectId: "project-a" });
const providerB = await settings.provider("openrouter", { projectId: "project-b" });
expect(JSON.stringify([providerA, providerB])).not.toContain("provider-a-token");
expect(JSON.stringify([providerA, providerB])).not.toContain("provider-b-token");
const leaseA = await providerA.openAgentLease({ runId: "run-a" });
const leaseB = await providerB.openAgentLease({ runId: "run-b" });
expect(leaseA.sdkEnv).toMatchObject({
ANTHROPIC_BASE_URL: "http://127.0.0.1:12345",
ANTHROPIC_AUTH_TOKEN: "run-capability-1",
ANTHROPIC_API_KEY: "",
});
expect(leaseB.sdkEnv.ANTHROPIC_AUTH_TOKEN).toBe("run-capability-2");
expect(leasedCredentials).toEqual([
{
baseUrl: "https://provider-a.example/api",
authToken: "provider-a-token",
anthropicApiKey: "provider-a-api-key",
},
{
baseUrl: "https://provider-b.example/api",
authToken: "provider-b-token",
anthropicApiKey: "",
},
]);
});
it("fails closed without project scope or for missing, inactive, and corrupt connections", async () => {
await prisma.user.create({ data: { id: "user-a", feishuOpenId: "ou_a", displayName: "A" } });
await prisma.organizationMembership.create({
data: { organizationId: DEFAULT_ORG_ID, userId: "user-a", role: "ADMIN" },
});
await prisma.project.create({
data: {
id: "project-a",
organizationId: DEFAULT_ORG_ID,
name: "A",
workspaceDir: "/tmp/project-a",
},
});
const settings = new DatabaseRuntimeSettings(prisma, secrets, {
ANTHROPIC_AUTH_TOKEN: "must-not-be-a-fallback",
});
await expect(settings.provider("openrouter")).rejects.toThrow("projectId is required");
await expect(settings.provider("openrouter", { projectId: "project-a" })).rejects.toThrow(
"active provider connection not found",
);
const service = new ProviderConnectionService(prisma, secrets, async () => {});
await service.rotateByok({
organizationId: DEFAULT_ORG_ID,
providerId: "openrouter",
actorUserId: "user-a",
baseUrl: "https://provider-a.example/api",
authToken: "provider-a-token",
});
await prisma.organization.update({
where: { id: DEFAULT_ORG_ID },
data: { status: "SUSPENDED" },
});
await expect(settings.provider("openrouter", { projectId: "project-a" })).rejects.toThrow(
`organization ${DEFAULT_ORG_ID} is SUSPENDED`,
);
await prisma.organization.update({ where: { id: DEFAULT_ORG_ID }, data: { status: "ACTIVE" } });
const connection = await prisma.organizationProviderConnection.findUniqueOrThrow({
where: {
organizationId_providerId: { organizationId: DEFAULT_ORG_ID, providerId: "openrouter" },
},
});
const version = await prisma.providerCredentialVersion.findUniqueOrThrow({
where: { id: connection.activeSecretVersionId ?? "missing" },
});
const envelope = structuredClone(version.envelope) as {
payload: { ciphertext: string };
};
const bytes = Buffer.from(envelope.payload.ciphertext, "base64");
bytes[0] = (bytes[0] ?? 0) ^ 1;
envelope.payload.ciphertext = bytes.toString("base64");
await prisma.providerCredentialVersion.update({
where: { id: version.id },
data: { envelope },
});
const corruptProvider = await settings.provider("openrouter", { projectId: "project-a" });
await expect(corruptProvider.openAgentLease({ runId: "run-corrupt" })).rejects.toThrow(
"secret envelope authentication failed",
);
});
});
+17 -4
View File
@@ -1,8 +1,10 @@
import { createServer } from "node:net";
import { once } from "node:events";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { startHub } from "../../src/hub.js";
import { TEST_DATABASE_URL } from "./helpers.js";
import { resetDb, TEST_DATABASE_URL, TEST_SECRET_KEY, TEST_SECRET_KEY_ID } from "./helpers.js";
const ENV_KEYS = [
"DATABASE_URL",
@@ -15,6 +17,7 @@ const ENV_KEYS = [
"HUB_SESSION_SECRET",
"HUB_PUBLIC_BASE_URL",
"HUB_FEISHU_LISTENER_ENABLED",
"HUB_SECRET_KEYRING_FILE",
"HOST",
"PORT",
] as const;
@@ -30,16 +33,23 @@ describe("Hub startup", () => {
});
it("rejects startup before the Feishu listener when the HTTP bind fails", async () => {
await resetDb();
const blocker = createServer();
blocker.listen(0, "127.0.0.1");
await once(blocker, "listening");
const address = blocker.address();
if (address === null || typeof address === "string") throw new Error("expected an INET test listener");
const keyringDirectory = await mkdtemp(join(tmpdir(), "cph-hub-start-keyring-"));
const keyringFile = join(keyringDirectory, "keyring.json");
await writeFile(keyringFile, JSON.stringify({
version: 1,
activeKeyId: TEST_SECRET_KEY_ID,
keys: { [TEST_SECRET_KEY_ID]: TEST_SECRET_KEY.toString("base64") },
}), { mode: 0o600 });
Object.assign(process.env, {
DATABASE_URL: TEST_DATABASE_URL,
ANTHROPIC_AUTH_TOKEN: "test-provider-token",
ANTHROPIC_API_KEY: "",
FEISHU_APP_ID: "test-app",
FEISHU_APP_SECRET: "test-secret",
FEISHU_BOT_OPEN_ID: "ou_test_bot",
@@ -47,15 +57,18 @@ describe("Hub startup", () => {
HUB_SESSION_SECRET: "integration-session-secret-with-32-bytes",
HUB_PUBLIC_BASE_URL: "https://hub.example.test",
HUB_FEISHU_LISTENER_ENABLED: "false",
HUB_SECRET_KEYRING_FILE: keyringFile,
HOST: "127.0.0.1",
PORT: String(address.port),
});
try {
const { startHub } = await import("../../src/hub.js");
await expect(startHub()).rejects.toMatchObject({ code: "EADDRINUSE" });
} finally {
blocker.close();
await once(blocker, "close");
await rm(keyringDirectory, { recursive: true, force: true });
}
});
});
+13 -10
View File
@@ -30,13 +30,16 @@ function makeTestSettings(models: InMemoryModelRegistry): RuntimeSettings {
async provider(providerId) {
return {
id: providerId,
baseUrl: "https://openrouter.ai/api",
authToken: "test-token",
anthropicApiKey: "",
sdkEnv: {
ANTHROPIC_BASE_URL: "https://openrouter.ai/api",
ANTHROPIC_AUTH_TOKEN: "test-token",
ANTHROPIC_API_KEY: "",
async openAgentLease() {
return {
sdkEnv: {
ANTHROPIC_BASE_URL: "http://127.0.0.1:12345",
ANTHROPIC_AUTH_TOKEN: "test-run-capability",
ANTHROPIC_API_KEY: "",
},
sensitiveValues: ["test-run-capability"],
async close() {},
};
},
};
},
@@ -153,9 +156,9 @@ describe("trigger full lifecycle (integration)", () => {
});
expect(rt.sentTexts.some((text) => text.includes("mock response") && text.includes("本次成本: $0.0023"))).toBe(true);
expect(runAgentCalls).toHaveLength(1);
expect(runAgentCalls[0]?.providerEnv).toMatchObject({
ANTHROPIC_BASE_URL: "https://openrouter.ai/api",
ANTHROPIC_AUTH_TOKEN: "test-token",
expect(runAgentCalls[0]?.providerProxyEnv).toMatchObject({
ANTHROPIC_BASE_URL: "http://127.0.0.1:12345",
ANTHROPIC_AUTH_TOKEN: "test-run-capability",
ANTHROPIC_API_KEY: "",
});
expect(runAgentCalls[0]?.maxTurns).toBe(7);