d25d482dc2
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
250 lines
6.7 KiB
TypeScript
250 lines
6.7 KiB
TypeScript
import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
|
|
import { db } from '@sim/db'
|
|
import { chat } from '@sim/db/schema'
|
|
import { createLogger } from '@sim/logger'
|
|
import { generateId } from '@sim/utils/id'
|
|
import { and, eq, isNull } from 'drizzle-orm'
|
|
import { encryptSecret } from '@/lib/core/security/encryption'
|
|
import { getBaseUrl } from '@/lib/core/utils/urls'
|
|
import { performFullDeploy } from '@/lib/workflows/orchestration/deploy'
|
|
|
|
const logger = createLogger('ChatDeployOrchestration')
|
|
|
|
export interface ChatDeployPayload {
|
|
workflowId: string
|
|
userId: string
|
|
identifier: string
|
|
title: string
|
|
description?: string
|
|
/** Summary of what changed in this deployment version (distinct from the chat-facing `description`). */
|
|
versionDescription?: string
|
|
/** Short name/label for this deployment version. */
|
|
versionName?: string
|
|
customizations?: { primaryColor?: string; welcomeMessage?: string; imageUrl?: string }
|
|
authType?: 'public' | 'password' | 'email' | 'sso'
|
|
password?: string | null
|
|
allowedEmails?: string[]
|
|
outputConfigs?: Array<{ blockId: string; path: string }>
|
|
workspaceId?: string | null
|
|
}
|
|
|
|
export interface PerformChatDeployResult {
|
|
success: boolean
|
|
chatId?: string
|
|
chatUrl?: string
|
|
deployedAt?: Date | null
|
|
version?: number
|
|
error?: string
|
|
}
|
|
|
|
/**
|
|
* Deploys a chat: deploys the underlying workflow via `performFullDeploy`,
|
|
* encrypts passwords, creates or updates the chat record, fires telemetry,
|
|
* and records an audit entry. Both the chat API route and the copilot
|
|
* `deploy_chat` tool must use this function.
|
|
*/
|
|
export async function performChatDeploy(
|
|
params: ChatDeployPayload
|
|
): Promise<PerformChatDeployResult> {
|
|
const {
|
|
workflowId,
|
|
userId,
|
|
identifier,
|
|
title,
|
|
description = '',
|
|
authType = 'public',
|
|
password,
|
|
allowedEmails = [],
|
|
outputConfigs = [],
|
|
} = params
|
|
|
|
const customizations = {
|
|
primaryColor: params.customizations?.primaryColor || 'var(--brand-hover)',
|
|
welcomeMessage: params.customizations?.welcomeMessage || 'Hi there! How can I help you today?',
|
|
...(params.customizations?.imageUrl ? { imageUrl: params.customizations.imageUrl } : {}),
|
|
}
|
|
|
|
const deployResult = await performFullDeploy({
|
|
workflowId,
|
|
userId,
|
|
versionDescription: params.versionDescription,
|
|
versionName: params.versionName,
|
|
})
|
|
if (!deployResult.success) {
|
|
return { success: false, error: deployResult.error || 'Failed to deploy workflow' }
|
|
}
|
|
|
|
let encryptedPassword: string | null = null
|
|
if (authType === 'password' && password) {
|
|
const { encrypted } = await encryptSecret(password)
|
|
encryptedPassword = encrypted
|
|
}
|
|
|
|
const [existingDeployment] = await db
|
|
.select()
|
|
.from(chat)
|
|
.where(and(eq(chat.workflowId, workflowId), isNull(chat.archivedAt)))
|
|
.limit(1)
|
|
|
|
let chatId: string
|
|
if (existingDeployment) {
|
|
chatId = existingDeployment.id
|
|
|
|
let passwordToStore: string | null
|
|
if (authType === 'password') {
|
|
passwordToStore = encryptedPassword || existingDeployment.password
|
|
} else {
|
|
passwordToStore = null
|
|
}
|
|
|
|
await db
|
|
.update(chat)
|
|
.set({
|
|
identifier,
|
|
title,
|
|
description: description || null,
|
|
customizations,
|
|
authType,
|
|
password: passwordToStore,
|
|
allowedEmails: authType === 'email' || authType === 'sso' ? allowedEmails : [],
|
|
outputConfigs,
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(eq(chat.id, chatId))
|
|
} else {
|
|
chatId = generateId()
|
|
await db.insert(chat).values({
|
|
id: chatId,
|
|
workflowId,
|
|
userId,
|
|
identifier,
|
|
title,
|
|
description: description || null,
|
|
customizations,
|
|
isActive: true,
|
|
authType,
|
|
password: encryptedPassword,
|
|
allowedEmails: authType === 'email' || authType === 'sso' ? allowedEmails : [],
|
|
outputConfigs,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
})
|
|
}
|
|
|
|
const baseUrl = getBaseUrl()
|
|
let chatUrl: string
|
|
try {
|
|
const url = new URL(baseUrl)
|
|
let host = url.host
|
|
if (host.startsWith('www.')) {
|
|
host = host.substring(4)
|
|
}
|
|
chatUrl = `${url.protocol}//${host}/chat/${identifier}`
|
|
} catch {
|
|
chatUrl = `${baseUrl}/chat/${identifier}`
|
|
}
|
|
|
|
logger.info(`Chat "${title}" deployed successfully at ${chatUrl}`)
|
|
|
|
try {
|
|
const { PlatformEvents } = await import('@/lib/core/telemetry')
|
|
PlatformEvents.chatDeployed({
|
|
chatId,
|
|
workflowId,
|
|
authType,
|
|
hasOutputConfigs: outputConfigs.length > 0,
|
|
})
|
|
} catch (_e) {
|
|
// Telemetry is best-effort
|
|
}
|
|
|
|
recordAudit({
|
|
workspaceId: params.workspaceId || null,
|
|
actorId: userId,
|
|
action: AuditAction.CHAT_DEPLOYED,
|
|
resourceType: AuditResourceType.CHAT,
|
|
resourceId: chatId,
|
|
resourceName: title,
|
|
description: `Deployed chat "${title}"`,
|
|
metadata: {
|
|
workflowId,
|
|
identifier,
|
|
authType,
|
|
chatUrl,
|
|
isUpdate: !!existingDeployment,
|
|
hasOutputConfigs: outputConfigs.length > 0,
|
|
hasCustomizations: !!(
|
|
params.customizations?.primaryColor ||
|
|
params.customizations?.welcomeMessage ||
|
|
params.customizations?.imageUrl
|
|
),
|
|
},
|
|
})
|
|
|
|
return {
|
|
success: true,
|
|
chatId,
|
|
chatUrl,
|
|
deployedAt: deployResult.deployedAt,
|
|
version: deployResult.version,
|
|
}
|
|
}
|
|
|
|
export interface PerformChatUndeployParams {
|
|
chatId: string
|
|
userId: string
|
|
workspaceId?: string | null
|
|
}
|
|
|
|
export interface PerformChatUndeployResult {
|
|
success: boolean
|
|
error?: string
|
|
}
|
|
|
|
/**
|
|
* Undeploys a chat: deletes the chat record and records an audit entry.
|
|
* Both the chat manage DELETE route and the copilot `deploy_chat` undeploy
|
|
* action must use this function.
|
|
*/
|
|
export async function performChatUndeploy(
|
|
params: PerformChatUndeployParams
|
|
): Promise<PerformChatUndeployResult> {
|
|
const { chatId, userId, workspaceId } = params
|
|
|
|
const [chatRecord] = await db
|
|
.select({
|
|
title: chat.title,
|
|
workflowId: chat.workflowId,
|
|
identifier: chat.identifier,
|
|
authType: chat.authType,
|
|
})
|
|
.from(chat)
|
|
.where(eq(chat.id, chatId))
|
|
.limit(1)
|
|
|
|
if (!chatRecord) {
|
|
return { success: false, error: 'Chat not found' }
|
|
}
|
|
|
|
await db.delete(chat).where(eq(chat.id, chatId))
|
|
|
|
logger.info(`Chat "${chatId}" deleted successfully`)
|
|
|
|
recordAudit({
|
|
workspaceId: workspaceId || null,
|
|
actorId: userId,
|
|
action: AuditAction.CHAT_DELETED,
|
|
resourceType: AuditResourceType.CHAT,
|
|
resourceId: chatId,
|
|
resourceName: chatRecord.title || chatId,
|
|
description: `Deleted chat deployment "${chatRecord.title || chatId}"`,
|
|
metadata: {
|
|
workflowId: chatRecord.workflowId || undefined,
|
|
identifier: chatRecord.identifier || undefined,
|
|
authType: chatRecord.authType || undefined,
|
|
},
|
|
})
|
|
|
|
return { success: true }
|
|
}
|