forked from bai/curriculum-project-hub
feat: add deployable alpha silo
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import type { Prisma, PrismaClient } from "@prisma/client";
|
||||
import { lockActiveOrganization } from "../org/status.js";
|
||||
|
||||
export type FeishuPrincipalKind = "USER" | "CHAT" | "DEPARTMENT" | "USER_GROUP" | "EVENT" | "CALLBACK";
|
||||
|
||||
export interface ScopedFeishuIdentity {
|
||||
readonly identityId: string;
|
||||
readonly connectionId: string;
|
||||
readonly organizationId: string;
|
||||
readonly userId: string;
|
||||
readonly openId: string;
|
||||
readonly unionId: string | null;
|
||||
readonly principalId: string;
|
||||
readonly displayName: string;
|
||||
readonly avatarUrl: string | null;
|
||||
}
|
||||
|
||||
export function scopedFeishuPrincipalId(
|
||||
kind: FeishuPrincipalKind,
|
||||
connectionId: string,
|
||||
providerLocalId: string,
|
||||
): string {
|
||||
const normalizedConnectionId = nonEmpty(connectionId, "Feishu connectionId");
|
||||
const normalizedProviderLocalId = nonEmpty(providerLocalId, `Feishu ${kind} identifier`);
|
||||
const digest = createHash("sha256")
|
||||
.update("cph-feishu-principal\0")
|
||||
.update(kind, "utf8")
|
||||
.update("\0")
|
||||
.update(normalizedConnectionId, "utf8")
|
||||
.update("\0")
|
||||
.update(normalizedProviderLocalId, "utf8")
|
||||
.digest("base64url");
|
||||
return `feishu:${kind.toLowerCase()}:${normalizedConnectionId}:${digest}`;
|
||||
}
|
||||
|
||||
export async function upsertScopedFeishuIdentity(
|
||||
prisma: PrismaClient,
|
||||
input: {
|
||||
readonly connectionId: string;
|
||||
readonly openId: string;
|
||||
readonly unionId?: string | undefined;
|
||||
readonly displayName: string;
|
||||
readonly avatarUrl?: string | undefined;
|
||||
},
|
||||
): Promise<ScopedFeishuIdentity> {
|
||||
return prisma.$transaction((tx) => upsertScopedFeishuIdentityInTransaction(tx, input));
|
||||
}
|
||||
|
||||
export async function upsertScopedFeishuIdentityInTransaction(
|
||||
prisma: Prisma.TransactionClient,
|
||||
input: {
|
||||
readonly connectionId: string;
|
||||
readonly expectedOrganizationId?: string | undefined;
|
||||
readonly openId: string;
|
||||
readonly unionId?: string | undefined;
|
||||
readonly displayName: string;
|
||||
readonly avatarUrl?: string | undefined;
|
||||
},
|
||||
): Promise<ScopedFeishuIdentity> {
|
||||
const connectionId = nonEmpty(input.connectionId, "Feishu connectionId");
|
||||
const openId = nonEmpty(input.openId, "Feishu openId");
|
||||
const displayName = nonEmpty(input.displayName, "Feishu displayName");
|
||||
const unionId = optionalNonEmpty(input.unionId, "Feishu unionId");
|
||||
const avatarUrl = optionalNonEmpty(input.avatarUrl, "Feishu avatarUrl");
|
||||
const principalId = scopedFeishuPrincipalId("USER", connectionId, openId);
|
||||
const userId = deterministicFeishuUserId(connectionId, openId);
|
||||
|
||||
const connection = await lockActiveConnection(prisma, connectionId);
|
||||
if (input.expectedOrganizationId !== undefined &&
|
||||
input.expectedOrganizationId !== connection.organizationId) {
|
||||
throw new Error("Feishu identity Organization scope mismatch");
|
||||
}
|
||||
const identity = await prisma.feishuUserIdentity.upsert({
|
||||
where: { connectionId_openId: { connectionId, openId } },
|
||||
update: {
|
||||
...(unionId !== undefined ? { unionId } : {}),
|
||||
user: {
|
||||
update: {
|
||||
displayName,
|
||||
...(avatarUrl !== undefined ? { avatarUrl } : {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
create: {
|
||||
openId,
|
||||
...(unionId !== undefined ? { unionId } : {}),
|
||||
connection: { connect: { id: connectionId } },
|
||||
user: {
|
||||
connectOrCreate: {
|
||||
where: { id: userId },
|
||||
create: {
|
||||
id: userId,
|
||||
feishuOpenId: principalId,
|
||||
displayName,
|
||||
...(avatarUrl !== undefined ? { avatarUrl } : {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
include: { user: true },
|
||||
});
|
||||
return scopedIdentity(connection.organizationId, identity);
|
||||
}
|
||||
|
||||
export async function resolveScopedFeishuIdentity(
|
||||
prisma: PrismaClient,
|
||||
input: {
|
||||
readonly connectionId: string;
|
||||
readonly openId: string;
|
||||
readonly expectedOrganizationId?: string | undefined;
|
||||
},
|
||||
): Promise<ScopedFeishuIdentity | null> {
|
||||
const connectionId = nonEmpty(input.connectionId, "Feishu connectionId");
|
||||
const openId = nonEmpty(input.openId, "Feishu openId");
|
||||
return prisma.$transaction(async (tx) => {
|
||||
const connection = await lockActiveConnection(tx, connectionId);
|
||||
if (input.expectedOrganizationId !== undefined &&
|
||||
input.expectedOrganizationId !== connection.organizationId) {
|
||||
throw new Error("Feishu identity Organization scope mismatch");
|
||||
}
|
||||
const identity = await tx.feishuUserIdentity.findUnique({
|
||||
where: { connectionId_openId: { connectionId, openId } },
|
||||
include: { user: true },
|
||||
});
|
||||
return identity === null ? null : scopedIdentity(connection.organizationId, identity);
|
||||
});
|
||||
}
|
||||
|
||||
async function lockActiveConnection(
|
||||
prisma: Prisma.TransactionClient,
|
||||
connectionId: string,
|
||||
): Promise<{ readonly organizationId: string }> {
|
||||
const scope = await prisma.organizationFeishuApplicationConnection.findUnique({
|
||||
where: { id: connectionId },
|
||||
select: { organizationId: true },
|
||||
});
|
||||
if (scope === null) throw new Error("active Feishu Application Connection not found");
|
||||
await lockActiveOrganization(prisma, scope.organizationId);
|
||||
await prisma.$queryRaw`
|
||||
SELECT "id" FROM "OrganizationFeishuApplicationConnection"
|
||||
WHERE "id" = ${connectionId} FOR SHARE
|
||||
`;
|
||||
const connection = await prisma.organizationFeishuApplicationConnection.findUnique({
|
||||
where: { id: connectionId },
|
||||
select: {
|
||||
organizationId: true,
|
||||
status: true,
|
||||
activeSecretVersion: { select: { connectionId: true, retiredAt: true } },
|
||||
},
|
||||
});
|
||||
if (connection === null || connection.status !== "ACTIVE" || connection.activeSecretVersion === null ||
|
||||
connection.organizationId !== scope.organizationId ||
|
||||
connection.activeSecretVersion.connectionId !== connectionId ||
|
||||
connection.activeSecretVersion.retiredAt !== null) {
|
||||
throw new Error("active Feishu Application Connection not found");
|
||||
}
|
||||
return { organizationId: connection.organizationId };
|
||||
}
|
||||
|
||||
function scopedIdentity(
|
||||
organizationId: string,
|
||||
identity: {
|
||||
readonly id: string;
|
||||
readonly connectionId: string;
|
||||
readonly userId: string;
|
||||
readonly openId: string;
|
||||
readonly unionId: string | null;
|
||||
readonly user: {
|
||||
readonly displayName: string;
|
||||
readonly avatarUrl: string | null;
|
||||
};
|
||||
},
|
||||
): ScopedFeishuIdentity {
|
||||
return {
|
||||
identityId: identity.id,
|
||||
connectionId: identity.connectionId,
|
||||
organizationId,
|
||||
userId: identity.userId,
|
||||
openId: identity.openId,
|
||||
unionId: identity.unionId,
|
||||
principalId: scopedFeishuPrincipalId("USER", identity.connectionId, identity.openId),
|
||||
displayName: identity.user.displayName,
|
||||
avatarUrl: identity.user.avatarUrl,
|
||||
};
|
||||
}
|
||||
|
||||
export function deterministicFeishuUserId(connectionId: string, openId: string): string {
|
||||
const digest = createHash("sha256")
|
||||
.update("cph-feishu-user\0")
|
||||
.update(connectionId, "utf8")
|
||||
.update("\0")
|
||||
.update(openId, "utf8")
|
||||
.digest("base64url");
|
||||
return `feishu_${digest}`;
|
||||
}
|
||||
|
||||
function nonEmpty(value: string, label: string): string {
|
||||
const normalized = value.trim();
|
||||
if (normalized === "") throw new Error(`${label} is required`);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function optionalNonEmpty(value: string | undefined, label: string): string | undefined {
|
||||
if (value === undefined) return undefined;
|
||||
return nonEmpty(value, label);
|
||||
}
|
||||
Reference in New Issue
Block a user