Files
curriculum-project-hub/hub/src/deployment/bootstrap-silo.ts
T

441 lines
16 KiB
TypeScript

import { randomUUID } from "node:crypto";
import type { Prisma, PrismaClient } from "@prisma/client";
import {
fingerprintFeishuAppId,
validateFeishuApplicationCredential,
type FeishuApplicationCredentialInput,
} from "../connections/feishuApplicationConnections.js";
import {
decryptStoredProviderCredential,
ProviderConnectionService,
type ProviderCredentialInput,
} from "../connections/providerConnections.js";
import { probeFeishuApplication, type FeishuReadinessProbe } from "../connections/feishuReadiness.js";
import { probeOpenRouterCredential, type ProviderReadinessProbe } from "../connections/providerReadiness.js";
import { deterministicFeishuUserId, scopedFeishuPrincipalId } from "../feishu/identityNamespace.js";
import { LocalSecretEnvelope } from "../security/secretEnvelope.js";
export interface AlphaSiloBootstrapInput {
readonly organization: { readonly id: string; readonly slug: string; readonly name: string };
readonly owner: { readonly openId: string; readonly displayName: string; readonly unionId?: string };
readonly feishu: FeishuApplicationCredentialInput;
readonly provider: ProviderCredentialInput & { readonly providerId: string };
readonly teams?: readonly {
readonly slug: string;
readonly name: string;
readonly description?: string;
}[];
}
export interface AlphaSiloBootstrapResult {
readonly organizationId: string;
readonly ownerUserId: string;
readonly feishuConnectionId: string;
readonly initialized: boolean;
readonly providerConfigured: boolean;
}
/**
* Idempotently bootstrap the one Organization allowed in an Alpha Silo.
* Secrets are accepted only in-memory and persisted as authenticated envelopes.
*/
export async function bootstrapAlphaSilo(
prisma: PrismaClient,
secrets: LocalSecretEnvelope,
input: AlphaSiloBootstrapInput,
probes: {
readonly feishu?: FeishuReadinessProbe;
readonly provider?: ProviderReadinessProbe;
} = {},
): Promise<AlphaSiloBootstrapResult> {
const normalized = validateInput(input);
await removePristineLegacyMigrationOrganization(prisma);
const existing = await loadExistingSilo(prisma, normalized.organization.id, normalized.owner.openId);
let initialized = false;
let ownerUserId: string;
let feishuConnectionId: string;
if (existing === null) {
await (probes.feishu ?? probeFeishuApplication)(normalized.feishu);
const initializedSilo = await initializeSilo(prisma, secrets, normalized);
initialized = true;
ownerUserId = initializedSilo.ownerUserId;
feishuConnectionId = initializedSilo.feishuConnectionId;
} else {
ownerUserId = existing.ownerUserId;
feishuConnectionId = existing.feishuConnectionId;
const expectedFingerprint = fingerprintFeishuAppId(normalized.feishu.appId);
if (existing.appIdentityFingerprint !== expectedFingerprint) {
throw new Error("bootstrap Feishu app identity does not match the existing Silo");
}
}
const providerService = new ProviderConnectionService(
prisma,
secrets,
probes.provider ?? probeOpenRouterCredential,
);
const existingProviders = await providerService.list(normalized.organization.id);
const existingProvider = existingProviders.find((connection) => connection.providerId === normalized.provider.providerId);
if (existingProvider === undefined) {
await providerService.rotateByok({
organizationId: normalized.organization.id,
actorUserId: ownerUserId,
providerId: normalized.provider.providerId,
baseUrl: normalized.provider.baseUrl,
authToken: normalized.provider.authToken,
...(normalized.provider.anthropicApiKey !== undefined
? { anthropicApiKey: normalized.provider.anthropicApiKey }
: {}),
});
} else if (existingProvider.status !== "ACTIVE" || existingProvider.mode !== "BYOK") {
throw new Error(`bootstrap provider ${normalized.provider.providerId} exists but is not ACTIVE BYOK`);
} else {
const stored = await loadExistingProviderCredential(
prisma,
secrets,
normalized.organization.id,
normalized.provider.providerId,
);
if (stored.baseUrl !== normalized.provider.baseUrl || stored.authToken !== normalized.provider.authToken ||
stored.anthropicApiKey !== (normalized.provider.anthropicApiKey ?? "")) {
throw new Error(`bootstrap provider ${normalized.provider.providerId} credential does not match existing Silo`);
}
}
await ensureBootstrapTeams(prisma, normalized.organization.id, ownerUserId, normalized.teams ?? []);
return {
organizationId: normalized.organization.id,
ownerUserId,
feishuConnectionId,
initialized,
providerConfigured: true,
};
}
const LEGACY_MIGRATION_ORGANIZATION = {
id: "org_default",
slug: "legacy-default",
name: "Legacy Default Organization",
inboxId: "folder_inbox_2b99350e0db97ad0cbcb55c20ee8bafa",
} as const;
/**
* A fresh database still receives the compatibility Organization inserted by
* the tenant-root migration, plus its later default settings and Inbox. An
* Alpha Silo bootstrap may replace only that exact, otherwise-unused scaffold.
* Any tenant data or shape drift remains a hard manual-repair error.
*/
async function removePristineLegacyMigrationOrganization(prisma: PrismaClient): Promise<void> {
await prisma.$transaction(async (tx) => {
const organization = await tx.organization.findUnique({
where: { id: LEGACY_MIGRATION_ORGANIZATION.id },
include: {
memberships: { take: 1, select: { id: true } },
projects: { take: 1, select: { id: true } },
teams: { take: 1, select: { id: true } },
externalDirectoryConnections: { take: 1, select: { id: true } },
providerConnections: { take: 1, select: { id: true } },
feishuApplicationConnection: { select: { id: true } },
auditEntries: { take: 1, select: { id: true } },
projectSettings: true,
folders: {
take: 2,
select: { id: true, parentId: true, name: true, sortKey: true, archivedAt: true },
},
},
});
if (organization === null) return;
const pristine =
organization.slug === LEGACY_MIGRATION_ORGANIZATION.slug &&
organization.name === LEGACY_MIGRATION_ORGANIZATION.name &&
organization.status === "ACTIVE" &&
organization.memberships.length === 0 &&
organization.projects.length === 0 &&
organization.teams.length === 0 &&
organization.externalDirectoryConnections.length === 0 &&
organization.providerConnections.length === 0 &&
organization.feishuApplicationConnection === null &&
organization.auditEntries.length === 0 &&
organization.projectSettings?.membersCanCreateProjects === true &&
organization.folders.length === 1 &&
organization.folders[0]?.id === LEGACY_MIGRATION_ORGANIZATION.inboxId &&
organization.folders[0].parentId === null &&
organization.folders[0].name === "Inbox" &&
organization.folders[0].sortKey === "000000" &&
organization.folders[0].archivedAt === null;
if (pristine) {
await tx.organization.delete({ where: { id: organization.id } });
}
});
}
async function ensureBootstrapTeams(
prisma: PrismaClient,
organizationId: string,
ownerUserId: string,
teams: NonNullable<AlphaSiloBootstrapInput["teams"]>,
): Promise<void> {
await prisma.$transaction(async (tx) => {
for (const team of teams) {
const existing = await tx.team.findFirst({
where: { organizationId, slug: team.slug, archivedAt: null },
select: { id: true, name: true, description: true },
});
if (existing !== null && (existing.name !== team.name || existing.description !== (team.description ?? null))) {
throw new Error(`bootstrap Team ${team.slug} does not match the existing Silo`);
}
const teamId = existing?.id ?? (await tx.team.create({
data: {
organizationId,
slug: team.slug,
name: team.name,
...(team.description !== undefined ? { description: team.description } : {}),
},
select: { id: true },
})).id;
const membership = await tx.teamMembership.findFirst({
where: { teamId, userId: ownerUserId, revokedAt: null },
select: { id: true },
});
if (membership === null) await tx.teamMembership.create({ data: { teamId, userId: ownerUserId } });
}
});
}
async function initializeSilo(
prisma: PrismaClient,
secrets: LocalSecretEnvelope,
input: AlphaSiloBootstrapInput,
): Promise<{ readonly ownerUserId: string; readonly feishuConnectionId: string }> {
const connectionId = randomUUID();
const secretVersionId = randomUUID();
const identityId = randomUUID();
const ownerUserId = deterministicFeishuUserId(connectionId, input.owner.openId);
const appIdentityFingerprint = fingerprintFeishuAppId(input.feishu.appId);
const payload = validateFeishuApplicationCredential(input.feishu);
const envelope = secrets.encryptJson({
purpose: "feishu-application",
organizationId: input.organization.id,
connectionId,
secretVersionId,
}, payload);
await prisma.$transaction(async (tx) => {
const count = await tx.organization.count();
if (count !== 0) throw new Error(`Silo bootstrap requires an empty Organization set; found ${count}`);
await tx.organization.create({ data: { ...input.organization } });
await tx.organizationAgentRole.createMany({
data: [
{
organizationId: input.organization.id,
roleId: "draft",
label: "草稿",
sortOrder: 10,
isDefault: true,
},
{
organizationId: input.organization.id,
roleId: "review",
label: "审校",
sortOrder: 20,
},
],
});
await tx.organizationProjectSettings.create({
data: { organizationId: input.organization.id, membersCanCreateProjects: true },
});
await tx.folder.create({
data: {
organizationId: input.organization.id,
name: "Inbox",
sortKey: "000000",
},
});
await tx.user.create({
data: {
id: ownerUserId,
feishuOpenId: scopedFeishuPrincipalId("USER", connectionId, input.owner.openId),
displayName: input.owner.displayName,
},
});
await tx.organizationFeishuApplicationConnection.create({
data: {
id: connectionId,
organizationId: input.organization.id,
appIdentityFingerprint,
status: "DRAFT",
},
});
await tx.feishuUserIdentity.create({
data: {
id: identityId,
connectionId,
userId: ownerUserId,
openId: input.owner.openId,
...(input.owner.unionId !== undefined ? { unionId: input.owner.unionId } : {}),
},
});
await tx.organizationMembership.create({
data: { organizationId: input.organization.id, userId: ownerUserId, role: "OWNER" },
});
for (const team of input.teams ?? []) {
await tx.team.create({
data: {
organizationId: input.organization.id,
slug: team.slug,
name: team.name,
...(team.description !== undefined ? { description: team.description } : {}),
memberships: { create: { userId: ownerUserId } },
},
});
}
await tx.feishuApplicationCredentialVersion.create({
data: {
id: secretVersionId,
connectionId,
version: 1,
envelopeVersion: envelope.version,
keyId: envelope.keyId,
envelope: envelope as unknown as Prisma.InputJsonValue,
createdByUserId: ownerUserId,
},
});
await tx.organizationFeishuApplicationConnection.update({
where: { id: connectionId },
data: {
status: "ACTIVE",
activeSecretVersionId: secretVersionId,
activatedAt: new Date(),
},
});
await tx.auditEntry.create({
data: {
organizationId: input.organization.id,
actorUserId: ownerUserId,
action: "alpha_silo.bootstrapped",
metadata: {
connectionId,
ownerIdentityId: identityId,
envelopeVersion: envelope.version,
keyId: envelope.keyId,
},
},
});
});
return { ownerUserId, feishuConnectionId: connectionId };
}
async function loadExistingSilo(
prisma: PrismaClient,
organizationId: string,
ownerOpenId: string,
): Promise<{
readonly ownerUserId: string;
readonly feishuConnectionId: string;
readonly appIdentityFingerprint: string;
} | null> {
const organizations = await prisma.organization.findMany({
take: 2,
include: {
memberships: {
where: { role: "OWNER", revokedAt: null },
take: 2,
include: { user: { include: { feishuIdentities: true } } },
},
feishuApplicationConnection: true,
},
});
if (organizations.length === 0) return null;
if (organizations.length !== 1 || organizations[0]!.id !== organizationId) {
throw new Error("bootstrap database is not the configured single-Organization Silo");
}
const organization = organizations[0]!;
if (organization.status !== "ACTIVE" || organization.memberships.length !== 1 ||
organization.feishuApplicationConnection?.status !== "ACTIVE") {
throw new Error("existing Silo is incomplete or inactive; manual repair is required");
}
const ownerIdentity = organization.memberships[0]!.user.feishuIdentities.find(
(identity) => identity.connectionId === organization.feishuApplicationConnection!.id,
);
if (ownerIdentity?.openId !== ownerOpenId) {
throw new Error("bootstrap owner identity does not match the existing Silo");
}
return {
ownerUserId: organization.memberships[0]!.userId,
feishuConnectionId: organization.feishuApplicationConnection.id,
appIdentityFingerprint: organization.feishuApplicationConnection.appIdentityFingerprint,
};
}
async function loadExistingProviderCredential(
prisma: PrismaClient,
secrets: LocalSecretEnvelope,
organizationId: string,
providerId: string,
): Promise<{ readonly baseUrl: string; readonly authToken: string; readonly anthropicApiKey: string }> {
const connection = await prisma.organizationProviderConnection.findUnique({
where: { organizationId_providerId: { organizationId, providerId } },
include: { activeSecretVersion: true },
});
const version = connection?.activeSecretVersion;
if (connection === null || connection === undefined || version === null || version === undefined ||
connection.status !== "ACTIVE" || version.retiredAt !== null) {
throw new Error(`active provider connection not found: ${providerId}`);
}
return decryptStoredProviderCredential(secrets, {
organizationId,
connectionId: connection.id,
providerId,
secretVersionId: version.id,
envelopeVersion: version.envelopeVersion,
keyId: version.keyId,
envelope: version.envelope,
});
}
function validateInput(input: AlphaSiloBootstrapInput): AlphaSiloBootstrapInput {
const required = (value: string, label: string): string => {
const normalized = value.trim();
if (normalized === "") throw new Error(`${label} is required`);
return normalized;
};
const slug = required(input.organization.slug, "Organization slug");
if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(slug)) {
throw new Error("Organization slug must be lowercase alphanumeric with optional hyphens");
}
if (input.provider.providerId.trim() !== "openrouter") {
throw new Error("Alpha Silo currently requires providerId=openrouter");
}
return {
organization: {
id: required(input.organization.id, "Organization id"),
slug,
name: required(input.organization.name, "Organization name"),
},
owner: {
openId: required(input.owner.openId, "Owner Feishu openId"),
displayName: required(input.owner.displayName, "Owner displayName"),
...(input.owner.unionId !== undefined ? { unionId: required(input.owner.unionId, "Owner unionId") } : {}),
},
feishu: input.feishu,
provider: input.provider,
...(input.teams !== undefined
? {
teams: input.teams.map((team) => {
const teamSlug = required(team.slug, "Team slug");
if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(teamSlug)) {
throw new Error(`invalid Team slug: ${teamSlug}`);
}
return {
slug: teamSlug,
name: required(team.name, `Team ${teamSlug} name`),
...(team.description !== undefined ? { description: team.description.trim() } : {}),
};
}),
}
: {}),
};
}