mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
* improvement(repo): restructuring to make realtime image narrower scoped * improvements * chore(repo): rebase fixes and quality improvements for realtime split Addresses merge-time issues and gaps from the realtime app split: - Retarget stale vi.mock paths to @sim/workflow-persistence/subblocks - Restore README branding, fix AGENTS.md script reference - Restore TSDoc on workflow-persistence subblocks helpers - Use toError() from @sim/utils/errors in save.ts - Add vitest config + local mocks so @sim/audit tests run standalone - Move socket.io-client to devDependencies in apps/realtime - Add missing package COPY steps to docker/app.Dockerfile - Add check:boundaries/check:realtime-prune scripts and wire into CI Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(security): consolidate crypto primitives into @sim/security Move general-purpose crypto primitives out of apps/sim into the @sim/security package so both apps/sim and apps/realtime can share them. @sim/security exports (all pure, dependency-free): ./compare safeCompare (constant-time HMAC-wrapped equality) ./encryption encrypt/decrypt (AES-256-GCM, iv:cipher:tag format) ./hash sha256Hex ./tokens generateSecureToken (base64url) Migrate apps/sim call sites to use these + @sim/utils helpers: crypto.randomUUID() -> generateId() from @sim/utils/id createHash('sha256').digest -> sha256Hex timingSafeEqual on hashed hex -> safeCompare new Promise(setTimeout) -> sleep from @sim/utils/helpers No behavior change: encryption format, digest output, and token length are preserved exactly. * refactor(copilot): use toError in remaining otel/finalize sites Replace the last two `error instanceof Error ? error : new Error(String(error))` patterns with toError from @sim/utils/errors. Completes the sweep of clean candidates — no behavior change. * refactor(security): consolidate HMAC-SHA256 primitives into @sim/security Adds hmacSha256Hex and hmacSha256Base64 to @sim/security/hmac and migrates 15 webhook providers plus 5 other hot paths (deployment token signing, outbound webhook requests, workspace notification delivery, notification test route, Shopify OAuth callback) off bare `createHmac` calls. Secret parameter accepts `string | Buffer` to cover base64-decoded Svix-style secrets (Resend) and MS Teams' HMAC scheme. AWS SigV4 signing in S3 and Textract tools intentionally retains direct `createHmac` usage — its multi-step key derivation chain doesn't fit a generic helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore(packages): post-audit test + packaging polish - Add safeCompare unit tests (identity, length mismatch, hex-nibble diff). - Add Buffer-secret cases to hmac tests to lock in Svix/MS-Teams contract. - Declare `reactflow` as a peerDependency on @sim/workflow-types — only used for type imports. - Add a barrel export to @sim/workflow-persistence for consumers that prefer package-level imports; subpath exports retained. - Document the data-field invariant in load.ts for loop/parallel subflow patching. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore(realtime): address PR review feedback - Remove redundant SOCKET_PORT=3002 env from Dockerfile runner stage (env.PORT already defaults to 3002 via zod schema). - Reorder PORT fallback so an explicitly-set SOCKET_PORT wins over the schema default for PORT; keeps SOCKET_PORT functional as an override instead of dead code. - Add dedicated type-check CI step for @sim/realtime so TS errors surface pre-deploy (the Dockerfile runs source TS via Bun and has no implicit build-time type check). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore(realtime): remove unused SOCKET_PORT env var SOCKET_PORT has lived in the socket server since the June 2025 refactor but was never actually set in any deploy config — docker-compose.prod, helm values/templates, .env.example, and docs all use PORT or the 3002 default exclusively. No self-hoster was ever pointed at SOCKET_PORT, so removing it is safe. Simplifies realtime port resolution to `env.PORT` (zod-validated with a 3002 default) and drops the orphaned sim-side schema entry. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Waleed Latif <walif6@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
109 lines
3.4 KiB
TypeScript
109 lines
3.4 KiB
TypeScript
import { db, workflowBlocks, workflowEdges, workflowSubflows } from '@sim/db'
|
|
import { createLogger } from '@sim/logger'
|
|
import { toError } from '@sim/utils/errors'
|
|
import type { BlockState, WorkflowState } from '@sim/workflow-types/workflow'
|
|
import { SUBFLOW_TYPES } from '@sim/workflow-types/workflow'
|
|
import type { InferInsertModel } from 'drizzle-orm'
|
|
import { eq } from 'drizzle-orm'
|
|
import { generateLoopBlocks, generateParallelBlocks } from './subflow-helpers'
|
|
import type { DbOrTx } from './types'
|
|
|
|
const logger = createLogger('WorkflowPersistenceSave')
|
|
|
|
type SubflowInsert = InferInsertModel<typeof workflowSubflows>
|
|
|
|
export async function saveWorkflowToNormalizedTables(
|
|
workflowId: string,
|
|
state: WorkflowState,
|
|
externalTx?: DbOrTx
|
|
): Promise<{ success: boolean; error?: string }> {
|
|
const blockRecords = state.blocks as Record<string, BlockState>
|
|
const canonicalLoops = generateLoopBlocks(blockRecords)
|
|
const canonicalParallels = generateParallelBlocks(blockRecords)
|
|
|
|
const execute = async (tx: DbOrTx) => {
|
|
await Promise.all([
|
|
tx.delete(workflowBlocks).where(eq(workflowBlocks.workflowId, workflowId)),
|
|
tx.delete(workflowEdges).where(eq(workflowEdges.workflowId, workflowId)),
|
|
tx.delete(workflowSubflows).where(eq(workflowSubflows.workflowId, workflowId)),
|
|
])
|
|
|
|
if (Object.keys(state.blocks).length > 0) {
|
|
const blockInserts = Object.values(state.blocks).map((block) => ({
|
|
id: block.id,
|
|
workflowId,
|
|
type: block.type,
|
|
name: block.name || '',
|
|
positionX: String(block.position?.x || 0),
|
|
positionY: String(block.position?.y || 0),
|
|
enabled: block.enabled ?? true,
|
|
horizontalHandles: block.horizontalHandles ?? true,
|
|
advancedMode: block.advancedMode ?? false,
|
|
triggerMode: block.triggerMode ?? false,
|
|
height: String(block.height || 0),
|
|
subBlocks: block.subBlocks || {},
|
|
outputs: block.outputs || {},
|
|
data: block.data || {},
|
|
parentId: block.data?.parentId || null,
|
|
extent: block.data?.extent || null,
|
|
locked: block.locked ?? false,
|
|
}))
|
|
|
|
await tx.insert(workflowBlocks).values(blockInserts)
|
|
}
|
|
|
|
if (state.edges.length > 0) {
|
|
const edgeInserts = state.edges.map((edge) => ({
|
|
id: edge.id,
|
|
workflowId,
|
|
sourceBlockId: edge.source,
|
|
targetBlockId: edge.target,
|
|
sourceHandle: edge.sourceHandle || null,
|
|
targetHandle: edge.targetHandle || null,
|
|
}))
|
|
|
|
await tx.insert(workflowEdges).values(edgeInserts)
|
|
}
|
|
|
|
const subflowInserts: SubflowInsert[] = []
|
|
|
|
Object.values(canonicalLoops).forEach((loop) => {
|
|
subflowInserts.push({
|
|
id: loop.id,
|
|
workflowId,
|
|
type: SUBFLOW_TYPES.LOOP,
|
|
config: loop,
|
|
})
|
|
})
|
|
|
|
Object.values(canonicalParallels).forEach((parallel) => {
|
|
subflowInserts.push({
|
|
id: parallel.id,
|
|
workflowId,
|
|
type: SUBFLOW_TYPES.PARALLEL,
|
|
config: parallel,
|
|
})
|
|
})
|
|
|
|
if (subflowInserts.length > 0) {
|
|
await tx.insert(workflowSubflows).values(subflowInserts)
|
|
}
|
|
}
|
|
|
|
if (externalTx) {
|
|
await execute(externalTx)
|
|
return { success: true }
|
|
}
|
|
|
|
try {
|
|
await db.transaction(execute)
|
|
return { success: true }
|
|
} catch (error) {
|
|
logger.error(`Error saving workflow ${workflowId} to normalized tables:`, error)
|
|
return {
|
|
success: false,
|
|
error: toError(error).message,
|
|
}
|
|
}
|
|
}
|