feat: add deployable alpha silo

This commit is contained in:
2026-07-11 00:25:45 +08:00
parent 44557da499
commit 9e954790dc
57 changed files with 2792 additions and 400 deletions
+58 -11
View File
@@ -5,6 +5,7 @@ import type { Folder, OrganizationMemberRole, PermissionRole, Prisma, PrismaClie
import { createPermissionAuthorizer } from "./permission.js";
import { writeAudit } from "./audit.js";
import { lockActiveOrganization, requireActiveOrganizationStatus } from "./org/status.js";
import { scopedFeishuPrincipalId } from "./feishu/identityNamespace.js";
export interface OrganizationProjectPolicy {
readonly organizationId: string;
@@ -140,7 +141,8 @@ export async function createProjectFromOrgAdmin(
return createManagedProject(prisma, {
organizationId: input.organizationId,
actorUserId: actor.userId,
actorFeishuOpenId: input.actorFeishuOpenId,
actorPrincipalId: actor.principalId,
feishuConnectionId: actor.connectionId,
name,
workspaceRoot: input.workspaceRoot,
folderId: input.folderId,
@@ -170,7 +172,8 @@ export async function createProjectFromFeishuChat(
return createManagedProject(prisma, {
organizationId: input.organizationId,
actorUserId: actor.userId,
actorFeishuOpenId: input.actorFeishuOpenId,
actorPrincipalId: actor.principalId,
feishuConnectionId: actor.connectionId,
name,
workspaceRoot: input.workspaceRoot,
folderId: input.folderId,
@@ -212,7 +215,7 @@ export async function bindFeishuChatToProject(
await replaceProjectGrant(tx, {
projectId: project.id,
principalType: "FEISHU_CHAT",
principalId: chatId,
principalId: scopedChatPrincipalId(actor.connectionId, chatId),
role: "EDIT",
createdByUserId: actor.userId,
});
@@ -262,7 +265,7 @@ export async function archiveFeishuChatBinding(
resourceType: "PROJECT",
resourceId: binding.projectId,
principalType: "FEISHU_CHAT",
principalId: binding.chatId,
principalId: scopedChatPrincipalId(actor.connectionId, binding.chatId),
revokedAt: null,
},
data: { revokedAt: new Date() },
@@ -282,7 +285,8 @@ async function createManagedProject(
input: {
readonly organizationId: string;
readonly actorUserId: string;
readonly actorFeishuOpenId: string;
readonly actorPrincipalId: string;
readonly feishuConnectionId: string | undefined;
readonly name: string;
readonly workspaceRoot: string;
readonly folderId: string | undefined;
@@ -332,7 +336,7 @@ async function createManagedProject(
await replaceProjectGrant(tx, {
projectId: created.id,
principalType: "USER",
principalId: input.actorFeishuOpenId,
principalId: input.actorPrincipalId,
role: "MANAGE",
createdByUserId: input.actorUserId,
});
@@ -343,7 +347,7 @@ async function createManagedProject(
await replaceProjectGrant(tx, {
projectId: created.id,
principalType: "FEISHU_CHAT",
principalId: input.chatId,
principalId: scopedChatPrincipalId(input.feishuConnectionId, input.chatId),
role: "EDIT",
createdByUserId: input.actorUserId,
});
@@ -385,7 +389,13 @@ async function requireProjectManager(
projectId: string,
organizationId: string,
actorFeishuOpenId: string,
): Promise<{ readonly userId: string; readonly role: OrganizationMemberRole; readonly via: "org-admin" | "project-manage" }> {
): Promise<{
readonly userId: string;
readonly role: OrganizationMemberRole;
readonly principalId: string;
readonly connectionId: string | undefined;
readonly via: "org-admin" | "project-manage";
}> {
const actor = await requireActiveOrgMember(prisma, organizationId, actorFeishuOpenId);
if (isOrgAdminRole(actor.role)) {
return { ...actor, via: "org-admin" };
@@ -405,9 +415,42 @@ async function requireActiveOrgMember(
prisma: PrismaClient,
organizationId: string,
feishuOpenId: string,
): Promise<{ readonly userId: string; readonly role: OrganizationMemberRole }> {
): Promise<{
readonly userId: string;
readonly role: OrganizationMemberRole;
readonly principalId: string;
readonly connectionId: string | undefined;
}> {
await requireActiveOrganization(prisma, organizationId);
const user = await prisma.user.findUnique({
const identity = await prisma.feishuUserIdentity.findFirst({
where: { openId: feishuOpenId, connection: { organizationId, status: "ACTIVE" } },
select: {
connectionId: true,
user: { select: {
id: true,
organizationMemberships: {
where: { organizationId, revokedAt: null },
select: { role: true },
take: 1,
},
} },
},
});
if (identity !== null) {
const membership = identity.user.organizationMemberships[0];
if (membership === undefined) {
throw new Error(`Feishu user ${feishuOpenId} is not an active member of organization ${organizationId}`);
}
return {
userId: identity.user.id,
role: membership.role,
principalId: scopedFeishuPrincipalId("USER", identity.connectionId, feishuOpenId),
connectionId: identity.connectionId,
};
}
// Explicit compatibility path for pre-ADR-0024 rows while the Silo branch
// completes migration. New bootstrap/member writes always create identities.
const user = process.env["NODE_ENV"] === "production" ? null : await prisma.user.findUnique({
where: { feishuOpenId },
select: {
id: true,
@@ -425,7 +468,11 @@ async function requireActiveOrgMember(
if (membership === undefined) {
throw new Error(`Feishu user ${feishuOpenId} is not an active member of organization ${organizationId}`);
}
return { userId: user.id, role: membership.role };
return { userId: user.id, role: membership.role, principalId: feishuOpenId, connectionId: undefined };
}
function scopedChatPrincipalId(connectionId: string | undefined, chatId: string): string {
return connectionId === undefined ? chatId : scopedFeishuPrincipalId("CHAT", connectionId, chatId);
}
async function ensureOrganizationProjectSettingsTx(