import { db } from '@sim/db' import { credential, credentialMember, credentialTypeEnum } from '@sim/db/schema' import { and, eq, inArray } from 'drizzle-orm' import type { DbOrTx } from '@/lib/db/types' import { checkWorkspaceAccess, type WorkspaceAccess } from '@/lib/workspaces/permissions/utils' type ActiveCredentialMember = typeof credentialMember.$inferSelect type CredentialRecord = typeof credential.$inferSelect export type CredentialType = (typeof credentialTypeEnum.enumValues)[number] /** * Credential types shared at the workspace level — every type except a user's * personal env vars. Derived from the enum so a newly added credential type is * treated as shared by default, keeping visibility, role, and admin derivation * consistent instead of drifting against a hand-maintained inclusion list. */ export const SHARED_CREDENTIAL_TYPES = credentialTypeEnum.enumValues.filter( (type) => type !== 'env_personal' ) /** Whether a credential is shared at the workspace level (i.e. not a personal env var). */ export function isSharedCredentialType(type: CredentialType): boolean { return type !== 'env_personal' } /** * Whether a user is an admin of a credential: an explicit credential-member admin, * or — for shared credentials only — a workspace admin (workspace admins are * derived credential admins, but never for personal env vars). */ export function deriveCredentialAdmin(params: { credentialType: CredentialType memberRole: ActiveCredentialMember['role'] | null | undefined workspaceCanAdmin: boolean }): boolean { return ( params.memberRole === 'admin' || (isSharedCredentialType(params.credentialType) && params.workspaceCanAdmin) ) } export interface CredentialActorContext { credential: CredentialRecord | null member: ActiveCredentialMember | null hasWorkspaceAccess: boolean canWriteWorkspace: boolean isAdmin: boolean } /** * Resolves user access context for a credential. Pass `workspaceAccess` when the * caller has already resolved access for the credential's workspace to skip a * redundant lookup; it is reused only when it matches the credential's workspace. */ export async function getCredentialActorContext( credentialId: string, userId: string, options?: { workspaceAccess?: WorkspaceAccess } ): Promise { const [credentialRow] = await db .select() .from(credential) .where(eq(credential.id, credentialId)) .limit(1) if (!credentialRow) { return { credential: null, member: null, hasWorkspaceAccess: false, canWriteWorkspace: false, isAdmin: false, } } const providedAccess = options?.workspaceAccess const workspaceAccess = providedAccess && providedAccess.workspace?.id === credentialRow.workspaceId ? providedAccess : await checkWorkspaceAccess(credentialRow.workspaceId, userId) const [memberRow] = await db .select() .from(credentialMember) .where( and( eq(credentialMember.credentialId, credentialId), eq(credentialMember.userId, userId), eq(credentialMember.status, 'active') ) ) .limit(1) const isAdmin = deriveCredentialAdmin({ credentialType: credentialRow.type, memberRole: memberRow?.role, workspaceCanAdmin: workspaceAccess.canAdmin, }) return { credential: credentialRow, member: memberRow ?? null, hasWorkspaceAccess: workspaceAccess.hasAccess, canWriteWorkspace: workspaceAccess.canWrite, isAdmin, } } /** * Revokes all credential memberships for a user across one or more workspaces. * Workspace owners and admins are derived credential admins, so no per-credential * owner promotion is needed to avoid orphaning a credential. Returns the number * of memberships revoked. */ export async function revokeWorkspaceCredentialMembershipsTx( tx: DbOrTx, workspaceId: string | string[], userId: string ): Promise { const workspaceIds = Array.isArray(workspaceId) ? workspaceId : [workspaceId] if (workspaceIds.length === 0) return 0 const workspaceCredentialIds = await tx .select({ id: credential.id }) .from(credential) .where(inArray(credential.workspaceId, workspaceIds)) if (workspaceCredentialIds.length === 0) return 0 const credIds = workspaceCredentialIds.map((c) => c.id) const revoked = await tx .update(credentialMember) .set({ status: 'revoked', updatedAt: new Date() }) .where( and( eq(credentialMember.userId, userId), eq(credentialMember.status, 'active'), inArray(credentialMember.credentialId, credIds) ) ) .returning({ id: credentialMember.id }) return revoked.length }