forked from EduCraft/curriculum-project-hub
feat: add deployable alpha silo
This commit is contained in:
@@ -0,0 +1,365 @@
|
||||
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);
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
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.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() } : {}),
|
||||
};
|
||||
}),
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user