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
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:
@@ -0,0 +1,249 @@
|
||||
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 }
|
||||
}
|
||||
Reference in New Issue
Block a user