chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
+555
View File
@@ -0,0 +1,555 @@
import { db } from '@sim/db'
import { credential, credentialMember, permissions, workspace } from '@sim/db/schema'
import { generateId } from '@sim/utils/id'
import { and, eq, inArray, isNotNull, isNull, notInArray, or, sql } from 'drizzle-orm'
import { hasWorkspaceAdminAccess } from '@/lib/workspaces/permissions/utils'
export interface WorkspaceMembership {
ownerId: string | null
/** All workspace members: the owner plus everyone with a workspace permission. */
memberUserIds: string[]
}
/**
* Resolves a workspace's membership in one owner lookup + one permissions scan.
* Credential-admin status is derived from workspace role at access time, so
* members are seeded only for use access (the owner plus permission holders).
*/
export async function getWorkspaceMembership(workspaceId: string): Promise<WorkspaceMembership> {
const [workspaceRows, permissionRows] = await Promise.all([
db
.select({ ownerId: workspace.ownerId })
.from(workspace)
.where(eq(workspace.id, workspaceId))
.limit(1),
db
.select({ userId: permissions.userId })
.from(permissions)
.where(and(eq(permissions.entityType, 'workspace'), eq(permissions.entityId, workspaceId))),
])
const ownerId = workspaceRows[0]?.ownerId ?? null
const memberUserIds = new Set<string>(permissionRows.map((row) => row.userId))
if (ownerId) {
memberUserIds.add(ownerId)
}
return { ownerId, memberUserIds: Array.from(memberUserIds) }
}
export interface WorkspaceEnvKeyAdminAccess {
/** Keys for which the caller is an active credential admin. */
adminKeys: Set<string>
/** Keys that already have an `env_workspace` credential (regardless of role). */
knownKeys: Set<string>
}
/**
* For a set of workspace env keys, resolves which the caller may administer
* (active `credential_member` with role `admin`) and which already have an
* `env_workspace` credential at all. Keys absent from `knownKeys` have no ACL
* yet (new or legacy), letting routes fall back to a workspace-permission gate.
*/
export async function getWorkspaceEnvKeyAdminAccess(params: {
workspaceId: string
envKeys: string[]
userId: string
}): Promise<WorkspaceEnvKeyAdminAccess> {
const { workspaceId, envKeys, userId } = params
const keys = Array.from(new Set(envKeys.filter(Boolean)))
if (keys.length === 0) return { adminKeys: new Set(), knownKeys: new Set() }
const rows = await db
.select({
envKey: credential.envKey,
role: credentialMember.role,
status: credentialMember.status,
})
.from(credential)
.leftJoin(
credentialMember,
and(eq(credentialMember.credentialId, credential.id), eq(credentialMember.userId, userId))
)
.where(
and(
eq(credential.workspaceId, workspaceId),
eq(credential.type, 'env_workspace'),
inArray(credential.envKey, keys)
)
)
const knownKeys = new Set<string>()
const adminKeys = new Set<string>()
for (const row of rows) {
if (!row.envKey) continue
knownKeys.add(row.envKey)
if (row.role === 'admin' && row.status === 'active') adminKeys.add(row.envKey)
}
return { adminKeys, knownKeys }
}
interface AccessibleEnvCredential {
type: 'env_workspace' | 'env_personal'
envKey: string
envOwnerUserId: string | null
updatedAt: Date
}
export async function getUserWorkspaceIds(userId: string): Promise<string[]> {
const [permissionRows, ownedWorkspaceRows] = await Promise.all([
db
.select({ workspaceId: workspace.id })
.from(permissions)
.innerJoin(
workspace,
and(eq(permissions.entityType, 'workspace'), eq(permissions.entityId, workspace.id))
)
.where(and(eq(permissions.userId, userId), isNull(workspace.archivedAt))),
db
.select({ workspaceId: workspace.id })
.from(workspace)
.where(and(eq(workspace.ownerId, userId), isNull(workspace.archivedAt))),
])
const workspaceIds = new Set<string>(permissionRows.map((row) => row.workspaceId))
for (const row of ownedWorkspaceRows) {
workspaceIds.add(row.workspaceId)
}
return Array.from(workspaceIds)
}
async function ensureWorkspaceCredentialMemberships(
credentialId: string,
memberUserIds: string[],
invitedBy: string
) {
if (!memberUserIds.length) return
const existingMemberships = await db
.select({
userId: credentialMember.userId,
status: credentialMember.status,
})
.from(credentialMember)
.where(
and(
eq(credentialMember.credentialId, credentialId),
inArray(credentialMember.userId, memberUserIds)
)
)
// Revoked memberships are filtered out so ON CONFLICT cannot resurrect them.
const revokedUserIds = new Set<string>(
existingMemberships.filter((row) => row.status === 'revoked').map((row) => row.userId)
)
const targetUserIds = memberUserIds.filter((id) => !revokedUserIds.has(id))
if (targetUserIds.length === 0) return
const now = new Date()
const values = targetUserIds.map((memberUserId) => ({
id: generateId(),
credentialId,
userId: memberUserId,
role: 'member' as const,
status: 'active' as const,
joinedAt: now,
invitedBy,
createdAt: now,
updatedAt: now,
}))
// Existing roles (including manual per-secret overrides) are preserved on
// conflict; only membership activeness and a missing joinedAt are reconciled.
await db
.insert(credentialMember)
.values(values)
.onConflictDoUpdate({
target: [credentialMember.credentialId, credentialMember.userId],
set: {
status: 'active',
joinedAt: sql`COALESCE(${credentialMember.joinedAt}, excluded.joined_at)`,
updatedAt: now,
},
})
}
export async function syncWorkspaceEnvCredentials(params: {
workspaceId: string
envKeys: string[]
actingUserId: string
}) {
const { workspaceId, envKeys, actingUserId } = params
const { ownerId, memberUserIds } = await getWorkspaceMembership(workspaceId)
if (!ownerId) return
const normalizedKeys = Array.from(new Set(envKeys.filter(Boolean)))
const existingCredentials = await db
.select({
id: credential.id,
envKey: credential.envKey,
})
.from(credential)
.where(and(eq(credential.workspaceId, workspaceId), eq(credential.type, 'env_workspace')))
const existingByKey = new Map(
existingCredentials
.filter((row): row is { id: string; envKey: string } => Boolean(row.envKey))
.map((row) => [row.envKey, row.id])
)
const credentialIdsToEnsureMembership = new Set<string>()
const now = new Date()
for (const envKey of normalizedKeys) {
const existingId = existingByKey.get(envKey)
if (existingId) credentialIdsToEnsureMembership.add(existingId)
}
const keysToCreate = normalizedKeys.filter((key) => !existingByKey.has(key))
if (keysToCreate.length > 0) {
const inserted = await db
.insert(credential)
.values(
keysToCreate.map((envKey) => ({
id: generateId(),
workspaceId,
type: 'env_workspace' as const,
displayName: envKey,
envKey,
createdBy: actingUserId,
createdAt: now,
updatedAt: now,
}))
)
.onConflictDoNothing()
.returning({ id: credential.id })
for (const row of inserted) {
credentialIdsToEnsureMembership.add(row.id)
}
}
for (const credentialId of credentialIdsToEnsureMembership) {
await ensureWorkspaceCredentialMemberships(credentialId, memberUserIds, ownerId)
}
if (normalizedKeys.length > 0) {
await db
.delete(credential)
.where(
and(
eq(credential.workspaceId, workspaceId),
eq(credential.type, 'env_workspace'),
notInArray(credential.envKey, normalizedKeys)
)
)
return
}
await db
.delete(credential)
.where(and(eq(credential.workspaceId, workspaceId), eq(credential.type, 'env_workspace')))
}
/**
* Creates credential records and bulk-inserts memberships for newly added workspace env keys.
* Use this instead of `syncWorkspaceEnvCredentials` when the caller knows exactly which keys are new.
*/
export async function createWorkspaceEnvCredentials(params: {
workspaceId: string
newKeys: string[]
actingUserId: string
}): Promise<void> {
const { workspaceId, newKeys, actingUserId } = params
const keys = Array.from(new Set(newKeys.filter(Boolean)))
if (keys.length === 0) return
const { ownerId, memberUserIds } = await getWorkspaceMembership(workspaceId)
if (!ownerId) return
const now = new Date()
const inserted = await db
.insert(credential)
.values(
keys.map((envKey) => ({
id: generateId(),
workspaceId,
type: 'env_workspace' as const,
displayName: envKey,
envKey,
createdBy: actingUserId,
createdAt: now,
updatedAt: now,
}))
)
.onConflictDoNothing()
.returning({ id: credential.id })
const createdIds = inserted.map((row) => row.id)
if (createdIds.length === 0 || memberUserIds.length === 0) return
// Bulk-insert memberships for all new credentials × all workspace members in one query
const membershipValues = createdIds.flatMap((credentialId) =>
memberUserIds.map((memberUserId) => ({
id: generateId(),
credentialId,
userId: memberUserId,
role: (memberUserId === actingUserId ? 'admin' : 'member') as 'admin' | 'member',
status: 'active' as const,
joinedAt: now,
invitedBy: actingUserId,
createdAt: now,
updatedAt: now,
}))
)
await db.insert(credentialMember).values(membershipValues).onConflictDoNothing()
}
/**
* Deletes credential records (and their memberships via cascade) for removed workspace env keys.
* Use this instead of `syncWorkspaceEnvCredentials` when the caller knows exactly which keys were deleted.
*/
export async function deleteWorkspaceEnvCredentials(params: {
workspaceId: string
removedKeys: string[]
}): Promise<void> {
const { workspaceId, removedKeys } = params
const keys = removedKeys.filter(Boolean)
if (keys.length === 0) return
await db
.delete(credential)
.where(
and(
eq(credential.workspaceId, workspaceId),
eq(credential.type, 'env_workspace'),
inArray(credential.envKey, keys)
)
)
}
export async function syncPersonalEnvCredentialsForUser(params: {
userId: string
envKeys: string[]
}): Promise<void> {
const { userId, envKeys } = params
const workspaceIds = await getUserWorkspaceIds(userId)
if (!workspaceIds.length) return
const normalizedKeys = Array.from(new Set(envKeys.filter(Boolean)))
const now = new Date()
await Promise.all(
workspaceIds.map(async (workspaceId) => {
if (normalizedKeys.length > 0) {
await db
.insert(credential)
.values(
normalizedKeys.map((envKey) => ({
id: generateId(),
workspaceId,
type: 'env_personal' as const,
displayName: envKey,
envKey,
envOwnerUserId: userId,
createdBy: userId,
createdAt: now,
updatedAt: now,
}))
)
.onConflictDoNothing()
}
const currentCredentials =
normalizedKeys.length > 0
? await db
.select({ id: credential.id })
.from(credential)
.where(
and(
eq(credential.workspaceId, workspaceId),
eq(credential.type, 'env_personal'),
eq(credential.envOwnerUserId, userId),
inArray(credential.envKey, normalizedKeys)
)
)
: []
if (currentCredentials.length > 0) {
await db
.insert(credentialMember)
.values(
currentCredentials.map(({ id: credentialId }) => ({
id: generateId(),
credentialId,
userId,
role: 'admin' as const,
status: 'active' as const,
joinedAt: now,
invitedBy: userId,
createdAt: now,
updatedAt: now,
}))
)
.onConflictDoUpdate({
target: [credentialMember.credentialId, credentialMember.userId],
set: { role: 'admin', status: 'active', updatedAt: now },
})
}
if (normalizedKeys.length > 0) {
await db
.delete(credential)
.where(
and(
eq(credential.workspaceId, workspaceId),
eq(credential.type, 'env_personal'),
eq(credential.envOwnerUserId, userId),
notInArray(credential.envKey, normalizedKeys)
)
)
} else {
await db
.delete(credential)
.where(
and(
eq(credential.workspaceId, workspaceId),
eq(credential.type, 'env_personal'),
eq(credential.envOwnerUserId, userId)
)
)
}
})
)
}
export async function getAccessibleEnvCredentials(
workspaceId: string,
userId: string,
options?: { isWorkspaceAdmin?: boolean }
): Promise<AccessibleEnvCredential[]> {
const isWorkspaceAdmin =
options?.isWorkspaceAdmin ?? (await hasWorkspaceAdminAccess(userId, workspaceId))
const rows = await db
.select({
type: credential.type,
envKey: credential.envKey,
envOwnerUserId: credential.envOwnerUserId,
updatedAt: credential.updatedAt,
})
.from(credential)
.leftJoin(
credentialMember,
and(
eq(credentialMember.credentialId, credential.id),
eq(credentialMember.userId, userId),
eq(credentialMember.status, 'active')
)
)
.where(
and(
eq(credential.workspaceId, workspaceId),
inArray(credential.type, ['env_workspace', 'env_personal']),
or(
isNotNull(credentialMember.id),
eq(credential.envOwnerUserId, userId),
isWorkspaceAdmin ? eq(credential.type, 'env_workspace') : undefined
)
)
)
return rows
.filter(
(row): row is typeof row & { type: 'env_workspace' | 'env_personal'; envKey: string } =>
row.envKey !== null && (row.type === 'env_workspace' || row.type === 'env_personal')
)
.map((row) => ({
type: row.type,
envKey: row.envKey,
envOwnerUserId: row.envOwnerUserId,
updatedAt: row.updatedAt,
}))
}
export interface AccessibleOAuthCredential {
id: string
providerId: string
displayName: string
role: 'admin' | 'member'
updatedAt: Date
}
export async function getAccessibleOAuthCredentials(
workspaceId: string,
userId: string,
options?: { isWorkspaceAdmin?: boolean }
): Promise<AccessibleOAuthCredential[]> {
const isWorkspaceAdmin =
options?.isWorkspaceAdmin ?? (await hasWorkspaceAdminAccess(userId, workspaceId))
if (isWorkspaceAdmin) {
const rows = await db
.select({
id: credential.id,
providerId: credential.providerId,
displayName: credential.displayName,
updatedAt: credential.updatedAt,
})
.from(credential)
.where(
and(
eq(credential.workspaceId, workspaceId),
inArray(credential.type, ['oauth', 'service_account'])
)
)
return rows
.filter((row): row is typeof row & { providerId: string } => Boolean(row.providerId))
.map((row) => ({
id: row.id,
providerId: row.providerId,
displayName: row.displayName,
role: 'admin' as const,
updatedAt: row.updatedAt,
}))
}
const rows = await db
.select({
id: credential.id,
providerId: credential.providerId,
displayName: credential.displayName,
role: credentialMember.role,
updatedAt: credential.updatedAt,
})
.from(credential)
.innerJoin(
credentialMember,
and(
eq(credentialMember.credentialId, credential.id),
eq(credentialMember.userId, userId),
eq(credentialMember.status, 'active')
)
)
.where(
and(
eq(credential.workspaceId, workspaceId),
inArray(credential.type, ['oauth', 'service_account'])
)
)
return rows
.filter((row): row is AccessibleOAuthCredential => Boolean(row.providerId))
.map((row) => ({
id: row.id,
providerId: row.providerId!,
displayName: row.displayName,
role: row.role,
updatedAt: row.updatedAt,
}))
}