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
@@ -0,0 +1,345 @@
import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
import { db } from '@sim/db'
import { type InvitationMembershipIntent, permissions, user } from '@sim/db/schema'
import { normalizeEmail } from '@sim/utils/string'
import { and, eq, sql } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { getUserOrganization } from '@/lib/billing/organizations/membership'
import { validateSeatAvailability } from '@/lib/billing/validation/seat-management'
import { PlatformEvents } from '@/lib/core/telemetry'
import {
type DirectGrantOutcome,
grantWorkspaceAccessDirectly,
} from '@/lib/invitations/direct-grant'
import {
cancelPendingInvitation,
createPendingInvitation,
findPendingGrantForWorkspaceEmail,
sendInvitationEmail,
} from '@/lib/invitations/send'
import { captureServerEvent } from '@/lib/posthog/server'
import {
getWorkspaceWithOwner,
hasWorkspaceAdminAccess,
type PermissionType,
type WorkspaceWithOwner,
} from '@/lib/workspaces/permissions/utils'
import { getWorkspaceInvitePolicy, type WorkspaceInvitePolicy } from '@/lib/workspaces/policy'
import { validateInvitationsAllowed } from '@/ee/access-control/utils/permission-check'
export interface WorkspaceInvitationContext {
workspaceId: string
inviterId: string
inviterName: string
inviterEmail?: string | null
workspaceDetails: WorkspaceWithOwner
invitePolicy: WorkspaceInvitePolicy
}
export interface WorkspaceInvitationResult {
id: string
workspaceId: string
email: string
permission: PermissionType
membershipIntent: InvitationMembershipIntent
expiresAt: Date | undefined
/** True when the user was granted access directly (no pending invitation). */
instantAdd?: boolean
/** Direct-grant outcome when `instantAdd` is true. */
outcome?: DirectGrantOutcome['outcome']
}
export class WorkspaceInvitationError extends Error {
status: number
email?: string
upgradeRequired?: boolean
constructor({
message,
status,
email,
upgradeRequired,
}: {
message: string
status: number
email?: string
upgradeRequired?: boolean
}) {
super(message)
this.name = 'WorkspaceInvitationError'
this.status = status
this.email = email
this.upgradeRequired = upgradeRequired
}
}
export async function prepareWorkspaceInvitationContext({
workspaceId,
inviterId,
inviterName,
inviterEmail,
}: {
workspaceId: string
inviterId: string
inviterName: string
inviterEmail?: string | null
}): Promise<WorkspaceInvitationContext> {
await validateInvitationsAllowed(inviterId, workspaceId)
const isAdmin = await hasWorkspaceAdminAccess(inviterId, workspaceId)
if (!isAdmin) {
throw new WorkspaceInvitationError({
message: 'You need admin permissions to invite users',
status: 403,
})
}
const workspaceDetails = await getWorkspaceWithOwner(workspaceId)
if (!workspaceDetails) {
throw new WorkspaceInvitationError({ message: 'Workspace not found', status: 404 })
}
const invitePolicy = await getWorkspaceInvitePolicy(workspaceDetails)
if (!invitePolicy.allowed) {
throw new WorkspaceInvitationError({
message: invitePolicy.reason ?? 'Invites are disabled for this workspace.',
status: 403,
upgradeRequired: invitePolicy.upgradeRequired,
})
}
return {
workspaceId,
inviterId,
inviterName,
inviterEmail,
workspaceDetails,
invitePolicy,
}
}
export async function createWorkspaceInvitation({
context,
email,
permission = 'read',
request,
}: {
context: WorkspaceInvitationContext
email: string
permission?: string
request: NextRequest
}): Promise<WorkspaceInvitationResult> {
const validPermissions: PermissionType[] = ['admin', 'write', 'read']
if (!validPermissions.includes(permission as PermissionType)) {
throw new WorkspaceInvitationError({
message: `Invalid permission: must be one of ${validPermissions.join(', ')}`,
status: 400,
email,
})
}
const invitationPermission = permission as PermissionType
const normalizedEmail = normalizeEmail(email)
let membershipIntent: InvitationMembershipIntent = 'internal'
const existingUser = await db
.select()
.from(user)
.where(sql`lower(${user.email}) = ${normalizedEmail}`)
.then((rows) => rows[0])
if (existingUser) {
const workspaceOrganizationId = context.workspaceDetails.organizationId
const existingMembership = workspaceOrganizationId
? await getUserOrganization(existingUser.id)
: null
const existingPermission = await db
.select()
.from(permissions)
.where(
and(
eq(permissions.entityId, context.workspaceId),
eq(permissions.entityType, 'workspace'),
eq(permissions.userId, existingUser.id)
)
)
.then((rows) => rows[0])
/**
* Already a workspace member: reject. Invites never change an existing
* member's permission — role changes go through the members list, not the
* invite flow. (The client also blocks re-inviting current teammates.)
*/
if (existingPermission) {
throw new WorkspaceInvitationError({
message: `${normalizedEmail} already has access to this workspace`,
status: 400,
email: normalizedEmail,
})
}
/**
* Invitee already belongs to the workspace's organization (and is not yet a
* member of this workspace): grant access directly, with no invitation or
* acceptance step.
*/
if (
workspaceOrganizationId &&
existingMembership &&
existingMembership.organizationId === workspaceOrganizationId
) {
const directGrant = await grantWorkspaceAccessDirectly({
userId: existingUser.id,
email: normalizedEmail,
workspaceId: context.workspaceId,
workspaceName: context.workspaceDetails.name,
permission: invitationPermission,
organizationId: workspaceOrganizationId,
actorId: context.inviterId,
actorName: context.inviterName,
actorEmail: context.inviterEmail,
request,
})
return {
id: existingUser.id,
workspaceId: context.workspaceId,
email: normalizedEmail,
permission: invitationPermission,
membershipIntent: 'internal',
expiresAt: undefined,
instantAdd: true,
outcome: directGrant.outcome,
}
}
if (workspaceOrganizationId) {
if (existingMembership && existingMembership.organizationId !== workspaceOrganizationId) {
membershipIntent = 'external'
} else if (context.invitePolicy.requiresSeat && !existingMembership) {
const seatValidation = await validateSeatAvailability(workspaceOrganizationId, 1)
if (!seatValidation.canInvite) {
throw new WorkspaceInvitationError({
message: seatValidation.reason || 'No available seats for this organization.',
status: 400,
email: normalizedEmail,
})
}
}
}
} else if (context.invitePolicy.requiresSeat && context.invitePolicy.organizationId) {
const seatValidation = await validateSeatAvailability(context.invitePolicy.organizationId, 1)
if (!seatValidation.canInvite) {
throw new WorkspaceInvitationError({
message: seatValidation.reason || 'No available seats for this organization.',
status: 400,
email: normalizedEmail,
})
}
}
const existingInvitation = await findPendingGrantForWorkspaceEmail({
workspaceId: context.workspaceId,
email: normalizedEmail,
})
if (existingInvitation) {
throw new WorkspaceInvitationError({
message: `${normalizedEmail} has already been invited to this workspace`,
status: 400,
email: normalizedEmail,
})
}
const { invitationId, token } = await createPendingInvitation({
kind: 'workspace',
email: normalizedEmail,
inviterId: context.inviterId,
organizationId: context.workspaceDetails.organizationId,
membershipIntent,
role: 'member',
grants: [
{
workspaceId: context.workspaceId,
permission: invitationPermission,
},
],
})
try {
PlatformEvents.workspaceMemberInvited({
workspaceId: context.workspaceId,
invitedBy: context.inviterId,
inviteeEmail: normalizedEmail,
role: invitationPermission,
membershipIntent,
})
} catch {
/**
* Telemetry must not fail invitation creation.
*/
}
captureServerEvent(
context.inviterId,
'workspace_member_invited',
{
workspace_id: context.workspaceId,
invitee_role: invitationPermission,
membership_intent: membershipIntent,
},
{
groups: { workspace: context.workspaceId },
setOnce: { first_invitation_sent_at: new Date().toISOString() },
}
)
const emailResult = await sendInvitationEmail({
invitationId,
token,
kind: 'workspace',
email: normalizedEmail,
inviterName: context.inviterName,
organizationId: context.workspaceDetails.organizationId,
organizationRole: 'member',
grants: [{ workspaceId: context.workspaceId, permission: invitationPermission }],
})
if (!emailResult.success) {
await cancelPendingInvitation(invitationId)
throw new WorkspaceInvitationError({
message: emailResult.error || 'Failed to send invitation email',
status: 502,
email: normalizedEmail,
})
}
recordAudit({
workspaceId: context.workspaceId,
actorId: context.inviterId,
actorName: context.inviterName,
actorEmail: context.inviterEmail,
action: AuditAction.MEMBER_INVITED,
resourceType: AuditResourceType.WORKSPACE,
resourceId: context.workspaceId,
resourceName: normalizedEmail,
description: `Invited ${normalizedEmail} as ${invitationPermission}`,
metadata: {
targetEmail: normalizedEmail,
targetRole: invitationPermission,
membershipIntent,
workspaceName: context.workspaceDetails.name,
invitationId,
},
request,
})
return {
id: invitationId,
workspaceId: context.workspaceId,
email: normalizedEmail,
permission: invitationPermission,
membershipIntent,
expiresAt: undefined,
}
}