import { randomUUID } from "node:crypto"; import type { Prisma, PrismaClient } from "@prisma/client"; import { lockActiveOrganization } from "../org/status.js"; import { LocalSecretEnvelope, type SecretEnvelopeV1, } from "../security/secretEnvelope.js"; import { probeOpenRouterCredential, type ProviderReadinessProbe } from "./providerReadiness.js"; const PROVIDER_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/; export interface ProviderCredentialInput { readonly baseUrl: string; readonly authToken: string; readonly anthropicApiKey?: string; } export interface RotateByokProviderInput extends ProviderCredentialInput { readonly organizationId: string; readonly providerId: string; readonly actorUserId: string; } export interface ProviderConnectionMetadata { readonly id: string; readonly providerId: string; readonly mode: "BYOK" | "PLATFORM_MANAGED"; readonly status: "DRAFT" | "ACTIVE" | "DISABLED"; readonly activeVersion: number | null; readonly keyId: string | null; readonly createdAt: Date; readonly updatedAt: Date; } export interface ProviderConnectionWriteResult extends ProviderConnectionMetadata { readonly created: boolean; } export interface ProviderSecretPayloadV1 { readonly schemaVersion: 1; readonly baseUrl: string; readonly authToken: string; readonly anthropicApiKey: string; } export class ProviderConnectionService { constructor( private readonly prisma: PrismaClient, private readonly secrets: LocalSecretEnvelope, private readonly readinessProbe: ProviderReadinessProbe = probeOpenRouterCredential, ) {} async rotateByok(input: RotateByokProviderInput): Promise { const payload = validateProviderCredential(input); await this.prisma.$transaction(async (tx) => { await requireByokActor(tx, input); }); await this.readinessProbe({ providerId: input.providerId, baseUrl: payload.baseUrl, authToken: payload.authToken, anthropicApiKey: payload.anthropicApiKey, }); return this.prisma.$transaction(async (tx) => { await requireByokActor(tx, input); const connection = await tx.organizationProviderConnection.upsert({ where: { organizationId_providerId: { organizationId: input.organizationId, providerId: input.providerId, }, }, update: {}, create: { id: randomUUID(), organizationId: input.organizationId, providerId: input.providerId, mode: "BYOK", status: "DRAFT", }, }); await lockConnection(tx, connection.id); const locked = await tx.organizationProviderConnection.findUniqueOrThrow({ where: { id: connection.id }, include: { activeSecretVersion: { select: { id: true } }, secretVersions: { orderBy: { version: "desc" }, take: 1, select: { version: true }, }, }, }); if (locked.organizationId !== input.organizationId || locked.providerId !== input.providerId) { throw new Error("provider connection scope changed while rotating credential"); } if (locked.mode !== "BYOK") { throw new Error( "provider connection is managed by platform administrators and cannot be changed through organization API", ); } const version = (locked.secretVersions[0]?.version ?? 0) + 1; const secretVersionId = randomUUID(); const envelope = this.secrets.encryptJson({ purpose: "provider-connection", organizationId: input.organizationId, connectionId: locked.id, secretVersionId, }, payload); const now = new Date(); const secretVersion = await tx.providerCredentialVersion.create({ data: { id: secretVersionId, connectionId: locked.id, version, envelopeVersion: envelope.version, keyId: envelope.keyId, envelope: envelope as unknown as Prisma.InputJsonValue, createdByUserId: input.actorUserId, }, }); if (locked.activeSecretVersion !== null) { await tx.providerCredentialVersion.update({ where: { id: locked.activeSecretVersion.id }, data: { retiredAt: now }, }); } const activated = await tx.organizationProviderConnection.update({ where: { id: locked.id }, data: { status: "ACTIVE", activeSecretVersionId: secretVersion.id, activatedAt: now, disabledAt: null, }, }); await tx.auditEntry.create({ data: { organizationId: input.organizationId, actorUserId: input.actorUserId, action: version === 1 ? "provider_connection.created" : "provider_connection.rotated", metadata: { connectionId: locked.id, providerId: input.providerId, mode: "BYOK", status: "ACTIVE", secretVersion: version, envelopeVersion: envelope.version, keyId: envelope.keyId, }, }, }); return { ...metadata(activated, { version, keyId: secretVersion.keyId }), created: version === 1, }; }); } async list(organizationId: string): Promise { return this.prisma.$transaction(async (tx) => { await lockActiveOrganization(tx, organizationId); const connections = await tx.organizationProviderConnection.findMany({ where: { organizationId }, orderBy: [{ providerId: "asc" }, { createdAt: "asc" }], include: { activeSecretVersion: { select: { version: true, keyId: true }, }, }, }); return connections.map((connection) => metadata(connection, connection.activeSecretVersion)); }); } } export function decryptStoredProviderCredential( secrets: LocalSecretEnvelope, input: { readonly organizationId: string; readonly connectionId: string; readonly providerId: string; readonly secretVersionId: string; readonly envelopeVersion: number; readonly keyId: string; readonly envelope: Prisma.JsonValue; }, ): ProviderSecretPayloadV1 { if (input.envelopeVersion !== 1) { throw new Error(`unsupported provider secret envelope version: ${input.envelopeVersion}`); } const envelope = input.envelope as unknown as SecretEnvelopeV1; if (envelope.keyId !== input.keyId || envelope.version !== input.envelopeVersion) { throw new Error("provider secret envelope metadata mismatch"); } const payload = secrets.decryptJson({ purpose: "provider-connection", organizationId: input.organizationId, connectionId: input.connectionId, secretVersionId: input.secretVersionId, }, envelope); return validateStoredProviderPayload(payload, input.providerId); } export async function verifyStoredProviderEnvelopes( prisma: PrismaClient | Prisma.TransactionClient, secrets: LocalSecretEnvelope, ): Promise { const versions = await prisma.providerCredentialVersion.findMany({ select: { id: true, envelopeVersion: true, keyId: true, envelope: true, connection: { select: { id: true, organizationId: true, providerId: true }, }, }, }); for (const version of versions) { decryptStoredProviderCredential(secrets, { organizationId: version.connection.organizationId, connectionId: version.connection.id, providerId: version.connection.providerId, secretVersionId: version.id, envelopeVersion: version.envelopeVersion, keyId: version.keyId, envelope: version.envelope, }); } return versions.length; } export async function rewrapStoredProviderEnvelopes( prisma: Prisma.TransactionClient, secrets: LocalSecretEnvelope, ): Promise<{ readonly verified: number; readonly rewrapped: number }> { const versions = await prisma.providerCredentialVersion.findMany({ select: { id: true, envelopeVersion: true, keyId: true, envelope: true, connection: { select: { id: true, organizationId: true, providerId: true }, }, }, orderBy: { createdAt: "asc" }, }); let rewrapped = 0; for (const version of versions) { const binding = { purpose: "provider-connection", organizationId: version.connection.organizationId, connectionId: version.connection.id, secretVersionId: version.id, } as const; decryptStoredProviderCredential(secrets, { organizationId: version.connection.organizationId, connectionId: version.connection.id, providerId: version.connection.providerId, secretVersionId: version.id, envelopeVersion: version.envelopeVersion, keyId: version.keyId, envelope: version.envelope, }); if (version.keyId === secrets.activeKeyId) continue; const envelope = version.envelope as unknown as SecretEnvelopeV1; const nextEnvelope = secrets.rewrap(binding, envelope); const updated = await prisma.providerCredentialVersion.updateMany({ where: { id: version.id, keyId: version.keyId }, data: { keyId: nextEnvelope.keyId, envelope: nextEnvelope as unknown as Prisma.InputJsonValue, }, }); if (updated.count !== 1) { throw new Error(`provider secret envelope changed during KEK rotation: ${version.id}`); } await prisma.auditEntry.create({ data: { organizationId: version.connection.organizationId, action: "provider_secret.kek_rewrapped", metadata: { connectionId: version.connection.id, providerId: version.connection.providerId, secretVersionId: version.id, previousKeyId: version.keyId, keyId: nextEnvelope.keyId, }, }, }); rewrapped += 1; } const verified = await verifyStoredProviderEnvelopes(prisma, secrets); const remaining = await prisma.providerCredentialVersion.count({ where: { keyId: { not: secrets.activeKeyId } }, }); if (remaining !== 0) { throw new Error(`KEK rotation left ${remaining} provider envelope(s) on a non-active key`); } return { verified, rewrapped }; } function validateProviderCredential(input: RotateByokProviderInput): ProviderSecretPayloadV1 { if (!PROVIDER_ID_PATTERN.test(input.providerId)) { throw new Error("invalid provider connection id"); } let baseUrl: URL; try { baseUrl = new URL(input.baseUrl); } catch (error) { throw new Error("invalid provider base URL", { cause: error }); } if (baseUrl.protocol !== "https:") { throw new Error("invalid provider base URL: HTTPS is required"); } const authToken = input.authToken.trim(); if (authToken === "") { throw new Error("provider auth token is required"); } return { schemaVersion: 1, baseUrl: baseUrl.toString().replace(/\/$/, ""), authToken, anthropicApiKey: input.anthropicApiKey?.trim() ?? "", }; } function metadata( connection: { readonly id: string; readonly providerId: string; readonly mode: "BYOK" | "PLATFORM_MANAGED"; readonly status: "DRAFT" | "ACTIVE" | "DISABLED"; readonly createdAt: Date; readonly updatedAt: Date; }, secret: { readonly version: number; readonly keyId: string } | null, ): ProviderConnectionMetadata { return { id: connection.id, providerId: connection.providerId, mode: connection.mode, status: connection.status, activeVersion: secret?.version ?? null, keyId: secret?.keyId ?? null, createdAt: connection.createdAt, updatedAt: connection.updatedAt, }; } function validateStoredProviderPayload(value: unknown, providerId: string): ProviderSecretPayloadV1 { if (!isRecord(value) || value["schemaVersion"] !== 1 || typeof value["baseUrl"] !== "string" || typeof value["authToken"] !== "string" || typeof value["anthropicApiKey"] !== "string" || value["authToken"].trim() === "") { throw new Error(`invalid encrypted provider credential payload: ${providerId}`); } let baseUrl: URL; try { baseUrl = new URL(value["baseUrl"]); } catch (error) { throw new Error(`invalid encrypted provider credential payload: ${providerId}`, { cause: error }); } if (baseUrl.protocol !== "https:") { throw new Error(`invalid encrypted provider credential payload: ${providerId}`); } return { schemaVersion: 1, baseUrl: value["baseUrl"], authToken: value["authToken"], anthropicApiKey: value["anthropicApiKey"], }; } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } async function lockConnection(tx: Prisma.TransactionClient, connectionId: string): Promise { await tx.$queryRaw`SELECT "id" FROM "OrganizationProviderConnection" WHERE "id" = ${connectionId} FOR UPDATE`; } async function requireByokActor( tx: Prisma.TransactionClient, input: Pick, ): Promise { await lockActiveOrganization(tx, input.organizationId); const [authorizedActor, existingConnection] = await Promise.all([ tx.organizationMembership.findFirst({ where: { organizationId: input.organizationId, userId: input.actorUserId, role: { in: ["OWNER", "ADMIN"] }, revokedAt: null, }, select: { id: true }, }), tx.organizationProviderConnection.findUnique({ where: { organizationId_providerId: { organizationId: input.organizationId, providerId: input.providerId, }, }, select: { mode: true }, }), ]); if (authorizedActor === null) { throw new Error("BYOK provider rotation requires an active Organization OWNER or ADMIN"); } if (existingConnection?.mode === "PLATFORM_MANAGED") { throw new Error( "provider connection is managed by platform administrators and cannot be changed through organization API", ); } }