diff --git a/.claude/rules/landing-seo-geo.md b/.claude/rules/landing-seo-geo.md
index 3f69b3993ef..4f6ec536ad0 100644
--- a/.claude/rules/landing-seo-geo.md
+++ b/.claude/rules/landing-seo-geo.md
@@ -22,7 +22,7 @@ paths:
- **Answer-first pattern**: each section's H2 + subtitle should directly answer a user question (e.g. "What is Sim?", "How fast can I deploy?").
- **Atomic answer blocks**: each feature / template card should be independently extractable by an AI summariser.
- **Entity consistency**: always write "Sim" by name — never "the platform" or "our tool".
-- **Keyword density**: first 150 visible chars of Hero must name "Sim", "AI workspace", "AI agents".
+- **Keyword density**: first 150 visible chars of Hero must name "AI workspace" and "AI agents". "Sim" is carried by the title tag, the meta description, and the Hero `sr-only` summary — the H1 does not have to spend its opening words on the brand.
- **sr-only summaries**: Hero and Templates each have a `
` (~50 words) as an atomic product/catalog summary for AI citation.
- **Specific numbers**: prefer concrete figures ("1,000+ integrations", "15+ AI providers") over vague claims.
diff --git a/apps/docs/content/docs/en/api-reference/(generated)/workflows/meta.json b/apps/docs/content/docs/en/api-reference/(generated)/workflows/meta.json
index 491129e3cdf..8e2caa1abe8 100644
--- a/apps/docs/content/docs/en/api-reference/(generated)/workflows/meta.json
+++ b/apps/docs/content/docs/en/api-reference/(generated)/workflows/meta.json
@@ -5,6 +5,8 @@
"cancelExecution",
"listWorkflows",
"getWorkflow",
+ "exportWorkflow",
+ "importWorkflow",
"deployWorkflow",
"undeployWorkflow",
"rollbackWorkflow",
diff --git a/apps/docs/openapi.json b/apps/docs/openapi.json
index 49d6e7ed91b..1db7ab8e662 100644
--- a/apps/docs/openapi.json
+++ b/apps/docs/openapi.json
@@ -1035,6 +1035,160 @@
}
}
},
+ "/api/v1/workflows/import": {
+ "post": {
+ "operationId": "importWorkflow",
+ "summary": "Import Workflow",
+ "description": "Create a new workflow in a workspace from an export payload produced by GET /api/v1/workflows/{id}/export. Block, edge, loop and parallel identifiers are regenerated, so the same payload can be imported repeatedly and alongside its source workflow. The workflow name is de-duplicated against the target folder. Requires write permission on the target workspace.",
+ "tags": ["Workflows"],
+ "x-codeSamples": [
+ {
+ "id": "curl",
+ "label": "cURL",
+ "lang": "bash",
+ "source": "curl -X POST \\\n \"https://www.sim.ai/api/v1/workflows/import\" \\\n -H \"X-API-Key: YOUR_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"workspaceId\": \"a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64\", \"workflow\": {\"version\": \"1.0\", \"state\": {\"blocks\": {}, \"edges\": []}}}'"
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "description": "The target workspace and the workflow payload to import.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": ["workspaceId", "workflow"],
+ "properties": {
+ "workspaceId": {
+ "type": "string",
+ "minLength": 1,
+ "description": "The workspace to import the workflow into. Requires write permission.",
+ "example": "a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64"
+ },
+ "folderId": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Optional folder to place the imported workflow in. Defaults to the workspace root.",
+ "example": "8a4c2e6b-0d1f-4b3a-9c5e-7f2d8b4a6c91"
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200,
+ "description": "Optional override for the imported workflow's name. Defaults to the name carried in the payload, then to \"Imported Workflow\".",
+ "example": "Customer Support Agent (copy)"
+ },
+ "description": {
+ "type": "string",
+ "maxLength": 2000,
+ "description": "Optional override for the imported workflow's description. Defaults to the description carried in the payload.",
+ "example": "Imported from the staging workspace"
+ },
+ "workflow": {
+ "description": "The workflow to import. Accepts the export envelope returned by GET /api/v1/workflows/{id}/export, a bare workflow state object (`{ blocks, edges, ... }`), or a JSON string of either.",
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/WorkflowExport"
+ },
+ {
+ "type": "object",
+ "additionalProperties": true,
+ "minProperties": 1,
+ "description": "A bare workflow state object."
+ },
+ {
+ "type": "string",
+ "minLength": 1,
+ "description": "A JSON string of either accepted object form."
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": "The workflow was imported.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "description": "The newly created workflow.",
+ "$ref": "#/components/schemas/ImportedWorkflow"
+ },
+ "limits": {
+ "$ref": "#/components/schemas/Limits",
+ "description": "Rate limit and usage information for the current API key."
+ }
+ }
+ },
+ "example": {
+ "data": {
+ "id": "7d2e9f14-3a6b-4c8d-b1e5-9f0a2c7d4e83",
+ "name": "Customer Support Agent (1)",
+ "description": "Routes incoming support tickets",
+ "workspaceId": "a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64",
+ "folderId": null,
+ "createdAt": "2026-06-20T14:15:22Z",
+ "updatedAt": "2026-06-20T14:15:22Z"
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
+ },
+ "404": {
+ "$ref": "#/components/responses/NotFound"
+ },
+ "413": {
+ "description": "The request body exceeds the 10 MB import limit.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "error": {
+ "type": "string",
+ "example": "Request body exceeds the maximum allowed size"
+ }
+ }
+ }
+ }
+ }
+ },
+ "423": {
+ "description": "The target folder is locked and cannot accept new workflows.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "error": {
+ "type": "string",
+ "example": "Folder is locked"
+ }
+ }
+ }
+ }
+ }
+ },
+ "429": {
+ "$ref": "#/components/responses/RateLimited"
+ }
+ }
+ }
+ },
"/api/v1/workflows/{id}": {
"get": {
"operationId": "getWorkflow",
@@ -1104,6 +1258,92 @@
}
}
},
+ "/api/v1/workflows/{id}/export": {
+ "get": {
+ "operationId": "exportWorkflow",
+ "summary": "Export Workflow",
+ "description": "Export a workflow as a portable JSON envelope that POST /api/v1/workflows/import accepts verbatim. Credential and password field values stored on blocks are redacted from the payload, while `{{ENV_VAR}}` references and block positions are preserved. Workflow variables are returned as stored \u2014 they are plaintext configuration readable by anyone with workspace read, and secrets belong in environment variables, which travel as unresolved references. Requires read permission on the workflow's workspace. Returns 404 when the workflow does not exist or you do not have access to it. Workspace-scoped bindings (knowledge bases, workspace files, channels, projects, folders, MCP servers) are cleared, since those ids do not resolve in another workspace \u2014 an imported copy needs them re-selected.",
+ "tags": ["Workflows"],
+ "x-codeSamples": [
+ {
+ "id": "curl",
+ "label": "cURL",
+ "lang": "bash",
+ "source": "curl -X GET \\\n \"https://www.sim.ai/api/v1/workflows/{id}/export\" \\\n -H \"X-API-Key: YOUR_API_KEY\""
+ }
+ ],
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "description": "The unique workflow identifier.",
+ "schema": {
+ "type": "string",
+ "example": "3b1f7c92-8d4e-4a6b-9c0d-5e2f8a714b36"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "The workflow export envelope.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "description": "The workflow export envelope.",
+ "$ref": "#/components/schemas/WorkflowExport"
+ },
+ "limits": {
+ "$ref": "#/components/schemas/Limits",
+ "description": "Rate limit and usage information for the current API key."
+ }
+ }
+ },
+ "example": {
+ "data": {
+ "version": "1.0",
+ "exportedAt": "2026-06-20T14:15:22Z",
+ "workflow": {
+ "id": "3b1f7c92-8d4e-4a6b-9c0d-5e2f8a714b36",
+ "name": "Customer Support Agent",
+ "description": "Routes incoming support tickets",
+ "workspaceId": "a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64",
+ "folderId": null
+ },
+ "state": {
+ "blocks": {},
+ "edges": [],
+ "loops": {},
+ "parallels": {},
+ "metadata": {
+ "name": "Customer Support Agent",
+ "description": "Routes incoming support tickets",
+ "exportedAt": "2026-06-20T14:15:22Z"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "404": {
+ "$ref": "#/components/responses/NotFound"
+ },
+ "429": {
+ "$ref": "#/components/responses/RateLimited"
+ }
+ }
+ }
+ },
"/api/v1/workflows/{id}/deploy": {
"post": {
"operationId": "deployWorkflow",
@@ -7299,6 +7539,154 @@
}
}
}
+ },
+ "WorkflowExportState": {
+ "type": "object",
+ "description": "The workflow graph. Block, edge, loop and parallel identifiers are regenerated on import, so a payload can be imported repeatedly without colliding with its source workflow.",
+ "properties": {
+ "blocks": {
+ "type": "object",
+ "description": "Map of block id to block definition, including canvas position and sub-block values.",
+ "additionalProperties": true,
+ "example": {}
+ },
+ "edges": {
+ "type": "array",
+ "description": "Connections between blocks.",
+ "items": {
+ "type": "object",
+ "additionalProperties": true
+ },
+ "example": []
+ },
+ "loops": {
+ "type": "object",
+ "description": "Loop container definitions, keyed by loop id.",
+ "additionalProperties": true,
+ "example": {}
+ },
+ "parallels": {
+ "type": "object",
+ "description": "Parallel container definitions, keyed by parallel id.",
+ "additionalProperties": true,
+ "example": {}
+ },
+ "metadata": {
+ "type": "object",
+ "description": "The source workflow's name, description and export timestamp.",
+ "properties": {
+ "name": {
+ "type": "string",
+ "example": "Customer Support Agent"
+ },
+ "description": {
+ "type": "string",
+ "example": "Routes incoming support tickets"
+ },
+ "exportedAt": {
+ "type": "string",
+ "format": "date-time",
+ "example": "2026-06-20T14:15:22Z"
+ }
+ }
+ },
+ "variables": {
+ "type": "object",
+ "description": "Workflow-level variables, keyed by variable id. Emitted as stored \u2014 treat as plaintext configuration, not a secret store.",
+ "additionalProperties": true,
+ "example": {}
+ }
+ }
+ },
+ "WorkflowExport": {
+ "type": "object",
+ "description": "A portable workflow export envelope. Pass this object straight back to POST /api/v1/workflows/import to recreate the workflow. Credential and password field values stored on blocks are redacted; `{{ENV_VAR}}` references and workflow variables are preserved as stored.",
+ "properties": {
+ "version": {
+ "type": "string",
+ "description": "Export format version.",
+ "enum": ["1.0"],
+ "example": "1.0"
+ },
+ "exportedAt": {
+ "type": "string",
+ "format": "date-time",
+ "description": "ISO 8601 timestamp of when the export was generated.",
+ "example": "2026-06-20T14:15:22Z"
+ },
+ "workflow": {
+ "type": "object",
+ "description": "Identity of the exported workflow at the time of export.",
+ "properties": {
+ "id": {
+ "type": "string",
+ "example": "3b1f7c92-8d4e-4a6b-9c0d-5e2f8a714b36"
+ },
+ "name": {
+ "type": "string",
+ "example": "Customer Support Agent"
+ },
+ "description": {
+ "type": "string",
+ "nullable": true,
+ "example": "Routes incoming support tickets"
+ },
+ "workspaceId": {
+ "type": "string",
+ "nullable": true,
+ "example": "a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64"
+ },
+ "folderId": {
+ "type": "string",
+ "nullable": true,
+ "example": null
+ }
+ }
+ },
+ "state": {
+ "$ref": "#/components/schemas/WorkflowExportState"
+ }
+ }
+ },
+ "ImportedWorkflow": {
+ "type": "object",
+ "description": "The workflow created by an import.",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique identifier of the newly created workflow.",
+ "example": "7d2e9f14-3a6b-4c8d-b1e5-9f0a2c7d4e83"
+ },
+ "name": {
+ "type": "string",
+ "description": "The workflow name after de-duplication against the target folder.",
+ "example": "Customer Support Agent (1)"
+ },
+ "description": {
+ "type": "string",
+ "nullable": true,
+ "example": "Routes incoming support tickets"
+ },
+ "workspaceId": {
+ "type": "string",
+ "example": "a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64"
+ },
+ "folderId": {
+ "type": "string",
+ "nullable": true,
+ "example": null
+ },
+ "createdAt": {
+ "type": "string",
+ "format": "date-time",
+ "example": "2026-06-20T14:15:22Z"
+ },
+ "updatedAt": {
+ "type": "string",
+ "format": "date-time",
+ "example": "2026-06-20T14:15:22Z"
+ }
+ }
}
},
"responses": {
diff --git a/apps/docs/package.json b/apps/docs/package.json
index 8581c46ea31..da9850a4834 100644
--- a/apps/docs/package.json
+++ b/apps/docs/package.json
@@ -20,7 +20,6 @@
"@sim/db": "workspace:*",
"@sim/emcn": "workspace:*",
"@sim/workflow-renderer": "workspace:*",
- "@vercel/og": "^0.6.5",
"ai": "5.0.203",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -32,7 +31,6 @@
"lucide-react": "^0.511.0",
"next": "16.2.11",
"next-themes": "^0.4.6",
- "postgres": "^3.4.5",
"react": "19.2.4",
"react-dom": "19.2.4",
"remark-breaks": "^4.0.0",
@@ -47,7 +45,7 @@
"@sim/tsconfig": "workspace:*",
"@tailwindcss/postcss": "^4.0.12",
"@types/mdx": "^2.0.13",
- "@types/node": "^22.14.1",
+ "@types/node": "24.2.1",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.0.4",
"@typescript/native-preview": "7.0.0-dev.20260707.2",
diff --git a/apps/sim/app/(landing)/CLAUDE.md b/apps/sim/app/(landing)/CLAUDE.md
index 5d96ffdf9ed..f6b82e94dc3 100644
--- a/apps/sim/app/(landing)/CLAUDE.md
+++ b/apps/sim/app/(landing)/CLAUDE.md
@@ -44,7 +44,7 @@ Target: Lighthouse 95+ on mobile, LCP < 2.0s, CLS < 0.05, minimal hydration cost
`page.tsx` owns the metadata (title, description, OG/Twitter, canonical, robots) - keep it the single source of truth and keep it aligned with the constitution's claim hierarchy. Beyond metadata:
-- **One `
`, in the hero, containing "Sim" and "AI workspace".** Strict hierarchy below it: H2 per section, H3 for items within a section. Never skip levels, never add a second H1.
+- **One ``, in the hero, containing "AI workspace".** The brand carries in the title tag, the meta description, and the hero's `sr-only` summary, so the H1 itself is free to lead with the non-brand keywords people actually search ("AI workspace", "AI agents") rather than spending its first two words on "Sim is the". Whichever wording it uses, the hero's `sr-only` paragraph must still open by naming Sim. Strict hierarchy below it: H2 per section, H3 for items within a section. Never skip levels, never add a second H1.
- **Semantic landmarks**: ``, ``, ``, ``; each section is ``. Decorative/animated elements get `aria-hidden='true'`.
- **Structured data**: emit JSON-LD (`Organization`, `WebSite`, `WebApplication` with `featureList`, `FAQPage` if an FAQ exists) from a server component rendered before visible content. Keep `featureList` in sync with the features actually shown on the page.
- **Crawlable links**: all internal navigation uses Next ` ` with real `href`s - never `onClick` navigation. External links get `rel='noopener noreferrer'`.
diff --git a/apps/sim/app/(landing)/changelog/components/changelog-actions/changelog-actions.tsx b/apps/sim/app/(landing)/changelog/components/changelog-actions/changelog-actions.tsx
index db14939e050..8bfb499b774 100644
--- a/apps/sim/app/(landing)/changelog/components/changelog-actions/changelog-actions.tsx
+++ b/apps/sim/app/(landing)/changelog/components/changelog-actions/changelog-actions.tsx
@@ -1,7 +1,8 @@
'use client'
import { ChipLink } from '@sim/emcn'
-import { BookOpen, Github, Rss } from 'lucide-react'
+import { BookOpen, Rss } from 'lucide-react'
+import { GithubOutlineIcon } from '@/components/icons'
/**
* Changelog hero actions - the GitHub / Documentation / RSS pill links shown
@@ -18,7 +19,7 @@ export function ChangelogActions() {
href='https://github.com/simstudioai/sim/releases'
target='_blank'
rel='noopener noreferrer'
- leftIcon={Github}
+ leftIcon={GithubOutlineIcon}
>
View on GitHub
diff --git a/apps/sim/app/(landing)/components/hero/hero.tsx b/apps/sim/app/(landing)/components/hero/hero.tsx
index 8036e4b082c..cb91350ab03 100644
--- a/apps/sim/app/(landing)/components/hero/hero.tsx
+++ b/apps/sim/app/(landing)/components/hero/hero.tsx
@@ -86,8 +86,8 @@ export function Hero() {
headingId='hero-heading'
heading={
<>
- Sim is the AI workspace for
- building and managing AI agents.
+ The AI Workspace for Building
+ and Managing AI Agents.
>
}
description='Open source, with 1,000+ integrations and every major LLM. Build, deploy, and manage agents visually, conversationally, or with code.'
diff --git a/apps/sim/app/api/files/export/[id]/route.test.ts b/apps/sim/app/api/files/export/[id]/route.test.ts
new file mode 100644
index 00000000000..069e1fc30a9
--- /dev/null
+++ b/apps/sim/app/api/files/export/[id]/route.test.ts
@@ -0,0 +1,179 @@
+/**
+ * @vitest-environment node
+ */
+import { createMockRequest } from '@sim/testing'
+import JSZip from 'jszip'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+import { PayloadSizeLimitError } from '@/lib/core/utils/stream-limits'
+
+const {
+ mockCheckAuth,
+ mockGetFileMetadataById,
+ mockVerifyFileAccess,
+ mockDownloadFile,
+ mockExtractEmbeddedImageIds,
+} = vi.hoisted(() => ({
+ mockCheckAuth: vi.fn(),
+ mockGetFileMetadataById: vi.fn(),
+ mockVerifyFileAccess: vi.fn(),
+ mockDownloadFile: vi.fn(),
+ mockExtractEmbeddedImageIds: vi.fn(),
+}))
+
+vi.mock('@/lib/auth/hybrid', () => ({ checkSessionOrInternalAuth: mockCheckAuth }))
+vi.mock('@/lib/uploads/server/metadata', () => ({
+ getFileMetadataById: mockGetFileMetadataById,
+}))
+vi.mock('@/app/api/files/authorization', () => ({ verifyFileAccess: mockVerifyFileAccess }))
+vi.mock('@/lib/uploads/core/storage-service', () => ({ downloadFile: mockDownloadFile }))
+vi.mock('@/lib/copilot/tools/server/files/embedded-image-refs', () => ({
+ extractEmbeddedImageIds: mockExtractEmbeddedImageIds,
+}))
+vi.mock('@sim/audit', () => ({
+ recordAudit: vi.fn(),
+ AuditAction: { FILE_DOWNLOADED: 'file.downloaded' },
+ AuditResourceType: { FILE: 'file' },
+}))
+vi.mock('@/lib/posthog/server', () => ({ captureServerEvent: vi.fn() }))
+
+import { GET } from '@/app/api/files/export/[id]/route'
+
+const MB = 1024 * 1024
+const DOC_ID = 'doc-1'
+const context = { params: Promise.resolve({ id: DOC_ID }) }
+
+function request() {
+ return createMockRequest('GET', undefined, {}, `http://localhost:3000/api/files/export/${DOC_ID}`)
+}
+
+function assetRecord(id: string, size: number) {
+ return {
+ id,
+ key: `workspace/ws-1/${id}`,
+ originalName: `${id}.png`,
+ contentType: 'image/png',
+ context: 'workspace',
+ size,
+ workspaceId: 'ws-1',
+ }
+}
+
+describe('markdown export bundling', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ mockCheckAuth.mockResolvedValue({ success: true, userId: 'user-1' })
+ mockVerifyFileAccess.mockResolvedValue(true)
+ mockGetFileMetadataById.mockImplementation(async (id: string) =>
+ id === DOC_ID
+ ? {
+ id: DOC_ID,
+ key: 'workspace/ws-1/doc.md',
+ originalName: 'doc.md',
+ contentType: 'text/markdown',
+ context: 'workspace',
+ size: 1024,
+ workspaceId: 'ws-1',
+ }
+ : assetRecord(id, 1 * MB)
+ )
+ mockDownloadFile.mockResolvedValue(Buffer.from('# Doc\n'))
+ mockExtractEmbeddedImageIds.mockReturnValue([])
+ })
+
+ it('rejects on declared asset bytes before downloading any of them', async () => {
+ mockExtractEmbeddedImageIds.mockReturnValue(['a', 'b', 'c'])
+ mockGetFileMetadataById.mockImplementation(async (id: string) =>
+ id === DOC_ID
+ ? {
+ id: DOC_ID,
+ key: 'workspace/ws-1/doc.md',
+ originalName: 'doc.md',
+ contentType: 'text/markdown',
+ context: 'workspace',
+ size: 1024,
+ workspaceId: 'ws-1',
+ }
+ : assetRecord(id, 100 * MB)
+ )
+
+ const response = await GET(request(), context)
+
+ expect(response.status).toBe(400)
+ expect((await response.json()).error).toContain('exceeds')
+ // Only the markdown body was read; the 300 MB of assets never left storage.
+ expect(mockDownloadFile).toHaveBeenCalledTimes(1)
+ })
+
+ it('counts the document body against the export limit, not just its assets', async () => {
+ // Assets alone sit under the cap; the body is what carries the bundle over it.
+ mockExtractEmbeddedImageIds.mockReturnValue(['a'])
+ mockDownloadFile.mockResolvedValue(Buffer.alloc(250 * MB))
+
+ const response = await GET(request(), context)
+
+ expect(response.status).toBe(400)
+ expect((await response.json()).error).toContain('document and its embedded files')
+ })
+
+ it('caps the document body read rather than loading it unbounded', async () => {
+ mockExtractEmbeddedImageIds.mockReturnValue([])
+
+ await GET(request(), context)
+
+ const bodyCall = mockDownloadFile.mock.calls.find(([options]) => options.key.endsWith('doc.md'))
+ expect(bodyCall?.[0].maxBytes).toBe(250 * MB)
+ })
+
+ it('reports an oversized body as a size rejection, not a server error', async () => {
+ mockExtractEmbeddedImageIds.mockReturnValue([])
+ mockDownloadFile.mockRejectedValue(
+ new PayloadSizeLimitError({ label: 'storage file download', maxBytes: 1 })
+ )
+
+ const response = await GET(request(), context)
+
+ // The cap exists to produce a clear limit message; a 500 would hide it.
+ expect(response.status).toBe(400)
+ expect((await response.json()).error).toContain('export limit')
+ })
+
+ it('caps each asset download rather than trusting its declared size', async () => {
+ mockExtractEmbeddedImageIds.mockReturnValue(['a'])
+
+ await GET(request(), context)
+
+ const assetCall = mockDownloadFile.mock.calls.find(
+ ([options]) => options.key === 'workspace/ws-1/a'
+ )
+ expect(assetCall?.[0].maxBytes).toBe(25 * MB)
+ })
+
+ it('drops an unreadable asset instead of failing the whole export', async () => {
+ mockExtractEmbeddedImageIds.mockReturnValue(['good', 'bad'])
+ mockDownloadFile.mockImplementation(async ({ key }: { key: string }) => {
+ if (key.endsWith('doc.md')) return Buffer.from('# Doc\n\n')
+ if (key.endsWith('bad')) throw new Error('storage down')
+ return Buffer.from('png-bytes')
+ })
+
+ const response = await GET(request(), context)
+
+ expect(response.status).toBe(200)
+ const zip = await JSZip.loadAsync(Buffer.from(await response.arrayBuffer()))
+ expect(zip.file('assets/good.png')).not.toBeNull()
+ expect(zip.file('assets/bad.png')).toBeNull()
+ })
+
+ it('skips an asset the caller cannot read', async () => {
+ mockExtractEmbeddedImageIds.mockReturnValue(['secret'])
+ mockVerifyFileAccess.mockImplementation(async (key: string) => !key.endsWith('secret'))
+
+ const response = await GET(request(), context)
+
+ expect(response.status).toBe(200)
+ // Authorization is settled during metadata resolution, before any asset read.
+ expect(mockDownloadFile.mock.calls.some(([options]) => options.key.endsWith('secret'))).toBe(
+ false
+ )
+ })
+})
diff --git a/apps/sim/app/api/files/export/[id]/route.ts b/apps/sim/app/api/files/export/[id]/route.ts
index 414d781c3a0..0e578ada87b 100644
--- a/apps/sim/app/api/files/export/[id]/route.ts
+++ b/apps/sim/app/api/files/export/[id]/route.ts
@@ -9,17 +9,32 @@ import { fileExportContract } from '@/lib/api/contracts/storage-transfer'
import { parseRequest } from '@/lib/api/server'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { extractEmbeddedImageIds } from '@/lib/copilot/tools/server/files/embedded-image-refs'
+import { MATERIALIZE_CONCURRENCY, mapWithConcurrency } from '@/lib/core/utils/concurrency'
+import { isPayloadSizeLimitError } from '@/lib/core/utils/stream-limits'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { captureServerEvent } from '@/lib/posthog/server'
import type { StorageContext } from '@/lib/uploads/config'
import { getServeStoragePrefix } from '@/lib/uploads/config'
import { downloadFile } from '@/lib/uploads/core/storage-service'
import { getFileMetadataById } from '@/lib/uploads/server/metadata'
+import { formatFileSize } from '@/lib/uploads/utils/file-utils'
import { verifyFileAccess } from '@/app/api/files/authorization'
import { encodeFilenameForHeader } from '@/app/api/files/utils'
const logger = createLogger('FilesExportAPI')
+/**
+ * Byte ceilings for a bundled export. The bytes behind an embed list are whatever the
+ * author put there, so without these the export would materialize unbounded assets in
+ * one request. They match the bulk-download route, so the two export surfaces reject at
+ * the same size.
+ *
+ * There is deliberately no count cap here: `extractEmbeddedFileRefs` already stops at
+ * `MAX_EMBEDDED_IMAGES`, so the list this route receives is bounded before it arrives.
+ */
+const MAX_EXPORT_ASSET_BYTES = 25 * 1024 * 1024
+const MAX_EXPORT_TOTAL_BYTES = 250 * 1024 * 1024
+
const MARKDOWN_MIME_TYPES = new Set(['text/markdown', 'text/x-markdown'])
const MARKDOWN_EXTENSIONS = new Set(['md', 'markdown'])
@@ -112,10 +127,26 @@ export const GET = withRouteHandler(
return NextResponse.redirect(new URL(servePath, request.url), { status: 302 })
}
- const mdBuffer = await downloadFile({
- key: record.key,
- context: record.context as StorageContext,
- })
+ // Capped like everything else in the bundle: the document body is usually the
+ // largest single entry, so leaving it unbounded left the export limit unenforced
+ // against the one item most able to exceed it. A body that alone exceeds the limit
+ // is a size rejection, so it reports as one rather than as a server error.
+ let mdBuffer: Buffer
+ try {
+ mdBuffer = await downloadFile({
+ key: record.key,
+ context: record.context as StorageContext,
+ maxBytes: MAX_EXPORT_TOTAL_BYTES,
+ })
+ } catch (error) {
+ if (!isPayloadSizeLimitError(error)) throw error
+ return NextResponse.json(
+ {
+ error: `This document exceeds the ${formatFileSize(MAX_EXPORT_TOTAL_BYTES)} export limit.`,
+ },
+ { status: 400 }
+ )
+ }
let mdContent = mdBuffer.toString('utf-8')
const imageIds = extractEmbeddedImageIds(mdContent)
@@ -136,34 +167,67 @@ export const GET = withRouteHandler(
})
}
- const fetchResults = await Promise.allSettled(
- imageIds.map(async (imageId) => {
- const imgRecord = await getFileMetadataById(imageId)
- if (!imgRecord) return null
- const imgHasAccess = await verifyFileAccess(imgRecord.key, userId)
- if (!imgHasAccess) return null
- const imgBuffer = await downloadFile({
- key: imgRecord.key,
- context: imgRecord.context as StorageContext,
- })
- return { imageId, originalName: imgRecord.originalName, buffer: imgBuffer }
+ // Metadata first: declared sizes bound the download before a byte is read, and the
+ // authorization check costs nothing to run here.
+ const assetTargets = (
+ await mapWithConcurrency(imageIds, MATERIALIZE_CONCURRENCY, async (imageId) => {
+ try {
+ const imgRecord = await getFileMetadataById(imageId)
+ if (!imgRecord) return null
+ if (!(await verifyFileAccess(imgRecord.key, userId))) return null
+ return { imageId, record: imgRecord }
+ } catch (error) {
+ logger.warn('Failed to resolve asset for export', {
+ imageId,
+ error: toError(error).message,
+ })
+ return null
+ }
})
+ ).filter((target): target is NonNullable => target !== null)
+
+ // The body counts against the same budget as its assets — the zip holds both, so a
+ // limit that measured only the attachments would not describe the archive produced.
+ const bundleBytes =
+ mdBuffer.length + assetTargets.reduce((sum, target) => sum + target.record.size, 0)
+ if (bundleBytes > MAX_EXPORT_TOTAL_BYTES) {
+ return NextResponse.json(
+ {
+ error: `This document and its embedded files total ${formatFileSize(bundleBytes)}, which exceeds the ${formatFileSize(MAX_EXPORT_TOTAL_BYTES)} export limit.`,
+ },
+ { status: 400 }
+ )
+ }
+
+ const fetched = await mapWithConcurrency(
+ assetTargets,
+ MATERIALIZE_CONCURRENCY,
+ async ({ imageId, record: imgRecord }) => {
+ try {
+ const buffer = await downloadFile({
+ key: imgRecord.key,
+ context: imgRecord.context as StorageContext,
+ maxBytes: MAX_EXPORT_ASSET_BYTES,
+ })
+ return { imageId, originalName: imgRecord.originalName, buffer }
+ } catch (error) {
+ // A single unreadable or oversized asset drops out of the bundle rather than
+ // failing the whole export; the markdown keeps its original link.
+ logger.warn('Failed to fetch asset for export', {
+ imageId,
+ error: toError(error).message,
+ })
+ return null
+ }
+ }
)
const assetMap = new Map()
const usedFilenames = new Set()
- for (let i = 0; i < fetchResults.length; i++) {
- const result = fetchResults[i]
- if (result.status === 'rejected') {
- logger.warn('Failed to fetch asset for export', {
- imageId: imageIds[i],
- error: toError(result.reason).message,
- })
- continue
- }
- if (!result.value) continue
- const { imageId, originalName, buffer } = result.value
+ for (const result of fetched) {
+ if (!result) continue
+ const { imageId, originalName, buffer } = result
const preferred = safeFilename(originalName)
const filename = deduplicatedFilename(preferred, usedFilenames, imageId)
usedFilenames.add(filename)
diff --git a/apps/sim/app/api/tools/file/manage/route.ts b/apps/sim/app/api/tools/file/manage/route.ts
index 248002656ee..8f1cca412f6 100644
--- a/apps/sim/app/api/tools/file/manage/route.ts
+++ b/apps/sim/app/api/tools/file/manage/route.ts
@@ -11,6 +11,7 @@ import { checkInternalAuth } from '@/lib/auth/hybrid'
import { splitWorkspaceFilePath } from '@/lib/copilot/tools/server/files/workspace-file'
import { acquireLock, releaseLock } from '@/lib/core/config/redis'
import { generateRequestId } from '@/lib/core/utils/request'
+import { isPayloadSizeLimitError } from '@/lib/core/utils/stream-limits'
import { ensureAbsoluteUrl } from '@/lib/core/utils/urls'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { isSupportedFileType, parseBuffer } from '@/lib/file-parsers'
@@ -685,7 +686,11 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
const denied = await assertToolFileAccess(userFile.key, userId, requestId, logger)
if (denied) return denied
- const buffer = await downloadFileFromStorage(userFile, requestId, logger, {
+ // Generated docs store their generation source, not the rendered binary, so
+ // the archive must carry the servable bytes instead of the raw source text.
+ // A still-compiling artifact throws, and the handler's catch turns that into
+ // the shared 409 via `docNotReadyResponse`.
+ const { buffer } = await downloadServableFileFromStorage(userFile, requestId, logger, {
maxBytes: MAX_COMPRESS_FILE_BYTES,
})
totalBytes += buffer.length
@@ -864,6 +869,11 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
}
const notReady = docNotReadyResponse(error)
if (notReady) return notReady
+ // A file over its per-file cap is a size rejection, not a fault. Rendered
+ // documents can cross it even when the stored source was well under.
+ if (isPayloadSizeLimitError(error)) {
+ return NextResponse.json({ success: false, error: error.message }, { status: 413 })
+ }
if (error instanceof ShareValidationError) {
return NextResponse.json({ success: false, error: error.message }, { status: 400 })
}
diff --git a/apps/sim/app/api/v1/admin/folders/[id]/export/route.ts b/apps/sim/app/api/v1/admin/folders/[id]/export/route.ts
index 6591ee39e19..ddec95e57d3 100644
--- a/apps/sim/app/api/v1/admin/folders/[id]/export/route.ts
+++ b/apps/sim/app/api/v1/admin/folders/[id]/export/route.ts
@@ -21,6 +21,7 @@ import { parseRequest } from '@/lib/api/server'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { exportFolderToZip, sanitizePathSegment } from '@/lib/workflows/operations/import-export'
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
+import { parseWorkflowVariables } from '@/lib/workflows/variables/parse'
import { encodeFilenameForHeader } from '@/app/api/files/utils'
import { withAdminAuthParams } from '@/app/api/v1/admin/middleware'
import {
@@ -28,11 +29,7 @@ import {
notFoundResponse,
singleResponse,
} from '@/app/api/v1/admin/responses'
-import {
- type FolderExportPayload,
- parseWorkflowVariables,
- type WorkflowExportState,
-} from '@/app/api/v1/admin/types'
+import type { FolderExportPayload, WorkflowExportState } from '@/app/api/v1/admin/types'
const logger = createLogger('AdminFolderExportAPI')
diff --git a/apps/sim/app/api/v1/admin/types.ts b/apps/sim/app/api/v1/admin/types.ts
index 4754de31054..77e5d7f6366 100644
--- a/apps/sim/app/api/v1/admin/types.ts
+++ b/apps/sim/app/api/v1/admin/types.ts
@@ -327,53 +327,6 @@ export interface WorkspaceImportResponse {
// Utility Functions
// =============================================================================
-/**
- * Parse workflow variables from database JSON format to Record format.
- * Handles both legacy Array and current Record formats.
- */
-export function parseWorkflowVariables(
- dbVariables: DbWorkflow['variables']
-): Record | undefined {
- if (!dbVariables) return undefined
-
- try {
- const varsObj = typeof dbVariables === 'string' ? JSON.parse(dbVariables) : dbVariables
-
- // Handle legacy Array format by converting to Record
- if (Array.isArray(varsObj)) {
- const result: Record = {}
- for (const v of varsObj) {
- result[v.id] = {
- id: v.id,
- name: v.name,
- type: v.type,
- value: v.value,
- }
- }
- return result
- }
-
- // Already Record format - normalize and return
- if (typeof varsObj === 'object' && varsObj !== null) {
- const result: Record = {}
- for (const [key, v] of Object.entries(varsObj)) {
- const variable = v as { id: string; name: string; type: VariableType; value: unknown }
- result[key] = {
- id: variable.id,
- name: variable.name,
- type: variable.type,
- value: variable.value,
- }
- }
- return result
- }
- } catch {
- // pass
- }
-
- return undefined
-}
-
/**
* Extract workflow metadata from various export formats.
* Handles both full export payload and raw state formats.
diff --git a/apps/sim/app/api/v1/admin/workflows/[id]/export/route.ts b/apps/sim/app/api/v1/admin/workflows/[id]/export/route.ts
index f5207ada0a7..cf321d1aeb3 100644
--- a/apps/sim/app/api/v1/admin/workflows/[id]/export/route.ts
+++ b/apps/sim/app/api/v1/admin/workflows/[id]/export/route.ts
@@ -14,17 +14,14 @@ import { adminV1ExportWorkflowContract } from '@/lib/api/contracts/v1/admin'
import { parseRequest } from '@/lib/api/server'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
+import { parseWorkflowVariables } from '@/lib/workflows/variables/parse'
import { withAdminAuthParams } from '@/app/api/v1/admin/middleware'
import {
internalErrorResponse,
notFoundResponse,
singleResponse,
} from '@/app/api/v1/admin/responses'
-import {
- parseWorkflowVariables,
- type WorkflowExportPayload,
- type WorkflowExportState,
-} from '@/app/api/v1/admin/types'
+import type { WorkflowExportPayload, WorkflowExportState } from '@/app/api/v1/admin/types'
const logger = createLogger('AdminWorkflowExportAPI')
diff --git a/apps/sim/app/api/v1/admin/workflows/export/route.ts b/apps/sim/app/api/v1/admin/workflows/export/route.ts
index f3f90f968cd..ed5779f52bf 100644
--- a/apps/sim/app/api/v1/admin/workflows/export/route.ts
+++ b/apps/sim/app/api/v1/admin/workflows/export/route.ts
@@ -26,17 +26,14 @@ import { parseRequest } from '@/lib/api/server'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { sanitizePathSegment } from '@/lib/workflows/operations/import-export'
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
+import { parseWorkflowVariables } from '@/lib/workflows/variables/parse'
import { withAdminAuth } from '@/app/api/v1/admin/middleware'
import {
badRequestResponse,
internalErrorResponse,
listResponse,
} from '@/app/api/v1/admin/responses'
-import {
- parseWorkflowVariables,
- type WorkflowExportPayload,
- type WorkflowExportState,
-} from '@/app/api/v1/admin/types'
+import type { WorkflowExportPayload, WorkflowExportState } from '@/app/api/v1/admin/types'
const logger = createLogger('AdminWorkflowsExportAPI')
diff --git a/apps/sim/app/api/v1/admin/workflows/import/route.ts b/apps/sim/app/api/v1/admin/workflows/import/route.ts
index b672f25b78f..502c2592b0e 100644
--- a/apps/sim/app/api/v1/admin/workflows/import/route.ts
+++ b/apps/sim/app/api/v1/admin/workflows/import/route.ts
@@ -24,20 +24,17 @@ import { adminV1ImportWorkflowContract } from '@/lib/api/contracts/v1/admin'
import { parseRequest } from '@/lib/api/server'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { parseWorkflowJson } from '@/lib/workflows/operations/import-export'
+import { prepareWorkflowStateForPersistence } from '@/lib/workflows/persistence/prepare-state'
import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils'
import { deduplicateWorkflowName } from '@/lib/workflows/utils'
+import { normalizeImportedVariables } from '@/lib/workflows/variables/parse'
import { withAdminAuth } from '@/app/api/v1/admin/middleware'
import {
badRequestResponse,
internalErrorResponse,
notFoundResponse,
} from '@/app/api/v1/admin/responses'
-import {
- extractWorkflowMetadata,
- type VariableType,
- type WorkflowImportRequest,
- type WorkflowVariable,
-} from '@/app/api/v1/admin/types'
+import { extractWorkflowMetadata, type WorkflowImportRequest } from '@/app/api/v1/admin/types'
const logger = createLogger('AdminWorkflowImportAPI')
@@ -110,49 +107,29 @@ export const POST = withRouteHandler(
variables: {},
})
- const saveResult = await saveWorkflowToNormalizedTables(workflowId, workflowData)
+ /**
+ * Same normalization the editor and the v1 import API run, via the one
+ * shared implementation — without it this route wrote raw parsed state,
+ * so a dangling edge tripped the `workflow_edges` foreign key and a block
+ * missing its backfilled columns could land unopenable.
+ */
+ const { state: preparedState, warnings } = prepareWorkflowStateForPersistence(workflowData)
+ if (warnings.length > 0) {
+ logger.warn('Admin API: normalized imported workflow with warnings', { warnings })
+ }
+
+ const saveResult = await saveWorkflowToNormalizedTables(workflowId, {
+ ...workflowData,
+ ...preparedState,
+ })
if (!saveResult.success) {
await db.delete(workflow).where(eq(workflow.id, workflowId))
return internalErrorResponse(`Failed to save workflow state: ${saveResult.error}`)
}
- if (
- workflowData.variables &&
- typeof workflowData.variables === 'object' &&
- !Array.isArray(workflowData.variables)
- ) {
- const variablesRecord: Record = {}
- const vars = workflowData.variables as Record<
- string,
- { id?: string; name: string; type?: VariableType; value: unknown }
- >
- Object.entries(vars).forEach(([key, v]) => {
- const varId = v.id || key
- variablesRecord[varId] = {
- id: varId,
- name: v.name,
- type: v.type ?? 'string',
- value: v.value,
- }
- })
-
- await db
- .update(workflow)
- .set({ variables: variablesRecord, updatedAt: new Date() })
- .where(eq(workflow.id, workflowId))
- } else if (workflowData.variables && Array.isArray(workflowData.variables)) {
- const variablesRecord: Record = {}
- workflowData.variables.forEach((v) => {
- const varId = v.id || generateId()
- variablesRecord[varId] = {
- id: varId,
- name: v.name,
- type: (v.type as VariableType) ?? 'string',
- value: v.value,
- }
- })
-
+ const variablesRecord = normalizeImportedVariables(workflowData.variables)
+ if (Object.keys(variablesRecord).length > 0) {
await db
.update(workflow)
.set({ variables: variablesRecord, updatedAt: new Date() })
diff --git a/apps/sim/app/api/v1/admin/workspaces/[id]/export/route.ts b/apps/sim/app/api/v1/admin/workspaces/[id]/export/route.ts
index e8913c74e0f..06204ab2898 100644
--- a/apps/sim/app/api/v1/admin/workspaces/[id]/export/route.ts
+++ b/apps/sim/app/api/v1/admin/workspaces/[id]/export/route.ts
@@ -22,6 +22,7 @@ import { parseRequest } from '@/lib/api/server'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { exportWorkspaceToZip, sanitizePathSegment } from '@/lib/workflows/operations/import-export'
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
+import { parseWorkflowVariables } from '@/lib/workflows/variables/parse'
import { encodeFilenameForHeader } from '@/app/api/files/utils'
import { withAdminAuthParams } from '@/app/api/v1/admin/middleware'
import {
@@ -29,11 +30,10 @@ import {
notFoundResponse,
singleResponse,
} from '@/app/api/v1/admin/responses'
-import {
- type FolderExportPayload,
- parseWorkflowVariables,
- type WorkflowExportState,
- type WorkspaceExportPayload,
+import type {
+ FolderExportPayload,
+ WorkflowExportState,
+ WorkspaceExportPayload,
} from '@/app/api/v1/admin/types'
const logger = createLogger('AdminWorkspaceExportAPI')
diff --git a/apps/sim/app/api/v1/admin/workspaces/[id]/import/route.ts b/apps/sim/app/api/v1/admin/workspaces/[id]/import/route.ts
index 7bbf9a5ea01..967e432ef39 100644
--- a/apps/sim/app/api/v1/admin/workspaces/[id]/import/route.ts
+++ b/apps/sim/app/api/v1/admin/workspaces/[id]/import/route.ts
@@ -41,8 +41,10 @@ import {
extractWorkflowsFromZip,
parseWorkflowJson,
} from '@/lib/workflows/operations/import-export'
+import { prepareWorkflowStateForPersistence } from '@/lib/workflows/persistence/prepare-state'
import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils'
import { deduplicateWorkflowName } from '@/lib/workflows/utils'
+import { normalizeImportedVariables } from '@/lib/workflows/variables/parse'
import { getWorkspaceWithOwner } from '@/lib/workspaces/permissions/utils'
import { withAdminAuthParams } from '@/app/api/v1/admin/middleware'
import {
@@ -52,7 +54,6 @@ import {
} from '@/app/api/v1/admin/responses'
import type {
ImportResult,
- WorkflowVariable,
WorkspaceImportRequest,
WorkspaceImportResponse,
} from '@/app/api/v1/admin/types'
@@ -270,7 +271,22 @@ async function importSingleWorkflow(
variables: {},
})
- const saveResult = await saveWorkflowToNormalizedTables(workflowId, workflowData)
+ /**
+ * Same normalization the editor, the v1 import API and the single-workflow
+ * admin import run, via the one shared implementation. Without it this route
+ * wrote raw parsed state, so a dangling edge tripped the `workflow_edges`
+ * foreign key and failed the restore, and blocks missing their backfilled
+ * columns could land unopenable.
+ */
+ const { state: preparedState, warnings } = prepareWorkflowStateForPersistence(workflowData)
+ if (warnings.length > 0) {
+ logger.warn(`Admin API: normalized "${dedupedName}" with warnings`, { warnings })
+ }
+
+ const saveResult = await saveWorkflowToNormalizedTables(workflowId, {
+ ...workflowData,
+ ...preparedState,
+ })
if (!saveResult.success) {
await db.delete(workflow).where(eq(workflow.id, workflowId))
@@ -282,18 +298,13 @@ async function importSingleWorkflow(
}
}
- if (workflowData.variables && Array.isArray(workflowData.variables)) {
- const variablesRecord: Record = {}
- workflowData.variables.forEach((v) => {
- const varId = v.id || generateId()
- variablesRecord[varId] = {
- id: varId,
- name: v.name,
- type: v.type || 'string',
- value: v.value,
- }
- })
-
+ /**
+ * Previously guarded on `Array.isArray`, which silently dropped every
+ * variable in the current record form — the exact shape this workspace's
+ * own export emits — so an export/import round trip lost all of them.
+ */
+ const variablesRecord = normalizeImportedVariables(workflowData.variables)
+ if (Object.keys(variablesRecord).length > 0) {
await db
.update(workflow)
.set({ variables: variablesRecord, updatedAt: new Date() })
diff --git a/apps/sim/app/api/v1/files/[fileId]/route.test.ts b/apps/sim/app/api/v1/files/[fileId]/route.test.ts
new file mode 100644
index 00000000000..9a85c408bb1
--- /dev/null
+++ b/apps/sim/app/api/v1/files/[fileId]/route.test.ts
@@ -0,0 +1,135 @@
+/**
+ * @vitest-environment node
+ */
+import { createMockRequest } from '@sim/testing'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+
+const {
+ mockCheckRateLimit,
+ mockValidateWorkspaceAccess,
+ mockGetWorkspaceFile,
+ mockFetchServableWorkspaceFileBuffer,
+} = vi.hoisted(() => ({
+ mockCheckRateLimit: vi.fn(),
+ mockValidateWorkspaceAccess: vi.fn(),
+ mockGetWorkspaceFile: vi.fn(),
+ mockFetchServableWorkspaceFileBuffer: vi.fn(),
+}))
+
+vi.mock('@/app/api/v1/middleware', () => ({
+ checkRateLimit: mockCheckRateLimit,
+ createRateLimitResponse: () => new Response('rate limited', { status: 429 }),
+ validateWorkspaceAccess: mockValidateWorkspaceAccess,
+}))
+vi.mock('@/lib/uploads/contexts/workspace', () => ({
+ getWorkspaceFile: mockGetWorkspaceFile,
+ fetchServableWorkspaceFileBuffer: mockFetchServableWorkspaceFileBuffer,
+}))
+vi.mock('@/lib/workspace-files/orchestration', () => ({
+ performDeleteWorkspaceFileItems: vi.fn(),
+}))
+vi.mock('@sim/audit', () => ({
+ recordAudit: vi.fn(),
+ AuditAction: { FILE_DOWNLOADED: 'file.downloaded', FILE_DELETED: 'file.deleted' },
+ AuditResourceType: { FILE: 'file' },
+}))
+vi.mock('@/lib/posthog/server', () => ({ captureServerEvent: vi.fn() }))
+
+import { DocCompileUserError } from '@/lib/copilot/tools/server/files/doc-compile'
+import { GET } from '@/app/api/v1/files/[fileId]/route'
+
+const WORKSPACE_ID = 'ws-1'
+const FILE_ID = 'file-1'
+const context = { params: Promise.resolve({ fileId: FILE_ID }) }
+
+const DOCX_MIME = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
+
+function request() {
+ return createMockRequest(
+ 'GET',
+ undefined,
+ {},
+ `http://localhost:3000/api/v1/files/${FILE_ID}?workspaceId=${WORKSPACE_ID}`
+ )
+}
+
+/** A generated document: the name carries the target extension, the type the source. */
+function generatedDocument(name = 'report.docx') {
+ return {
+ id: FILE_ID,
+ workspaceId: WORKSPACE_ID,
+ name,
+ key: `workspace/${WORKSPACE_ID}/${FILE_ID}`,
+ path: `/serve/${FILE_ID}`,
+ size: 6_242,
+ type: 'text/x-docxjs',
+ uploadedBy: 'user-1',
+ uploadedAt: new Date('2026-01-01'),
+ updatedAt: new Date('2026-01-01'),
+ }
+}
+
+describe('v1 file download', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ mockCheckRateLimit.mockResolvedValue({ allowed: true, userId: 'user-1' })
+ mockValidateWorkspaceAccess.mockResolvedValue(null)
+ mockGetWorkspaceFile.mockResolvedValue(generatedDocument())
+ mockFetchServableWorkspaceFileBuffer.mockResolvedValue({
+ buffer: Buffer.from('PKrendered'),
+ contentType: DOCX_MIME,
+ })
+ })
+
+ it('serves the rendered bytes and the rendered content type', async () => {
+ const response = await GET(request(), context)
+
+ expect(response.status).toBe(200)
+ // Not the record's `text/x-docxjs`, which describes the stored source.
+ expect(response.headers.get('Content-Type')).toBe(DOCX_MIME)
+ expect(Buffer.from(await response.arrayBuffer()).toString()).toContain('rendered')
+ })
+
+ it('names the download with an extension matching the served content type', async () => {
+ // The renderer picks its output format from the file name, so the two cannot
+ // disagree: a `.docx` renders to a docx. This pins that invariant.
+ const response = await GET(request(), context)
+
+ const disposition = response.headers.get('Content-Disposition') ?? ''
+ expect(disposition).toContain('report.docx')
+ expect(response.headers.get('Content-Type')).toBe(DOCX_MIME)
+ })
+
+ it('reports Content-Length from the rendered bytes, not the declared source size', async () => {
+ const rendered = Buffer.alloc(50_000)
+ mockFetchServableWorkspaceFileBuffer.mockResolvedValue({
+ buffer: rendered,
+ contentType: DOCX_MIME,
+ })
+
+ const response = await GET(request(), context)
+
+ expect(response.headers.get('Content-Length')).toBe(String(rendered.length))
+ })
+
+ it('returns a retryable 409 while the artifact is still compiling', async () => {
+ mockFetchServableWorkspaceFileBuffer.mockRejectedValue(
+ new DocCompileUserError('Document is still being generated')
+ )
+
+ const response = await GET(request(), context)
+
+ // A 500 would give the caller no reason to try again.
+ expect(response.status).toBe(409)
+ expect((await response.json()).error).toContain('still being generated')
+ })
+
+ it('404s a file that does not exist', async () => {
+ mockGetWorkspaceFile.mockResolvedValue(null)
+
+ const response = await GET(request(), context)
+
+ expect(response.status).toBe(404)
+ expect(mockFetchServableWorkspaceFileBuffer).not.toHaveBeenCalled()
+ })
+})
diff --git a/apps/sim/app/api/v1/files/[fileId]/route.ts b/apps/sim/app/api/v1/files/[fileId]/route.ts
index 258e36b9ffb..ee5c13c3509 100644
--- a/apps/sim/app/api/v1/files/[fileId]/route.ts
+++ b/apps/sim/app/api/v1/files/[fileId]/route.ts
@@ -6,7 +6,11 @@ import { parseRequest } from '@/lib/api/server'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { captureServerEvent } from '@/lib/posthog/server'
-import { fetchWorkspaceFileBuffer, getWorkspaceFile } from '@/lib/uploads/contexts/workspace'
+import {
+ fetchServableWorkspaceFileBuffer,
+ getWorkspaceFile,
+} from '@/lib/uploads/contexts/workspace'
+import { docNotReadyMessage, isDocNotReadyError } from '@/lib/uploads/utils/servable-file-response'
import { performDeleteWorkspaceFileItems } from '@/lib/workspace-files/orchestration'
import {
checkRateLimit,
@@ -48,7 +52,9 @@ export const GET = withRouteHandler(async (request: NextRequest, context: FileRo
return NextResponse.json({ error: 'File not found' }, { status: 404 })
}
- const buffer = await fetchWorkspaceFileBuffer(fileRecord)
+ // Generated docs store their generation source; serve the rendered artifact.
+ // Its content type is the rendered one, not the source MIME on the record.
+ const { buffer, contentType } = await fetchServableWorkspaceFileBuffer(fileRecord)
recordAudit({
workspaceId,
@@ -73,21 +79,30 @@ export const GET = withRouteHandler(async (request: NextRequest, context: FileRo
{ groups: { workspace: workspaceId } }
)
- return new Response(new Uint8Array(buffer), {
- status: 200,
- headers: {
- 'Content-Type': fileRecord.type || 'application/octet-stream',
- 'Content-Disposition': `attachment; filename="${fileRecord.name.replace(/[^\w.-]/g, '_')}"; filename*=UTF-8''${encodeURIComponent(fileRecord.name)}`,
- 'Content-Length': String(buffer.length),
- 'X-File-Id': fileRecord.id,
- 'X-File-Name': encodeURIComponent(fileRecord.name),
- 'X-Uploaded-At':
- fileRecord.uploadedAt instanceof Date
- ? fileRecord.uploadedAt.toISOString()
- : String(fileRecord.uploadedAt),
- },
- })
+ // View, not copy — a second full copy would double peak memory for a large file.
+ return new Response(
+ new Uint8Array(buffer.buffer as ArrayBuffer, buffer.byteOffset, buffer.byteLength),
+ {
+ status: 200,
+ headers: {
+ 'Content-Type': contentType || fileRecord.type || 'application/octet-stream',
+ 'Content-Disposition': `attachment; filename="${fileRecord.name.replace(/[^\w.-]/g, '_')}"; filename*=UTF-8''${encodeURIComponent(fileRecord.name)}`,
+ 'Content-Length': String(buffer.length),
+ 'X-File-Id': fileRecord.id,
+ 'X-File-Name': encodeURIComponent(fileRecord.name),
+ 'X-Uploaded-At':
+ fileRecord.uploadedAt instanceof Date
+ ? fileRecord.uploadedAt.toISOString()
+ : String(fileRecord.uploadedAt),
+ },
+ }
+ )
} catch (error) {
+ // A generated doc whose artifact is still compiling is retryable, not a fault:
+ // without this the caller sees a 500 and has no reason to try again.
+ if (isDocNotReadyError(error)) {
+ return NextResponse.json({ error: docNotReadyMessage() }, { status: 409 })
+ }
logger.error(`[${requestId}] Error downloading file:`, error)
return NextResponse.json({ error: 'Failed to download file' }, { status: 500 })
}
diff --git a/apps/sim/app/api/v1/middleware.ts b/apps/sim/app/api/v1/middleware.ts
index 61ec99a5abd..c9f757d91df 100644
--- a/apps/sim/app/api/v1/middleware.ts
+++ b/apps/sim/app/api/v1/middleware.ts
@@ -22,6 +22,8 @@ export type V1Endpoint =
| 'workflow-detail'
| 'workflow-deploy'
| 'workflow-rollback'
+ | 'workflow-export'
+ | 'workflow-import'
| 'audit-logs'
| 'tables'
| 'table-detail'
diff --git a/apps/sim/app/api/v1/workflows/[id]/export/route.test.ts b/apps/sim/app/api/v1/workflows/[id]/export/route.test.ts
new file mode 100644
index 00000000000..01097f8b22b
--- /dev/null
+++ b/apps/sim/app/api/v1/workflows/[id]/export/route.test.ts
@@ -0,0 +1,224 @@
+/**
+ * @vitest-environment node
+ *
+ * Tests for GET /api/v1/workflows/[id]/export — verifies auth, workspace
+ * permission enforcement (masked as 404), payload shape, secret sanitization,
+ * and edge-handle normalization.
+ */
+
+import { createMockRequest, workflowAuthzMockFns } from '@sim/testing'
+import { NextResponse } from 'next/server'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+
+const {
+ mockCheckRateLimit,
+ mockValidateWorkspaceAccess,
+ mockLoadWorkflowFromNormalizedTables,
+ mockRecordAudit,
+} = vi.hoisted(() => ({
+ mockCheckRateLimit: vi.fn(),
+ mockValidateWorkspaceAccess: vi.fn(),
+ mockLoadWorkflowFromNormalizedTables: vi.fn(),
+ mockRecordAudit: vi.fn(),
+}))
+
+vi.mock('@/app/api/v1/middleware', () => ({
+ checkRateLimit: mockCheckRateLimit,
+ createRateLimitResponse: vi.fn(() =>
+ NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+ ),
+ validateWorkspaceAccess: mockValidateWorkspaceAccess,
+}))
+
+vi.mock('@/lib/workflows/persistence/utils', () => ({
+ loadWorkflowFromNormalizedTables: mockLoadWorkflowFromNormalizedTables,
+}))
+
+vi.mock('@/app/api/v1/logs/meta', () => ({
+ getUserLimits: vi.fn().mockResolvedValue({}),
+ createApiResponse: vi.fn((body: unknown) => ({ body, headers: {} })),
+}))
+
+vi.mock('@sim/audit', () => ({
+ recordAudit: mockRecordAudit,
+ AuditAction: { WORKFLOW_EXPORTED: 'workflow.exported' },
+ AuditResourceType: { WORKFLOW: 'workflow' },
+}))
+
+/**
+ * Overrides the global registry mock (whose blocks declare no subBlocks) so
+ * `sanitizeForExport` has a `password: true` field to actually redact.
+ */
+vi.mock('@/blocks/registry', () => ({
+ getBlock: vi.fn(() => ({
+ name: 'Starter',
+ description: 'Mock block',
+ icon: () => null,
+ subBlocks: [
+ { id: 'apiKey', type: 'short-input', password: true },
+ { id: 'endpoint', type: 'short-input' },
+ { id: 'secretFromEnv', type: 'short-input', password: true },
+ ],
+ outputs: {},
+ })),
+ getAllBlocks: vi.fn(() => []),
+ getLatestBlock: vi.fn(() => undefined),
+ getBlockByToolName: vi.fn(() => undefined),
+}))
+
+import { GET } from '@/app/api/v1/workflows/[id]/export/route'
+
+const WORKFLOW_ID = 'wf-1'
+
+const WORKFLOW_RECORD = {
+ id: WORKFLOW_ID,
+ name: 'My Workflow',
+ description: 'Does a thing',
+ workspaceId: 'ws-1',
+ folderId: 'folder-1',
+ variables: {
+ 'var-1': { id: 'var-1', name: 'apiHost', type: 'string', value: 'https://example.com' },
+ },
+}
+
+const NORMALIZED_STATE = {
+ blocks: {
+ 'block-1': {
+ id: 'block-1',
+ type: 'starter',
+ name: 'Start',
+ position: { x: 0, y: 0 },
+ subBlocks: {
+ apiKey: { id: 'apiKey', type: 'short-input', value: 'sk-super-secret' },
+ endpoint: { id: 'endpoint', type: 'short-input', value: 'https://api.example.com' },
+ secretFromEnv: { id: 'secretFromEnv', type: 'short-input', value: '{{MY_SECRET}}' },
+ },
+ outputs: {},
+ enabled: true,
+ },
+ },
+ edges: [
+ {
+ id: 'edge-1',
+ source: 'block-1',
+ target: 'block-2',
+ sourceHandle: null,
+ targetHandle: 'target',
+ },
+ ],
+ loops: {},
+ parallels: {},
+ isFromNormalizedTables: true,
+}
+
+function makeContext(id = WORKFLOW_ID) {
+ return { params: Promise.resolve({ id }) }
+}
+
+function makeRequest() {
+ return createMockRequest(
+ 'GET',
+ undefined,
+ {},
+ `http://localhost:3000/api/v1/workflows/${WORKFLOW_ID}/export`
+ )
+}
+
+describe('GET /api/v1/workflows/[id]/export', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ mockCheckRateLimit.mockResolvedValue({ allowed: true, userId: 'user-1' })
+ mockValidateWorkspaceAccess.mockResolvedValue(null)
+ workflowAuthzMockFns.mockGetActiveWorkflowRecord.mockResolvedValue(WORKFLOW_RECORD)
+ mockLoadWorkflowFromNormalizedTables.mockResolvedValue(NORMALIZED_STATE)
+ })
+
+ it('returns 401 when the API key is rejected', async () => {
+ mockCheckRateLimit.mockResolvedValue({ allowed: false })
+
+ const response = await GET(makeRequest(), makeContext())
+
+ expect(response.status).toBe(401)
+ })
+
+ it('returns 404 when the workflow does not exist', async () => {
+ workflowAuthzMockFns.mockGetActiveWorkflowRecord.mockResolvedValue(null)
+
+ const response = await GET(makeRequest(), makeContext())
+
+ expect(response.status).toBe(404)
+ })
+
+ it('masks a permission failure as 404 so callers cannot probe existence', async () => {
+ mockValidateWorkspaceAccess.mockResolvedValue(
+ NextResponse.json({ error: 'Access denied' }, { status: 403 })
+ )
+
+ const response = await GET(makeRequest(), makeContext())
+
+ expect(response.status).toBe(404)
+ await expect(response.json()).resolves.toEqual({ error: 'Workflow not found' })
+ })
+
+ it('returns 404 when the workflow row exists but its state does not', async () => {
+ mockLoadWorkflowFromNormalizedTables.mockResolvedValue(null)
+
+ const response = await GET(makeRequest(), makeContext())
+
+ expect(response.status).toBe(404)
+ })
+
+ it('returns the export envelope with workflow metadata and state', async () => {
+ const response = await GET(makeRequest(), makeContext())
+ const body = await response.json()
+
+ expect(response.status).toBe(200)
+ expect(body.data.version).toBe('1.0')
+ expect(typeof body.data.exportedAt).toBe('string')
+ expect(body.data.workflow).toEqual({
+ id: WORKFLOW_ID,
+ name: 'My Workflow',
+ description: 'Does a thing',
+ workspaceId: 'ws-1',
+ folderId: 'folder-1',
+ })
+ expect(body.data.state.metadata).toMatchObject({
+ name: 'My Workflow',
+ description: 'Does a thing',
+ })
+ expect(Object.keys(body.data.state.blocks)).toEqual(['block-1'])
+ })
+
+ it('strips secret sub-block values but preserves env-var references', async () => {
+ const response = await GET(makeRequest(), makeContext())
+ const body = await response.json()
+
+ const subBlocks = body.data.state.blocks['block-1'].subBlocks
+ expect(JSON.stringify(body)).not.toContain('sk-super-secret')
+ expect(subBlocks.apiKey.value).toBeNull()
+ expect(subBlocks.endpoint.value).toBe('https://api.example.com')
+ expect(subBlocks.secretFromEnv.value).toBe('{{MY_SECRET}}')
+ })
+
+ it('normalizes null edge handles to omitted values', async () => {
+ const response = await GET(makeRequest(), makeContext())
+ const body = await response.json()
+
+ const [edge] = body.data.state.edges
+ expect(edge.sourceHandle).toBeUndefined()
+ expect(edge.targetHandle).toBe('target')
+ })
+
+ it('records a workflow-exported audit entry', async () => {
+ await GET(makeRequest(), makeContext())
+
+ expect(mockRecordAudit).toHaveBeenCalledWith(
+ expect.objectContaining({
+ action: 'workflow.exported',
+ resourceId: WORKFLOW_ID,
+ workspaceId: 'ws-1',
+ actorId: 'user-1',
+ })
+ )
+ })
+})
diff --git a/apps/sim/app/api/v1/workflows/[id]/export/route.ts b/apps/sim/app/api/v1/workflows/[id]/export/route.ts
new file mode 100644
index 00000000000..b445e81a3f1
--- /dev/null
+++ b/apps/sim/app/api/v1/workflows/[id]/export/route.ts
@@ -0,0 +1,191 @@
+import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
+import { createLogger } from '@sim/logger'
+import { getActiveWorkflowRecord } from '@sim/platform-authz/workflow'
+import { getErrorMessage } from '@sim/utils/errors'
+import { generateId } from '@sim/utils/id'
+import { type NextRequest, NextResponse } from 'next/server'
+import type { Edge } from 'reactflow'
+import {
+ type V1WorkflowExportPayload,
+ v1ExportWorkflowContract,
+} from '@/lib/api/contracts/v1/workflows'
+import { parseRequest } from '@/lib/api/server'
+import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
+import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
+import { sanitizeForExport } from '@/lib/workflows/sanitization/json-sanitizer'
+import { parseWorkflowVariables } from '@/lib/workflows/variables/parse'
+import { createApiResponse, getUserLimits } from '@/app/api/v1/logs/meta'
+import {
+ checkRateLimit,
+ createRateLimitResponse,
+ validateWorkspaceAccess,
+} from '@/app/api/v1/middleware'
+
+const logger = createLogger('V1WorkflowExportAPI')
+
+export const dynamic = 'force-dynamic'
+export const revalidate = 0
+
+type ExportedEdge = V1WorkflowExportPayload['state']['edges'][number]
+
+/**
+ * Projects a persisted ReactFlow edge onto the wire shape declared by the
+ * response contract. Field-by-field rather than a spread because ReactFlow's
+ * `Edge` is looser than the contract in three places: handles are `null` when
+ * unset (the contract and the importer both model absent as `undefined`),
+ * `label` is a `ReactNode`, and the marker fields accept an `EdgeMarker`
+ * object. Non-serializable values in those slots are dropped rather than
+ * emitted as `{}`.
+ */
+function toExportedEdge(edge: Edge): ExportedEdge {
+ return {
+ id: edge.id,
+ source: edge.source,
+ target: edge.target,
+ sourceHandle: edge.sourceHandle ?? undefined,
+ targetHandle: edge.targetHandle ?? undefined,
+ type: edge.type,
+ animated: edge.animated,
+ style: edge.style as Record | undefined,
+ data: edge.data,
+ label: typeof edge.label === 'string' ? edge.label : undefined,
+ labelStyle: edge.labelStyle as Record | undefined,
+ labelShowBg: edge.labelShowBg,
+ labelBgStyle: edge.labelBgStyle as Record | undefined,
+ labelBgPadding: edge.labelBgPadding,
+ labelBgBorderRadius: edge.labelBgBorderRadius,
+ markerStart: typeof edge.markerStart === 'string' ? edge.markerStart : undefined,
+ markerEnd: typeof edge.markerEnd === 'string' ? edge.markerEnd : undefined,
+ }
+}
+
+/**
+ * GET /api/v1/workflows/[id]/export
+ *
+ * Exports a workflow as a portable JSON envelope that
+ * `POST /api/v1/workflows/import` accepts verbatim.
+ *
+ * Unlike the admin export (`/api/v1/admin/workflows/[id]/export`), which emits
+ * the raw state for backup/restore, this surface runs the payload through
+ * `sanitizeForExport`, which nulls three classes of sub-block value:
+ * - `password: true` fields, unless the value is a whole `{{ENV_VAR}}`
+ * reference, which is preserved so the import resolves it in the target
+ * workspace;
+ * - `oauth-input` credentials;
+ * - **workspace-scoped bindings** — `knowledge-base-selector`, `file-selector`,
+ * `channel-selector`, `project-selector`, `folder-selector`,
+ * `mcp-server-selector` and friends, plus fields keyed `knowledgeBaseId`,
+ * `fileId`, `channelId`, `projectId`, `documentId`, `tagFilters`. These point
+ * at rows that do not exist in another workspace, so they are cleared rather
+ * than carried across as dangling ids.
+ *
+ * The last class means an export is **not** a byte-for-byte clone even when
+ * re-imported into the same workspace: those bindings come back empty and must
+ * be re-selected. This matches the in-app export and is documented on the
+ * public endpoint so callers do not expect otherwise.
+ *
+ * Workflow **variables** are emitted as stored, matching `GET
+ * /api/v1/workflows/[id]` and the in-app export. Variables are plaintext
+ * workflow configuration readable by anyone with workspace read (the same
+ * permission this route requires); secrets belong in environment variables,
+ * which travel as unresolved `{{ENV_VAR}}` references. Redacting them here
+ * would break import round-trips without narrowing access.
+ */
+export const GET = withRouteHandler(
+ async (request: NextRequest, context: { params: Promise<{ id: string }> }) => {
+ const requestId = generateId().slice(0, 8)
+
+ try {
+ const rateLimit = await checkRateLimit(request, 'workflow-export')
+ if (!rateLimit.allowed) {
+ return createRateLimitResponse(rateLimit)
+ }
+
+ const userId = rateLimit.userId!
+ const parsed = await parseRequest(v1ExportWorkflowContract, request, context, {
+ validationErrorResponse: () =>
+ NextResponse.json({ error: 'Invalid workflow ID' }, { status: 400 }),
+ })
+ if (!parsed.success) return parsed.response
+
+ const { id } = parsed.data.params
+
+ logger.info(`[${requestId}] Exporting workflow ${id}`, { userId })
+
+ const workflowData = await getActiveWorkflowRecord(id)
+ if (!workflowData?.workspaceId) {
+ return NextResponse.json({ error: 'Workflow not found' }, { status: 404 })
+ }
+
+ const accessError = await validateWorkspaceAccess(rateLimit, userId, workflowData.workspaceId)
+ if (accessError) {
+ return NextResponse.json({ error: 'Workflow not found' }, { status: 404 })
+ }
+
+ const normalizedData = await loadWorkflowFromNormalizedTables(id)
+ if (!normalizedData) {
+ return NextResponse.json({ error: 'Workflow state not found' }, { status: 404 })
+ }
+
+ const sanitized = sanitizeForExport({
+ blocks: normalizedData.blocks,
+ edges: normalizedData.edges,
+ loops: normalizedData.loops,
+ parallels: normalizedData.parallels,
+ metadata: {
+ name: workflowData.name,
+ description: workflowData.description ?? undefined,
+ },
+ variables: parseWorkflowVariables(workflowData.variables),
+ })
+
+ const payload: V1WorkflowExportPayload = {
+ version: '1.0',
+ exportedAt: sanitized.exportedAt,
+ workflow: {
+ id: workflowData.id,
+ name: workflowData.name,
+ description: workflowData.description,
+ workspaceId: workflowData.workspaceId,
+ folderId: workflowData.folderId,
+ },
+ state: {
+ ...sanitized.state,
+ edges: sanitized.state.edges.map(toExportedEdge),
+ metadata: {
+ ...sanitized.state.metadata,
+ name: workflowData.name,
+ description: workflowData.description ?? undefined,
+ exportedAt: sanitized.exportedAt,
+ },
+ },
+ }
+
+ recordAudit({
+ workspaceId: workflowData.workspaceId,
+ actorId: userId,
+ action: AuditAction.WORKFLOW_EXPORTED,
+ resourceType: AuditResourceType.WORKFLOW,
+ resourceId: workflowData.id,
+ resourceName: workflowData.name,
+ description: `Exported workflow "${workflowData.name}" via the API`,
+ metadata: {
+ workspaceId: workflowData.workspaceId,
+ folderId: workflowData.folderId || undefined,
+ blocksCount: Object.keys(payload.state.blocks).length,
+ edgesCount: payload.state.edges.length,
+ },
+ request,
+ })
+
+ const limits = await getUserLimits(userId)
+ const apiResponse = createApiResponse({ data: payload }, limits, rateLimit)
+
+ return NextResponse.json(apiResponse.body, { headers: apiResponse.headers })
+ } catch (error: unknown) {
+ const message = getErrorMessage(error, 'Unknown error')
+ logger.error(`[${requestId}] Workflow export error`, { error: message })
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
+ }
+ }
+)
diff --git a/apps/sim/app/api/v1/workflows/import/route.test.ts b/apps/sim/app/api/v1/workflows/import/route.test.ts
new file mode 100644
index 00000000000..f35a471f802
--- /dev/null
+++ b/apps/sim/app/api/v1/workflows/import/route.test.ts
@@ -0,0 +1,476 @@
+/**
+ * @vitest-environment node
+ *
+ * Tests for POST /api/v1/workflows/import — verifies auth, write-permission
+ * enforcement, payload-shape tolerance (export envelope / bare state / JSON
+ * string), metadata resolution, variable persistence, and rollback when
+ * persisting the imported state fails.
+ */
+
+import { createMockRequest } from '@sim/testing'
+import { NextResponse } from 'next/server'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+
+const {
+ mockCheckRateLimit,
+ mockValidateWorkspaceAccess,
+ mockPerformCreateWorkflow,
+ mockSaveWorkflowToNormalizedTables,
+ mockParseWorkflowJson,
+ mockAssertFolderMutable,
+ mockAssertFolderInWorkspace,
+ mockExtractAndPersistCustomTools,
+ mockPrepareWorkflowState,
+ mockDbDelete,
+ mockDbUpdate,
+ mockWorkspaceRows,
+} = vi.hoisted(() => ({
+ mockCheckRateLimit: vi.fn(),
+ mockValidateWorkspaceAccess: vi.fn(),
+ mockPerformCreateWorkflow: vi.fn(),
+ mockSaveWorkflowToNormalizedTables: vi.fn(),
+ mockParseWorkflowJson: vi.fn(),
+ mockAssertFolderMutable: vi.fn(),
+ mockAssertFolderInWorkspace: vi.fn(),
+ mockExtractAndPersistCustomTools: vi.fn(),
+ mockPrepareWorkflowState: vi.fn(),
+ mockDbDelete: vi.fn(),
+ mockDbUpdate: vi.fn(),
+ mockWorkspaceRows: { value: [{ id: 'ws-1' }] as Array<{ id: string }> },
+}))
+
+vi.mock('@/app/api/v1/middleware', () => ({
+ checkRateLimit: mockCheckRateLimit,
+ createRateLimitResponse: vi.fn(() =>
+ NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+ ),
+ validateWorkspaceAccess: mockValidateWorkspaceAccess,
+}))
+
+vi.mock('@/lib/workflows/orchestration', () => ({
+ performCreateWorkflow: mockPerformCreateWorkflow,
+}))
+
+vi.mock('@/lib/workflows/persistence/utils', () => ({
+ saveWorkflowToNormalizedTables: mockSaveWorkflowToNormalizedTables,
+}))
+
+vi.mock('@/lib/workflows/operations/import-export', () => ({
+ parseWorkflowJson: mockParseWorkflowJson,
+}))
+
+vi.mock('@/app/api/v1/logs/meta', () => ({
+ getUserLimits: vi.fn().mockResolvedValue({}),
+ createApiResponse: vi.fn((body: unknown) => ({ body, headers: {} })),
+}))
+
+vi.mock('@sim/platform-authz/workflow', () => ({
+ assertFolderInWorkspace: mockAssertFolderInWorkspace,
+ assertFolderMutable: mockAssertFolderMutable,
+ FolderLockedError: class FolderLockedError extends Error {
+ status = 423
+ },
+ FolderNotFoundError: class FolderNotFoundError extends Error {
+ status = 400
+ },
+}))
+
+vi.mock('@/lib/workflows/persistence/custom-tools-persistence', () => ({
+ extractAndPersistCustomTools: mockExtractAndPersistCustomTools,
+}))
+
+/**
+ * Mocked to keep the block registry (and its icon/CSS graph) out of this
+ * suite. The real normalization is covered by `prepare-state.test.ts` and by
+ * the end-to-end round trip in `import-export-roundtrip.test.ts`.
+ */
+vi.mock('@/lib/workflows/persistence/prepare-state', () => ({
+ prepareWorkflowStateForPersistence: mockPrepareWorkflowState,
+}))
+
+vi.mock('@sim/db', () => ({
+ db: {
+ select: vi.fn(() => ({
+ from: vi.fn(() => ({
+ where: vi.fn(() => ({
+ limit: vi.fn(async () => mockWorkspaceRows.value),
+ })),
+ })),
+ })),
+ delete: mockDbDelete,
+ transaction: vi.fn(async (fn: (tx: unknown) => Promise) => fn({ update: mockDbUpdate })),
+ },
+}))
+
+import { POST } from '@/app/api/v1/workflows/import/route'
+
+const WORKSPACE_ID = 'ws-1'
+const CREATED_AT = new Date('2026-07-01T00:00:00Z')
+
+/** A schema-valid block: the route now gates on `workflowStateSchema`. */
+const VALID_BLOCK = {
+ id: 'block-1',
+ type: 'starter',
+ name: 'Start',
+ position: { x: 0, y: 0 },
+ subBlocks: {},
+ outputs: {},
+ enabled: true,
+}
+
+const PARSED_STATE = {
+ blocks: { 'block-1': VALID_BLOCK },
+ edges: [],
+ loops: {},
+ parallels: {},
+ variables: undefined as unknown,
+}
+
+const EXPORT_ENVELOPE = {
+ version: '1.0',
+ exportedAt: '2026-07-01T00:00:00.000Z',
+ workflow: {
+ id: 'wf-source',
+ name: 'Payload Name',
+ description: 'Payload description',
+ workspaceId: 'ws-other',
+ folderId: null,
+ },
+ state: { blocks: PARSED_STATE.blocks, edges: [], loops: {}, parallels: {} },
+}
+
+function makeRequest(body: unknown) {
+ return createMockRequest('POST', body, {}, 'http://localhost:3000/api/v1/workflows/import')
+}
+
+function validBody(overrides: Record = {}) {
+ return { workspaceId: WORKSPACE_ID, workflow: EXPORT_ENVELOPE, ...overrides }
+}
+
+describe('POST /api/v1/workflows/import', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ mockWorkspaceRows.value = [{ id: WORKSPACE_ID }]
+ mockCheckRateLimit.mockResolvedValue({ allowed: true, userId: 'user-1' })
+ mockValidateWorkspaceAccess.mockResolvedValue(null)
+ mockAssertFolderMutable.mockResolvedValue(undefined)
+ mockAssertFolderInWorkspace.mockResolvedValue(undefined)
+ mockExtractAndPersistCustomTools.mockResolvedValue({ saved: 0, errors: [] })
+ mockPrepareWorkflowState.mockImplementation((state: { blocks: unknown; edges: unknown }) => ({
+ state: { blocks: state.blocks, edges: state.edges, loops: {}, parallels: {} },
+ warnings: [],
+ }))
+ mockParseWorkflowJson.mockReturnValue({ data: { ...PARSED_STATE }, errors: [] })
+ mockSaveWorkflowToNormalizedTables.mockResolvedValue({ success: true })
+ mockPerformCreateWorkflow.mockResolvedValue({
+ success: true,
+ workflow: {
+ id: 'wf-new',
+ name: 'Payload Name',
+ description: 'Payload description',
+ workspaceId: WORKSPACE_ID,
+ folderId: null,
+ createdAt: CREATED_AT,
+ updatedAt: CREATED_AT,
+ },
+ })
+ mockDbDelete.mockReturnValue({ where: vi.fn().mockResolvedValue(undefined) })
+ mockDbUpdate.mockReturnValue({
+ set: vi.fn(() => ({ where: vi.fn().mockResolvedValue(undefined) })),
+ })
+ })
+
+ it('returns 401 when the API key is rejected', async () => {
+ mockCheckRateLimit.mockResolvedValue({ allowed: false })
+
+ const response = await POST(makeRequest(validBody()))
+
+ expect(response.status).toBe(401)
+ })
+
+ it('rejects a body without workspaceId', async () => {
+ const response = await POST(makeRequest({ workflow: EXPORT_ENVELOPE }))
+
+ expect(response.status).toBe(400)
+ })
+
+ it('rejects a body without a workflow payload', async () => {
+ const response = await POST(makeRequest({ workspaceId: WORKSPACE_ID }))
+
+ expect(response.status).toBe(400)
+ })
+
+ it('requires write permission on the target workspace', async () => {
+ mockValidateWorkspaceAccess.mockResolvedValue(
+ NextResponse.json({ error: 'Access denied' }, { status: 403 })
+ )
+
+ const response = await POST(makeRequest(validBody()))
+
+ expect(response.status).toBe(403)
+ expect(mockValidateWorkspaceAccess).toHaveBeenCalledWith(
+ expect.anything(),
+ 'user-1',
+ WORKSPACE_ID,
+ 'write'
+ )
+ expect(mockPerformCreateWorkflow).not.toHaveBeenCalled()
+ })
+
+ it('authorizes before touching the payload', async () => {
+ mockValidateWorkspaceAccess.mockResolvedValue(
+ NextResponse.json({ error: 'Access denied' }, { status: 403 })
+ )
+
+ await POST(makeRequest(validBody()))
+
+ expect(mockParseWorkflowJson).not.toHaveBeenCalled()
+ })
+
+ it('returns 404 when the target workspace does not exist', async () => {
+ mockWorkspaceRows.value = []
+
+ const response = await POST(makeRequest(validBody()))
+
+ expect(response.status).toBe(404)
+ })
+
+ it('returns 400 with parser errors for an invalid workflow payload', async () => {
+ mockParseWorkflowJson.mockReturnValue({
+ data: null,
+ errors: ['Missing or invalid field: blocks'],
+ })
+
+ const response = await POST(makeRequest(validBody()))
+
+ expect(response.status).toBe(400)
+ await expect(response.json()).resolves.toEqual({
+ error: 'Invalid workflow: Missing or invalid field: blocks',
+ })
+ })
+
+ it('creates the workflow and returns 201 with the resource shape', async () => {
+ const response = await POST(makeRequest(validBody()))
+ const body = await response.json()
+
+ expect(response.status).toBe(201)
+ expect(body.data).toEqual({
+ id: 'wf-new',
+ name: 'Payload Name',
+ description: 'Payload description',
+ workspaceId: WORKSPACE_ID,
+ folderId: null,
+ createdAt: CREATED_AT.toISOString(),
+ updatedAt: CREATED_AT.toISOString(),
+ })
+ expect(mockSaveWorkflowToNormalizedTables).toHaveBeenCalledWith(
+ 'wf-new',
+ expect.anything(),
+ expect.anything()
+ )
+ })
+
+ it('derives the name from the export envelope and deduplicates it', async () => {
+ await POST(makeRequest(validBody()))
+
+ expect(mockPerformCreateWorkflow).toHaveBeenCalledWith(
+ expect.objectContaining({
+ name: 'Payload Name',
+ description: 'Payload description',
+ workspaceId: WORKSPACE_ID,
+ deduplicate: true,
+ userId: 'user-1',
+ })
+ )
+ })
+
+ it('prefers the explicit name and description overrides', async () => {
+ await POST(makeRequest(validBody({ name: 'Override', description: 'Override desc' })))
+
+ expect(mockPerformCreateWorkflow).toHaveBeenCalledWith(
+ expect.objectContaining({ name: 'Override', description: 'Override desc' })
+ )
+ })
+
+ it('falls back to a default name when the payload carries no metadata', async () => {
+ await POST(
+ makeRequest({
+ workspaceId: WORKSPACE_ID,
+ workflow: { blocks: PARSED_STATE.blocks, edges: [] },
+ })
+ )
+
+ expect(mockPerformCreateWorkflow).toHaveBeenCalledWith(
+ expect.objectContaining({ name: 'Imported Workflow', description: '' })
+ )
+ })
+
+ it('accepts a JSON string payload', async () => {
+ const response = await POST(
+ makeRequest(validBody({ workflow: JSON.stringify(EXPORT_ENVELOPE) }))
+ )
+
+ expect(response.status).toBe(201)
+ expect(mockParseWorkflowJson).toHaveBeenCalledWith(JSON.stringify(EXPORT_ENVELOPE))
+ expect(mockPerformCreateWorkflow).toHaveBeenCalledWith(
+ expect.objectContaining({ name: 'Payload Name' })
+ )
+ })
+
+ it('persists variables from the parsed state', async () => {
+ mockParseWorkflowJson.mockReturnValue({
+ data: {
+ ...PARSED_STATE,
+ variables: { 'var-1': { id: 'var-1', name: 'host', type: 'string', value: 'x' } },
+ },
+ errors: [],
+ })
+
+ await POST(makeRequest(validBody()))
+
+ expect(mockDbUpdate).toHaveBeenCalled()
+ })
+
+ it('normalizes legacy array-form variables into a keyed record', async () => {
+ const setSpy = vi.fn(() => ({ where: vi.fn().mockResolvedValue(undefined) }))
+ mockDbUpdate.mockReturnValue({ set: setSpy })
+ mockParseWorkflowJson.mockReturnValue({
+ data: {
+ ...PARSED_STATE,
+ variables: [{ id: 'var-1', name: 'host', type: 'string', value: 'x' }],
+ },
+ errors: [],
+ })
+
+ await POST(makeRequest(validBody()))
+
+ expect(setSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ variables: { 'var-1': { id: 'var-1', name: 'host', type: 'string', value: 'x' } },
+ })
+ )
+ })
+
+ it('skips the variables write when the payload has none', async () => {
+ await POST(makeRequest(validBody()))
+
+ expect(mockDbUpdate).not.toHaveBeenCalled()
+ })
+
+ it('writes the graph and variables inside a single transaction', async () => {
+ mockParseWorkflowJson.mockReturnValue({
+ data: {
+ ...PARSED_STATE,
+ variables: { 'var-1': { id: 'var-1', name: 'host', type: 'string', value: 'x' } },
+ },
+ errors: [],
+ })
+
+ await POST(makeRequest(validBody()))
+
+ const tx = { update: mockDbUpdate }
+ expect(mockSaveWorkflowToNormalizedTables).toHaveBeenCalledWith('wf-new', expect.anything(), tx)
+ expect(mockDbUpdate).toHaveBeenCalled()
+ })
+
+ it('deletes the created row and returns 500 when persisting state fails', async () => {
+ const whereSpy = vi.fn().mockResolvedValue(undefined)
+ mockDbDelete.mockReturnValue({ where: whereSpy })
+ mockSaveWorkflowToNormalizedTables.mockResolvedValue({ success: false, error: 'boom' })
+
+ const response = await POST(makeRequest(validBody()))
+
+ expect(response.status).toBe(500)
+ expect(mockDbDelete).toHaveBeenCalled()
+ expect(whereSpy).toHaveBeenCalled()
+ })
+
+ it('rolls back the created workflow when the variables write throws', async () => {
+ const whereSpy = vi.fn().mockResolvedValue(undefined)
+ mockDbDelete.mockReturnValue({ where: whereSpy })
+ mockDbUpdate.mockImplementation(() => {
+ throw new Error('variables write failed')
+ })
+ mockParseWorkflowJson.mockReturnValue({
+ data: {
+ ...PARSED_STATE,
+ variables: { 'var-1': { id: 'var-1', name: 'host', type: 'string', value: 'x' } },
+ },
+ errors: [],
+ })
+
+ const response = await POST(makeRequest(validBody()))
+
+ expect(response.status).toBe(500)
+ expect(mockDbDelete).toHaveBeenCalled()
+ expect(whereSpy).toHaveBeenCalled()
+ })
+
+ it('caps a payload-derived name at the declared bound, ellipsis included', async () => {
+ await POST(
+ makeRequest(
+ validBody({
+ workflow: { ...EXPORT_ENVELOPE, workflow: { name: 'N'.repeat(500) } },
+ })
+ )
+ )
+
+ const { name } = mockPerformCreateWorkflow.mock.calls[0][0]
+ expect(name.length).toBeLessThanOrEqual(200)
+ expect(name.endsWith('...')).toBe(true)
+ })
+
+ it('prefers state.metadata.name, matching the in-app importer', async () => {
+ await POST(
+ makeRequest(
+ validBody({
+ workflow: {
+ workflow: { name: 'FromWorkflow' },
+ state: { ...EXPORT_ENVELOPE.state, metadata: { name: 'FromStateMetadata' } },
+ },
+ })
+ )
+ )
+
+ expect(mockPerformCreateWorkflow).toHaveBeenCalledWith(
+ expect.objectContaining({ name: 'FromStateMetadata' })
+ )
+ })
+
+ it('unwraps an export response envelope so its metadata still resolves', async () => {
+ await POST(makeRequest(validBody({ workflow: { data: EXPORT_ENVELOPE, limits: {} } })))
+
+ expect(mockPerformCreateWorkflow).toHaveBeenCalledWith(
+ expect.objectContaining({ name: 'Payload Name', description: 'Payload description' })
+ )
+ })
+
+ /**
+ * Unreachable in practice — the route always passes `deduplicate: true`, so
+ * the orchestrator renames instead of conflicting. Covers the defensive
+ * mapping of the orchestrator's documented result union.
+ */
+ it('maps a conflict result from the orchestrator to 409', async () => {
+ mockPerformCreateWorkflow.mockResolvedValue({
+ success: false,
+ error: 'A workflow named "Payload Name" already exists in this folder',
+ errorCode: 'conflict',
+ })
+
+ const response = await POST(makeRequest(validBody()))
+
+ expect(response.status).toBe(409)
+ })
+
+ it('maps an invalid target folder from the orchestrator to 400', async () => {
+ mockPerformCreateWorkflow.mockResolvedValue({
+ success: false,
+ error: 'Target folder not found',
+ errorCode: 'validation',
+ })
+
+ const response = await POST(makeRequest(validBody({ folderId: 'folder-x' })))
+
+ expect(response.status).toBe(400)
+ })
+})
diff --git a/apps/sim/app/api/v1/workflows/import/route.ts b/apps/sim/app/api/v1/workflows/import/route.ts
new file mode 100644
index 00000000000..037377ed063
--- /dev/null
+++ b/apps/sim/app/api/v1/workflows/import/route.ts
@@ -0,0 +1,388 @@
+import { db } from '@sim/db'
+import { workflow, workspace } from '@sim/db/schema'
+import { createLogger } from '@sim/logger'
+import {
+ assertFolderInWorkspace,
+ assertFolderMutable,
+ FolderLockedError,
+ FolderNotFoundError,
+} from '@sim/platform-authz/workflow'
+import { getErrorMessage } from '@sim/utils/errors'
+import { generateId } from '@sim/utils/id'
+import { truncate } from '@sim/utils/string'
+import { and, eq, isNull } from 'drizzle-orm'
+import { type NextRequest, NextResponse } from 'next/server'
+import {
+ V1_IMPORT_DESCRIPTION_MAX_LENGTH,
+ V1_IMPORT_NAME_MAX_LENGTH,
+ type V1ImportWorkflowData,
+ v1ImportWorkflowContract,
+} from '@/lib/api/contracts/v1/workflows'
+import { workflowStateSchema } from '@/lib/api/contracts/workflows'
+import { getValidationErrorMessage, parseRequest, serializeZodIssues } from '@/lib/api/server'
+import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
+import { parseWorkflowJson } from '@/lib/workflows/operations/import-export'
+import { performCreateWorkflow } from '@/lib/workflows/orchestration'
+import { extractAndPersistCustomTools } from '@/lib/workflows/persistence/custom-tools-persistence'
+import { prepareWorkflowStateForPersistence } from '@/lib/workflows/persistence/prepare-state'
+import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils'
+import { normalizeImportedVariables } from '@/lib/workflows/variables/parse'
+import { createApiResponse, getUserLimits } from '@/app/api/v1/logs/meta'
+import {
+ checkRateLimit,
+ createRateLimitResponse,
+ validateWorkspaceAccess,
+} from '@/app/api/v1/middleware'
+import type { WorkflowState } from '@/stores/workflows/workflow/types'
+
+const logger = createLogger('V1WorkflowImportAPI')
+
+export const dynamic = 'force-dynamic'
+export const revalidate = 0
+
+/**
+ * Workflow JSON is a bounded document — a few hundred blocks at the outside.
+ * Capping well below the platform-wide `DEFAULT_MAX_JSON_BODY_BYTES` (50 MB)
+ * keeps a hostile caller from buffering a large body before validation runs.
+ */
+const MAX_IMPORT_BODY_BYTES = 10 * 1024 * 1024
+
+const DEFAULT_IMPORTED_WORKFLOW_NAME = 'Imported Workflow'
+
+const TRUNCATION_SUFFIX = '...'
+
+/**
+ * Caps a payload-derived string at `maxLength` *including* the ellipsis.
+ * `truncate` appends its suffix after slicing, so passing the limit straight
+ * through would yield `maxLength + 3` characters and overshoot the very bound
+ * this is enforcing.
+ */
+function capLength(value: string, maxLength: number): string {
+ return truncate(value, maxLength - TRUNCATION_SUFFIX.length, TRUNCATION_SUFFIX)
+}
+
+/**
+ * Reads a dot-delimited path off a parsed payload and returns it only when it
+ * is a non-empty string, so blank metadata falls through to the next candidate.
+ */
+function readString(source: unknown, path: string): string | undefined {
+ let current: unknown = source
+ for (const segment of path.split('.')) {
+ if (!current || typeof current !== 'object') return undefined
+ current = (current as Record)[segment]
+ }
+ return typeof current === 'string' && current.trim() ? current.trim() : undefined
+}
+
+/**
+ * Unwraps the `{ data: ... }` response envelope the export endpoint returns, so
+ * a caller can pipe an export response body straight into import.
+ * `parseWorkflowJson` already tolerates this shape when reading the graph;
+ * mirroring it here keeps metadata resolution from silently falling back to the
+ * default name for the same payload.
+ */
+function unwrapResponseEnvelope(payload: unknown): unknown {
+ if (!payload || typeof payload !== 'object') return payload
+ const inner = (payload as Record).data
+ if (!inner || typeof inner !== 'object') return payload
+ const candidate = inner as Record
+ return candidate.state || candidate.version || candidate.workflow ? candidate : payload
+}
+
+/**
+ * Resolves the imported workflow's name and description, preferring explicit
+ * request overrides and then the payload's own metadata. Accepts every shape
+ * the importer takes: the export envelope (`workflow.*`, `state.metadata.*`)
+ * and a bare state (`metadata.*`).
+ *
+ * Candidate order deliberately matches `extractWorkflowName` — the resolver the
+ * in-app importer has always used — so the same payload yields the same name on
+ * both surfaces. The in-app version additionally falls back to the uploaded
+ * filename, which has no analogue here; that is the only intended difference.
+ *
+ * Payload-derived values are capped at the same bounds the contract applies to
+ * the explicit overrides, otherwise the declared `maxLength` would not be the
+ * effective one — a caller could store an unbounded name simply by embedding it
+ * in the payload instead of passing it as a field.
+ */
+function resolveImportedMetadata(
+ rawPayload: unknown,
+ overrideName?: string,
+ overrideDescription?: string
+): { name: string; description: string } {
+ const payload = unwrapResponseEnvelope(rawPayload)
+
+ const name =
+ overrideName ||
+ capLength(
+ readString(payload, 'state.metadata.name') ||
+ readString(payload, 'workflow.name') ||
+ readString(payload, 'metadata.name') ||
+ DEFAULT_IMPORTED_WORKFLOW_NAME,
+ V1_IMPORT_NAME_MAX_LENGTH
+ )
+
+ const description =
+ overrideDescription ??
+ capLength(
+ readString(payload, 'state.metadata.description') ??
+ readString(payload, 'workflow.description') ??
+ readString(payload, 'metadata.description') ??
+ '',
+ V1_IMPORT_DESCRIPTION_MAX_LENGTH
+ )
+
+ return { name, description }
+}
+
+/**
+ * POST /api/v1/workflows/import
+ *
+ * Creates a new workflow in the target workspace from an export payload
+ * produced by `GET /api/v1/workflows/{id}/export`. Block, edge, loop and
+ * parallel ids are regenerated on import, so the same payload can be imported
+ * repeatedly and alongside its source workflow without collisions.
+ */
+export const POST = withRouteHandler(async (request: NextRequest) => {
+ const requestId = generateId().slice(0, 8)
+
+ try {
+ const rateLimit = await checkRateLimit(request, 'workflow-import')
+ if (!rateLimit.allowed) {
+ return createRateLimitResponse(rateLimit)
+ }
+
+ const userId = rateLimit.userId!
+ const parsed = await parseRequest(
+ v1ImportWorkflowContract,
+ request,
+ {},
+ {
+ maxBodyBytes: MAX_IMPORT_BODY_BYTES,
+ validationErrorResponse: (error) =>
+ NextResponse.json(
+ {
+ error: getValidationErrorMessage(error, 'Invalid request body'),
+ details: error.issues,
+ },
+ { status: 400 }
+ ),
+ }
+ )
+ if (!parsed.success) return parsed.response
+
+ const {
+ workspaceId,
+ folderId,
+ name: overrideName,
+ description: overrideDescription,
+ } = parsed.data.body
+
+ logger.info(`[${requestId}] Importing workflow into workspace ${workspaceId}`, {
+ userId,
+ folderId,
+ })
+
+ const accessError = await validateWorkspaceAccess(rateLimit, userId, workspaceId, 'write')
+ if (accessError) return accessError
+
+ const [workspaceData] = await db
+ .select({ id: workspace.id })
+ .from(workspace)
+ .where(and(eq(workspace.id, workspaceId), isNull(workspace.archivedAt)))
+ .limit(1)
+
+ if (!workspaceData) {
+ return NextResponse.json({ error: 'Workspace not found' }, { status: 404 })
+ }
+
+ /**
+ * Ownership before lock state: `assertFolderMutable` walks the folder's
+ * ancestor chain without filtering on workspace, so checking it first would
+ * let a caller distinguish a locked folder in someone else's workspace
+ * (423) from a nonexistent one (404).
+ */
+ if (folderId) {
+ await assertFolderInWorkspace(folderId, workspaceId)
+ }
+ await assertFolderMutable(folderId ?? null)
+
+ const rawWorkflow = parsed.data.body.workflow
+ const workflowContent =
+ typeof rawWorkflow === 'string' ? rawWorkflow : JSON.stringify(rawWorkflow)
+
+ const { data: parsedState, errors } = parseWorkflowJson(workflowContent)
+ if (!parsedState || errors.length > 0) {
+ return NextResponse.json({ error: `Invalid workflow: ${errors.join(', ')}` }, { status: 400 })
+ }
+
+ /**
+ * Variables are normalized before validation, not after: older exports
+ * carry them as an array, which is a shape `workflowStateSchema` rightly
+ * rejects but the importer has always accepted. Normalizing first keeps
+ * that tolerance while still validating what actually gets persisted.
+ */
+ const variables = normalizeImportedVariables(parsedState.variables)
+
+ /**
+ * `parseWorkflowJson` only checks that blocks/edges are structurally
+ * present. The normalized tables are read back through
+ * {@link workflowStateSchema}, and the client parses that response
+ * strictly — so a block field with the wrong type (`data.extent: 'child'`,
+ * `data.count: '5'`) would persist happily here and then throw on every
+ * subsequent load, leaving a workflow nothing can open. Gate on the same
+ * schema the canonical `PUT /api/workflows/[id]/state` path enforces.
+ */
+ const stateValidation = workflowStateSchema.safeParse({ ...parsedState, variables })
+ if (!stateValidation.success) {
+ const issue = stateValidation.error.issues[0]
+ const path = issue?.path.join('.')
+ return NextResponse.json(
+ {
+ error: `Invalid workflow state${path ? ` at ${path}` : ''}: ${issue?.message ?? 'validation failed'}`,
+ details: serializeZodIssues(stateValidation.error),
+ },
+ { status: 400 }
+ )
+ }
+
+ /**
+ * Same normalization the editor's `PUT /api/workflows/[id]/state` runs, via
+ * the one shared implementation — the two import surfaces must land
+ * byte-identical data for the same payload.
+ */
+ const { state: preparedState, warnings } = prepareWorkflowStateForPersistence(parsedState)
+ if (warnings.length > 0) {
+ logger.warn(`[${requestId}] Normalized imported workflow with warnings`, { warnings })
+ }
+
+ const workflowState: WorkflowState = { ...parsedState, ...preparedState }
+
+ let parsedPayload: unknown = rawWorkflow
+ if (typeof rawWorkflow === 'string') {
+ try {
+ parsedPayload = JSON.parse(rawWorkflow)
+ } catch {
+ parsedPayload = undefined
+ }
+ }
+
+ const { name, description } = resolveImportedMetadata(
+ parsedPayload,
+ overrideName,
+ overrideDescription
+ )
+
+ const created = await performCreateWorkflow({
+ name,
+ description,
+ workspaceId,
+ folderId,
+ deduplicate: true,
+ userId,
+ requestId,
+ })
+
+ if (!created.success || !created.workflow) {
+ const status =
+ created.errorCode === 'conflict' ? 409 : created.errorCode === 'validation' ? 400 : 500
+ return NextResponse.json({ error: created.error }, { status })
+ }
+
+ const workflowId = created.workflow.id
+
+ /**
+ * The graph and the variables are written in one transaction so an import
+ * can never half-land, and any failure deletes the shell row created above
+ * — a caller that receives an error must not be left with a partially
+ * imported workflow in their workspace.
+ */
+ try {
+ await db.transaction(async (tx) => {
+ const saveResult = await saveWorkflowToNormalizedTables(workflowId, workflowState, tx)
+ if (!saveResult.success) {
+ throw new Error(saveResult.error || 'Failed to save workflow state')
+ }
+
+ if (Object.keys(variables).length > 0) {
+ await tx
+ .update(workflow)
+ .set({ variables, updatedAt: new Date() })
+ .where(eq(workflow.id, workflowId))
+ }
+ })
+ } catch (error) {
+ logger.error(`[${requestId}] Failed to persist imported workflow, rolling back`, {
+ workflowId,
+ error: getErrorMessage(error, 'Unknown error'),
+ })
+ /**
+ * The rollback runs under the same conditions that just failed the write,
+ * so it can fail too. Losing it must not turn into an unlogged orphan:
+ * the caller still gets a 500, but the id is recorded loudly enough to
+ * clean up.
+ */
+ try {
+ await db.delete(workflow).where(eq(workflow.id, workflowId))
+ } catch (rollbackError) {
+ logger.error(
+ `[${requestId}] Rollback failed, workflow ${workflowId} is orphaned in workspace ${workspaceId}`,
+ { workflowId, workspaceId, error: getErrorMessage(rollbackError, 'Unknown error') }
+ )
+ }
+ return NextResponse.json({ error: 'Failed to save workflow state' }, { status: 500 })
+ }
+
+ /**
+ * Matches the canonical state-write path: agent blocks may carry inline
+ * custom-tool definitions that must exist as workspace rows to be
+ * resolvable at execution. Failures are logged, not fatal — the workflow
+ * itself imported successfully.
+ */
+ try {
+ const { saved, errors: toolErrors } = await extractAndPersistCustomTools(
+ workflowState,
+ workspaceId,
+ userId
+ )
+ if (saved > 0 || toolErrors.length > 0) {
+ logger.info(`[${requestId}] Persisted ${saved} custom tool(s) from import`, {
+ workflowId,
+ errors: toolErrors,
+ })
+ }
+ } catch (error) {
+ logger.error(`[${requestId}] Failed to persist custom tools from import`, {
+ workflowId,
+ error: getErrorMessage(error, 'Unknown error'),
+ })
+ }
+
+ logger.info(`[${requestId}] Imported workflow ${workflowId} into workspace ${workspaceId}`, {
+ name: created.workflow.name,
+ blocksCount: Object.keys(workflowState.blocks).length,
+ })
+
+ const data: V1ImportWorkflowData = {
+ id: workflowId,
+ name: created.workflow.name,
+ description: created.workflow.description ?? null,
+ workspaceId,
+ folderId: created.workflow.folderId ?? null,
+ createdAt: created.workflow.createdAt.toISOString(),
+ updatedAt: created.workflow.updatedAt.toISOString(),
+ }
+
+ const limits = await getUserLimits(userId)
+ const apiResponse = createApiResponse({ data }, limits, rateLimit)
+
+ return NextResponse.json(apiResponse.body, { status: 201, headers: apiResponse.headers })
+ } catch (error: unknown) {
+ if (error instanceof FolderLockedError || error instanceof FolderNotFoundError) {
+ return NextResponse.json({ error: error.message }, { status: error.status })
+ }
+ const message = getErrorMessage(error, 'Unknown error')
+ logger.error(`[${requestId}] Workflow import error`, { error: message })
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
+ }
+})
diff --git a/apps/sim/app/api/workflows/[id]/state/route.ts b/apps/sim/app/api/workflows/[id]/state/route.ts
index fbd33b045b4..209cfd226e8 100644
--- a/apps/sim/app/api/workflows/[id]/state/route.ts
+++ b/apps/sim/app/api/workflows/[id]/state/route.ts
@@ -17,14 +17,12 @@ import { generateRequestId } from '@/lib/core/utils/request'
import { getSocketServerUrl } from '@/lib/core/utils/urls'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { extractAndPersistCustomTools } from '@/lib/workflows/persistence/custom-tools-persistence'
+import { prepareWorkflowStateForPersistence } from '@/lib/workflows/persistence/prepare-state'
import {
loadWorkflowFromNormalizedTables,
saveWorkflowToNormalizedTables,
} from '@/lib/workflows/persistence/utils'
-import { sanitizeAgentToolsInBlocks } from '@/lib/workflows/sanitization/validation'
-import { validateEdges } from '@/stores/workflows/workflow/edge-validation'
import type { BlockState, WorkflowState } from '@/stores/workflows/workflow/types'
-import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils'
const logger = createLogger('WorkflowStateAPI')
@@ -152,46 +150,14 @@ export const PUT = withRouteHandler(
// per variable (the path param is the source of truth), so the check
// is unreachable and was removed.
- // Sanitize custom tools in agent blocks before saving
- const { blocks: sanitizedBlocks, warnings } = sanitizeAgentToolsInBlocks(
- state.blocks as Record
- )
-
- // Save to normalized tables
- // Ensure all required fields are present for WorkflowState type
- // Filter out blocks without type or name before saving
- const filteredBlocks = Object.entries(sanitizedBlocks).reduce(
- (acc, [blockId, block]: [string, BlockState]) => {
- if (block.type && block.name) {
- // Ensure all required fields are present
- acc[blockId] = {
- ...block,
- enabled: block.enabled !== undefined ? block.enabled : true,
- horizontalHandles:
- block.horizontalHandles !== undefined ? block.horizontalHandles : true,
- height: block.height !== undefined ? block.height : 0,
- subBlocks: block.subBlocks || {},
- outputs: block.outputs || {},
- }
- }
- return acc
- },
- {} as typeof state.blocks
- )
-
- const typedBlocks = filteredBlocks as Record
- const validatedEdges = validateEdges(state.edges as WorkflowState['edges'], typedBlocks)
- const validationWarnings = validatedEdges.dropped.map(
- ({ edge, reason }) => `Dropped edge "${edge.id}": ${reason}`
- )
- const canonicalLoops = generateLoopBlocks(typedBlocks)
- const canonicalParallels = generateParallelBlocks(typedBlocks)
+ const { state: preparedState, warnings: preparationWarnings } =
+ prepareWorkflowStateForPersistence({
+ blocks: state.blocks as Record,
+ edges: state.edges as WorkflowState['edges'],
+ })
const workflowState = {
- blocks: filteredBlocks,
- edges: validatedEdges.valid,
- loops: canonicalLoops,
- parallels: canonicalParallels,
+ ...preparedState,
lastSaved: state.lastSaved || Date.now(),
isDeployed: state.isDeployed || false,
deployedAt: state.deployedAt,
@@ -303,10 +269,7 @@ export const PUT = withRouteHandler(
)
}
- return NextResponse.json(
- { success: true, warnings: [...warnings, ...validationWarnings] },
- { status: 200 }
- )
+ return NextResponse.json({ success: true, warnings: preparationWarnings }, { status: 200 })
} catch (error: any) {
if (error instanceof WorkflowLockedError) {
return NextResponse.json({ error: error.message }, { status: error.status })
diff --git a/apps/sim/app/api/workspaces/[id]/files/download/route.integration.test.ts b/apps/sim/app/api/workspaces/[id]/files/download/route.integration.test.ts
new file mode 100644
index 00000000000..d7a871fbcbf
--- /dev/null
+++ b/apps/sim/app/api/workspaces/[id]/files/download/route.integration.test.ts
@@ -0,0 +1,171 @@
+/**
+ * @vitest-environment node
+ *
+ * Exercises the real archive plumbing — archiver, the lazy entry generator, and the
+ * Node-to-web bridge — against multi-chunk file streams, and validates the result with
+ * the operating system's `unzip` rather than the same JS library that produced it. The
+ * bug this route exists to fix was an archive a real zip reader could not open, so a
+ * self-consistent JS round-trip is not the assertion that matters.
+ */
+import { execFile } from 'node:child_process'
+import { createReadStream } from 'node:fs'
+import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+import { promisify } from 'node:util'
+import { createMockRequest } from '@sim/testing'
+import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
+
+const run = promisify(execFile)
+
+const {
+ mockGetSession,
+ mockVerifyWorkspaceMembership,
+ mockListWorkspaceFiles,
+ mockListFolders,
+ mockDownloadFileStream,
+} = vi.hoisted(() => ({
+ mockGetSession: vi.fn(),
+ mockVerifyWorkspaceMembership: vi.fn(),
+ mockListWorkspaceFiles: vi.fn(),
+ mockListFolders: vi.fn(),
+ mockDownloadFileStream: vi.fn(),
+}))
+
+vi.mock('@/lib/auth', () => ({
+ auth: { api: { getSession: vi.fn() } },
+ getSession: mockGetSession,
+}))
+vi.mock('@/app/api/workflows/utils', () => ({
+ verifyWorkspaceMembership: mockVerifyWorkspaceMembership,
+}))
+vi.mock('@/lib/uploads/contexts/workspace', () => ({
+ listWorkspaceFiles: mockListWorkspaceFiles,
+ listWorkspaceFileFolders: mockListFolders,
+ buildWorkspaceFileFolderPathMap: (folders: Array<{ id: string; name: string }>) =>
+ new Map(folders.map((folder) => [folder.id, folder.name])),
+ fetchServableWorkspaceFileBuffer: vi.fn(),
+}))
+vi.mock('@/lib/uploads/core/storage-service', () => ({
+ downloadFileStream: mockDownloadFileStream,
+}))
+vi.mock('@sim/audit', () => ({
+ recordAudit: vi.fn(),
+ AuditAction: { FILE_DOWNLOADED: 'file.downloaded' },
+ AuditResourceType: { FILE: 'file' },
+}))
+vi.mock('@/lib/posthog/server', () => ({ captureServerEvent: vi.fn() }))
+
+import { GET } from '@/app/api/workspaces/[id]/files/download/route'
+
+const WORKSPACE_ID = 'ws-1'
+const context = { params: Promise.resolve({ id: WORKSPACE_ID }) }
+
+let workDir: string
+let bigPath: string
+let bigBytes: Buffer
+/** The OS unzip is the point of this file; skip rather than fail where it is absent. */
+let hasUnzip = true
+
+beforeAll(async () => {
+ hasUnzip = await run('unzip', ['-v']).then(
+ () => true,
+ () => false
+ )
+ workDir = await mkdtemp(join(tmpdir(), 'sim-archive-'))
+ bigPath = join(workDir, 'big.bin')
+ // Several MB of non-repeating bytes: forces many 64 KiB chunks through the generator,
+ // the archiver queue and the web bridge, and would expose a truncation or ordering bug.
+ bigBytes = Buffer.alloc(5 * 1024 * 1024)
+ for (let i = 0; i < bigBytes.length; i++) bigBytes[i] = (i * 31 + (i >> 8)) & 0xff
+ await writeFile(bigPath, bigBytes)
+})
+
+afterAll(async () => {
+ await rm(workDir, { recursive: true, force: true })
+})
+
+describe('workspace files download — real archive', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ mockGetSession.mockResolvedValue({ user: { id: 'user-1' } })
+ mockVerifyWorkspaceMembership.mockResolvedValue({ role: 'member' })
+ mockListFolders.mockResolvedValue([{ id: 'folder-1', name: 'Reports', parentId: null }])
+ // A real fs stream, not a single pre-made buffer.
+ mockDownloadFileStream.mockImplementation(async () => createReadStream(bigPath))
+ })
+
+ it('produces an archive the OS unzip accepts, with byte-exact contents', async (ctx) => {
+ if (!hasUnzip) ctx.skip()
+ mockListWorkspaceFiles.mockResolvedValue([
+ {
+ id: 'f1',
+ name: 'big.bin',
+ key: `workspace/${WORKSPACE_ID}/f1`,
+ path: '/serve/f1',
+ size: bigBytes.length,
+ type: 'application/octet-stream',
+ folderId: 'folder-1',
+ },
+ ])
+
+ const response = await GET(
+ createMockRequest(
+ 'GET',
+ undefined,
+ {},
+ `http://localhost:3000/api/workspaces/${WORKSPACE_ID}/files/download?fileIds=f1`
+ ),
+ context
+ )
+ expect(response.status).toBe(200)
+
+ const zipPath = join(workDir, 'out.zip')
+ await writeFile(zipPath, Buffer.from(await response.arrayBuffer()))
+
+ // Independent validation: the CRCs and central directory have to satisfy a real
+ // zip reader, which is exactly what failed for the customer.
+ await expect(run('unzip', ['-t', zipPath])).resolves.toBeTruthy()
+
+ await run('unzip', ['-o', '-q', zipPath, '-d', join(workDir, 'out')])
+ const extracted = await readFile(join(workDir, 'out', 'Reports', 'big.bin'))
+ expect(extracted.length).toBe(bigBytes.length)
+ expect(extracted.equals(bigBytes)).toBe(true)
+ })
+
+ it('keeps entries intact and correctly named across several streamed files', async (ctx) => {
+ if (!hasUnzip) ctx.skip()
+ mockListWorkspaceFiles.mockResolvedValue(
+ ['a.bin', 'b.bin', 'c.bin'].map((name, index) => ({
+ id: `f${index}`,
+ name,
+ key: `workspace/${WORKSPACE_ID}/f${index}`,
+ path: `/serve/f${index}`,
+ size: bigBytes.length,
+ type: 'application/octet-stream',
+ folderId: 'folder-1',
+ }))
+ )
+
+ const response = await GET(
+ createMockRequest(
+ 'GET',
+ undefined,
+ {},
+ `http://localhost:3000/api/workspaces/${WORKSPACE_ID}/files/download?fileIds=f0&fileIds=f1&fileIds=f2`
+ ),
+ context
+ )
+
+ const zipPath = join(workDir, 'multi.zip')
+ await writeFile(zipPath, Buffer.from(await response.arrayBuffer()))
+ await expect(run('unzip', ['-t', zipPath])).resolves.toBeTruthy()
+
+ const { stdout } = await run('unzip', ['-l', zipPath])
+ for (const name of ['Reports/a.bin', 'Reports/b.bin', 'Reports/c.bin']) {
+ expect(stdout).toContain(name)
+ }
+ // Each entry carries the full payload — a shared or truncated stream would not.
+ expect(stdout.match(/5242880/g)).toHaveLength(3)
+ })
+})
diff --git a/apps/sim/app/api/workspaces/[id]/files/download/route.test.ts b/apps/sim/app/api/workspaces/[id]/files/download/route.test.ts
new file mode 100644
index 00000000000..41dc0680b7b
--- /dev/null
+++ b/apps/sim/app/api/workspaces/[id]/files/download/route.test.ts
@@ -0,0 +1,349 @@
+/**
+ * @vitest-environment node
+ */
+import { Readable } from 'stream'
+import { createMockRequest } from '@sim/testing'
+import JSZip from 'jszip'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+
+const {
+ mockGetSession,
+ mockVerifyWorkspaceMembership,
+ mockListWorkspaceFiles,
+ mockListWorkspaceFileFolders,
+ mockFetchServableWorkspaceFileBuffer,
+ mockDownloadFileStream,
+} = vi.hoisted(() => ({
+ mockGetSession: vi.fn(),
+ mockVerifyWorkspaceMembership: vi.fn(),
+ mockListWorkspaceFiles: vi.fn(),
+ mockListWorkspaceFileFolders: vi.fn(),
+ mockFetchServableWorkspaceFileBuffer: vi.fn(),
+ mockDownloadFileStream: vi.fn(),
+}))
+
+vi.mock('@/lib/auth', () => ({
+ auth: { api: { getSession: vi.fn() } },
+ getSession: mockGetSession,
+}))
+
+vi.mock('@/app/api/workflows/utils', () => ({
+ verifyWorkspaceMembership: mockVerifyWorkspaceMembership,
+}))
+
+vi.mock('@/lib/uploads/contexts/workspace', () => ({
+ listWorkspaceFiles: mockListWorkspaceFiles,
+ listWorkspaceFileFolders: mockListWorkspaceFileFolders,
+ buildWorkspaceFileFolderPathMap: (folders: Array<{ id: string; name: string }>) =>
+ new Map(folders.map((folder) => [folder.id, folder.name])),
+ fetchServableWorkspaceFileBuffer: mockFetchServableWorkspaceFileBuffer,
+}))
+
+vi.mock('@/lib/uploads/core/storage-service', () => ({
+ downloadFileStream: mockDownloadFileStream,
+}))
+
+vi.mock('@sim/audit', () => ({
+ recordAudit: vi.fn(),
+ AuditAction: { FILE_DOWNLOADED: 'file.downloaded' },
+ AuditResourceType: { FILE: 'file' },
+}))
+
+vi.mock('@/lib/posthog/server', () => ({ captureServerEvent: vi.fn() }))
+
+import { DocCompileUserError } from '@/lib/copilot/tools/server/files/doc-compile'
+import { PayloadSizeLimitError } from '@/lib/core/utils/stream-limits'
+import { GET } from '@/app/api/workspaces/[id]/files/download/route'
+
+const WORKSPACE_ID = 'ws-1'
+const context = { params: Promise.resolve({ id: WORKSPACE_ID }) }
+const MB = 1024 * 1024
+
+function workspaceFile(id: string, name: string, folderId: string | null = 'folder-1') {
+ return {
+ id,
+ name,
+ key: `workspace/${WORKSPACE_ID}/${id}`,
+ path: `/serve/${id}`,
+ size: 100,
+ type: 'application/octet-stream',
+ folderId,
+ }
+}
+
+/** A file whose stored bytes are a generator source, so it must be resolved. */
+function generatedDocument(id: string, name: string, folderId: string | null = 'folder-1') {
+ return { ...workspaceFile(id, name, folderId), type: 'text/x-docxjs' }
+}
+
+function requestFor(query: string) {
+ return createMockRequest(
+ 'GET',
+ undefined,
+ {},
+ `http://localhost:3000/api/workspaces/${WORKSPACE_ID}/files/download?${query}`
+ )
+}
+
+async function zipFrom(response: Response) {
+ return JSZip.loadAsync(Buffer.from(await response.arrayBuffer()))
+}
+
+describe('workspace files download route', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ mockGetSession.mockResolvedValue({ user: { id: 'user-1' } })
+ mockVerifyWorkspaceMembership.mockResolvedValue({ role: 'member' })
+ mockListWorkspaceFileFolders.mockResolvedValue([
+ { id: 'folder-1', name: 'Reports', parentId: null },
+ ])
+ mockDownloadFileStream.mockImplementation(async () => Readable.from([Buffer.from('plain')]))
+ })
+
+ it('zips the rendered bytes for a generated doc, not its stored source', async () => {
+ mockListWorkspaceFiles.mockResolvedValue([generatedDocument('f1', 'overview.docx')])
+ // A real .docx is a ZIP; the stored source would be plain JS text.
+ const rendered = Buffer.from('PKrendered-docx')
+ mockFetchServableWorkspaceFileBuffer.mockResolvedValue({
+ buffer: rendered,
+ contentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ })
+
+ const response = await GET(requestFor('fileIds=f1'), context)
+
+ expect(response.status).toBe(200)
+ const entry = (await zipFrom(response)).file('Reports/overview.docx')
+ expect(entry).not.toBeNull()
+ expect(Buffer.from(await entry!.async('uint8array'))).toEqual(rendered)
+ })
+
+ it('streams ordinary files instead of materializing them', async () => {
+ mockListWorkspaceFiles.mockResolvedValue([workspaceFile('f1', 'clip.mp4')])
+
+ const response = await GET(requestFor('fileIds=f1'), context)
+
+ expect(response.status).toBe(200)
+ // Nothing has been read yet: the entry opens its storage read only once the
+ // consumer pulls the archive, which is what keeps peak memory to one entry.
+ expect(mockDownloadFileStream).not.toHaveBeenCalled()
+
+ const zip = await zipFrom(response)
+
+ expect(mockDownloadFileStream).toHaveBeenCalledTimes(1)
+ // Never routed through the buffering document reader.
+ expect(mockFetchServableWorkspaceFileBuffer).not.toHaveBeenCalled()
+
+ const entry = zip.file('Reports/clip.mp4')
+ expect(entry).not.toBeNull()
+ expect(await entry!.async('string')).toBe('plain')
+ })
+
+ it('preserves nested folder paths across both entry kinds', async () => {
+ mockListWorkspaceFileFolders.mockResolvedValue([
+ { id: 'folder-1', name: 'Reports', parentId: null },
+ { id: 'folder-2', name: 'visuals', parentId: 'folder-1' },
+ ])
+ mockListWorkspaceFiles.mockResolvedValue([
+ generatedDocument('f1', 'summary.docx', 'folder-1'),
+ workspaceFile('f2', 'hero.png', 'folder-2'),
+ ])
+ mockFetchServableWorkspaceFileBuffer.mockResolvedValue({
+ buffer: Buffer.from('PKdoc'),
+ contentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ })
+
+ const zip = await zipFrom(await GET(requestFor('fileIds=f1&fileIds=f2'), context))
+
+ expect(zip.file('Reports/summary.docx')).not.toBeNull()
+ expect(zip.file('visuals/hero.png')).not.toBeNull()
+ })
+
+ it('returns 409 naming the documents whose artifacts are still compiling', async () => {
+ mockListWorkspaceFiles.mockResolvedValue([
+ generatedDocument('f1', 'ready.docx'),
+ generatedDocument('f2', 'pending.docx'),
+ ])
+ mockFetchServableWorkspaceFileBuffer.mockImplementation(async (file: { name: string }) => {
+ if (file.name === 'pending.docx')
+ throw new DocCompileUserError('Document is still being generated')
+ return { buffer: Buffer.from('PKok'), contentType: 'application/octet-stream' }
+ })
+
+ const response = await GET(requestFor('fileIds=f1&fileIds=f2'), context)
+
+ expect(response.status).toBe(409)
+ const body = await response.json()
+ expect(body.error).toContain('pending.docx')
+ expect(body.error).not.toContain('ready.docx')
+ })
+
+ it('rejects with 400, not 500, when a document blows its own allowance', async () => {
+ mockListWorkspaceFiles.mockResolvedValue([generatedDocument('f1', 'huge.docx')])
+ mockFetchServableWorkspaceFileBuffer.mockRejectedValue(
+ new PayloadSizeLimitError({ label: 'servable file download', maxBytes: 1 })
+ )
+
+ const response = await GET(requestFor('fileIds=f1'), context)
+
+ expect(response.status).toBe(400)
+ const body = await response.json()
+ expect(body.error).toContain('huge.docx')
+ expect(body.error).not.toContain('Selected files total')
+ })
+
+ it('counts streamed files against the same budget as rendered documents', async () => {
+ // 200 MB of ordinary files leaves 50 MB of the 250 MB budget for documents.
+ mockListWorkspaceFiles.mockResolvedValue([
+ { ...workspaceFile('f1', 'clip.mp4'), size: 200 * MB },
+ generatedDocument('f2', 'report.docx'),
+ ])
+ mockFetchServableWorkspaceFileBuffer.mockResolvedValue({
+ buffer: Buffer.from('PKdoc'),
+ contentType: 'application/octet-stream',
+ })
+
+ await zipFrom(await GET(requestFor('fileIds=f1&fileIds=f2'), context))
+
+ // Without reserving the streamed bytes the document would get the full 50 MB
+ // ceiling, letting the archive ship 250 MB of documents on top of 200 MB of video.
+ expect(mockFetchServableWorkspaceFileBuffer.mock.calls[0][1].maxBytes).toBe(50 * MB)
+ })
+
+ it('rejects when streamed files leave no budget for a document', async () => {
+ mockListWorkspaceFiles.mockResolvedValue([
+ { ...workspaceFile('f1', 'clip.mp4'), size: 249 * MB },
+ generatedDocument('f2', 'report.docx'),
+ ])
+ mockFetchServableWorkspaceFileBuffer.mockRejectedValue(
+ new PayloadSizeLimitError({ label: 'servable file download', maxBytes: 1 })
+ )
+
+ const response = await GET(requestFor('fileIds=f1&fileIds=f2'), context)
+
+ expect(response.status).toBe(400)
+ const body = await response.json()
+ expect(body.error).toContain('once documents are rendered')
+ // No byte count: the rendered total is not knowable, so quoting one would mislead.
+ expect(body.error).not.toContain('Selected files total')
+ })
+
+ it('blames the shared budget once earlier documents have consumed it', async () => {
+ mockListWorkspaceFiles.mockResolvedValue([
+ generatedDocument('f1', 'first.docx'),
+ generatedDocument('f2', 'second.docx'),
+ ])
+ mockFetchServableWorkspaceFileBuffer.mockImplementation(async (file: { name: string }) => {
+ // The first document eats the whole budget, so the second's cap is the remainder.
+ if (file.name === 'first.docx') {
+ return { buffer: Buffer.alloc(240 * MB), contentType: 'application/octet-stream' }
+ }
+ throw new PayloadSizeLimitError({ label: 'servable file download', maxBytes: 1 })
+ })
+
+ const response = await GET(requestFor('fileIds=f1&fileIds=f2'), context)
+
+ expect(response.status).toBe(400)
+ const body = await response.json()
+ expect(body.error).toContain('once documents are rendered')
+ expect(body.error).not.toContain('second.docx')
+ })
+
+ it.each([
+ ['text/x-docxjs', 'report.docx'],
+ ['text/x-pptxgenjs', 'deck.pptx'],
+ ['text/x-pdflibjs', 'isolated.pdf'],
+ ['text/x-python-pdf', 'sandboxed.pdf'],
+ ['text/x-python-xlsx', 'sheet.xlsx'],
+ ])('resolves %s rather than streaming its source', async (type, name) => {
+ // Both PDF generators must be covered: the isolated-vm path stores pdf-lib JS and
+ // the E2B path stores Python, and either one streamed raw is the corruption bug.
+ mockListWorkspaceFiles.mockResolvedValue([{ ...workspaceFile('f1', name), type }])
+ mockFetchServableWorkspaceFileBuffer.mockResolvedValue({
+ buffer: Buffer.from('PKrendered'),
+ contentType: 'application/octet-stream',
+ })
+
+ await zipFrom(await GET(requestFor('fileIds=f1'), context))
+
+ expect(mockFetchServableWorkspaceFileBuffer).toHaveBeenCalledTimes(1)
+ expect(mockDownloadFileStream).not.toHaveBeenCalled()
+ })
+
+ it('streams an uploaded office file rather than resolving it', async () => {
+ // A real upload serves exactly its stored bytes, so it must not take the buffered
+ // path — otherwise a selection of large decks is held in memory for nothing.
+ const upload = {
+ ...workspaceFile('f1', 'deck.pptx'),
+ size: 80 * MB,
+ type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ }
+ mockListWorkspaceFiles.mockResolvedValue([upload])
+
+ const response = await GET(requestFor('fileIds=f1'), context)
+ await zipFrom(response)
+
+ expect(response.status).toBe(200)
+ expect(mockFetchServableWorkspaceFileBuffer).not.toHaveBeenCalled()
+ expect(mockDownloadFileStream).toHaveBeenCalledTimes(1)
+ })
+
+ it('caps a generated document at the render headroom', async () => {
+ mockListWorkspaceFiles.mockResolvedValue([generatedDocument('f1', 'report.docx')])
+ mockFetchServableWorkspaceFileBuffer.mockResolvedValue({
+ buffer: Buffer.from('PKdoc'),
+ contentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ })
+
+ await zipFrom(await GET(requestFor('fileIds=f1'), context))
+
+ expect(mockFetchServableWorkspaceFileBuffer.mock.calls[0][1].maxBytes).toBe(50 * MB)
+ })
+
+ it('surfaces a storage failure as a 500 even when another document is pending', async () => {
+ mockListWorkspaceFiles.mockResolvedValue([
+ generatedDocument('f1', 'pending.docx'),
+ generatedDocument('f2', 'broken.docx'),
+ ])
+ mockFetchServableWorkspaceFileBuffer.mockImplementation(async (file: { name: string }) => {
+ if (file.name === 'pending.docx')
+ throw new DocCompileUserError('Document is still being generated')
+ throw new Error('storage down')
+ })
+
+ const response = await GET(requestFor('fileIds=f1&fileIds=f2'), context)
+
+ // A 409 would tell the client to retry something that can never succeed.
+ expect(response.status).toBe(500)
+ })
+
+ it('stops resolving documents once one hard-fails', async () => {
+ const files = Array.from({ length: 20 }, (_, index) =>
+ generatedDocument(`f${index}`, `doc${index}.docx`)
+ )
+ mockListWorkspaceFiles.mockResolvedValue(files)
+ mockFetchServableWorkspaceFileBuffer.mockImplementation(async (file: { name: string }) => {
+ if (file.name === 'doc0.docx') throw new Error('storage down')
+ return { buffer: Buffer.from('PKok'), contentType: 'application/octet-stream' }
+ })
+
+ const response = await GET(
+ requestFor(files.map((file) => `fileIds=${file.id}`).join('&')),
+ context
+ )
+
+ expect(response.status).toBe(500)
+ expect(mockFetchServableWorkspaceFileBuffer.mock.calls.length).toBeLessThan(files.length)
+ })
+
+ it('rejects a selection whose declared sizes already exceed the limit', async () => {
+ mockListWorkspaceFiles.mockResolvedValue([
+ { ...workspaceFile('f1', 'a.mp4'), size: 200 * MB },
+ { ...workspaceFile('f2', 'b.mp4'), size: 200 * MB },
+ ])
+
+ const response = await GET(requestFor('fileIds=f1&fileIds=f2'), context)
+
+ expect(response.status).toBe(400)
+ expect(mockDownloadFileStream).not.toHaveBeenCalled()
+ })
+})
diff --git a/apps/sim/app/api/workspaces/[id]/files/download/route.ts b/apps/sim/app/api/workspaces/[id]/files/download/route.ts
index 577f0e7b2dc..67b070f303f 100644
--- a/apps/sim/app/api/workspaces/[id]/files/download/route.ts
+++ b/apps/sim/app/api/workspaces/[id]/files/download/route.ts
@@ -1,19 +1,30 @@
+import { Readable } from 'node:stream'
import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
import { createLogger } from '@sim/logger'
-import JSZip from 'jszip'
+import { ZipArchive } from 'archiver'
import { type NextRequest, NextResponse } from 'next/server'
import { downloadWorkspaceFileItemsContract } from '@/lib/api/contracts/workspace-file-folders'
import { parseRequest } from '@/lib/api/server'
import { getSession } from '@/lib/auth'
+import { nodeReadableToWebStream } from '@/lib/core/utils/node-stream'
+import { PayloadSizeLimitError } from '@/lib/core/utils/stream-limits'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { captureServerEvent } from '@/lib/posthog/server'
+import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace'
import {
buildWorkspaceFileFolderPathMap,
- fetchWorkspaceFileBuffer,
+ fetchServableWorkspaceFileBuffer,
listWorkspaceFileFolders,
listWorkspaceFiles,
} from '@/lib/uploads/contexts/workspace'
-import { formatFileSize } from '@/lib/uploads/utils/file-utils'
+import { downloadFileStream } from '@/lib/uploads/core/storage-service'
+import {
+ formatFileSize,
+ isGeneratedDocumentSourceType,
+ isRenderableDocumentName,
+ MAX_RENDERED_DOCUMENT_BYTES,
+} from '@/lib/uploads/utils/file-utils'
+import { docNotReadyMessage, isDocNotReadyError } from '@/lib/uploads/utils/servable-file-response'
import { buildZipEntryPaths } from '@/lib/uploads/zip-entry-path'
import { verifyWorkspaceMembership } from '@/app/api/workflows/utils'
@@ -21,6 +32,62 @@ const logger = createLogger('WorkspaceFilesDownloadAPI')
const MAX_ZIP_DOWNLOAD_FILES = 100
const MAX_ZIP_DOWNLOAD_BYTES = 250 * 1024 * 1024
+/**
+ * Whether this entry's stored bytes are a generation source that has to be resolved
+ * before it can go in the archive. An ordinary uploaded `.docx` serves exactly what is
+ * stored, so it streams like anything else — routing every office extension through the
+ * buffered path would hold a whole selection of real documents in memory for a check
+ * the resolver settles on the first few magic bytes. Metadata without a type falls back
+ * to the extension: better to resolve and pass through than to stream source text under
+ * a document name.
+ */
+function needsRendering(file: WorkspaceFileRecord): boolean {
+ return file.type ? isGeneratedDocumentSourceType(file.type) : isRenderableDocumentName(file.name)
+}
+
+/**
+ * A `Readable` that opens its storage read on first pull rather than up front. The
+ * archiver works through entries sequentially, so handing it an open stream per entry
+ * would hold a connection per selected file — more than the storage client pools — with
+ * most sitting idle until their turn. The generator body does not run until the first
+ * read, and a failure to open surfaces as the stream's `error` event.
+ */
+function lazyWorkspaceFileStream(file: WorkspaceFileRecord): Readable {
+ return Readable.from(
+ (async function* () {
+ yield* await downloadFileStream({
+ key: file.key,
+ context: file.storageContext ?? 'workspace',
+ })
+ })(),
+ // `Readable.from` defaults to object mode; these are bytes headed for an archive.
+ { objectMode: false }
+ )
+}
+
+function selectionTooLargeResponse(bytes: number): NextResponse {
+ return NextResponse.json(
+ {
+ error: `Selected files total ${formatFileSize(bytes)}, which exceeds the ${formatFileSize(MAX_ZIP_DOWNLOAD_BYTES)} download limit.`,
+ },
+ { status: 400 }
+ )
+}
+
+/**
+ * The rendered archive would exceed the limit even though the declared sizes did not —
+ * generated documents render to more than the source they declared, so no accurate byte
+ * count exists to quote here.
+ */
+function archiveTooLargeResponse(): NextResponse {
+ return NextResponse.json(
+ {
+ error: `The selected files exceed the ${formatFileSize(MAX_ZIP_DOWNLOAD_BYTES)} download limit once documents are rendered. Select fewer files.`,
+ },
+ { status: 400 }
+ )
+}
+
function collectDescendantFolderIds(
selectedFolderIds: string[],
folders: Array<{ id: string; parentId: string | null }>
@@ -82,17 +149,61 @@ export const GET = withRouteHandler(
)
}
- const totalBytes = filesToZip.reduce((sum, file) => sum + file.size, 0)
- if (totalBytes > MAX_ZIP_DOWNLOAD_BYTES) {
- return NextResponse.json(
- {
- error: `Selected files total ${formatFileSize(totalBytes)}, which exceeds the ${formatFileSize(MAX_ZIP_DOWNLOAD_BYTES)} download limit.`,
- },
- { status: 400 }
- )
+ const declaredBytes = filesToZip.reduce((sum, file) => sum + file.size, 0)
+ if (declaredBytes > MAX_ZIP_DOWNLOAD_BYTES) {
+ return selectionTooLargeResponse(declaredBytes)
}
- const buffers = await Promise.all(filesToZip.map((file) => fetchWorkspaceFileBuffer(file)))
+ // Streamed entries ship exactly what they declared, so their share of the budget is
+ // known up front and is reserved here. Documents then draw from what is left —
+ // one budget across both kinds, or the archive could ship two full limits' worth.
+ const reservedForStreamed = filesToZip
+ .filter((file) => !needsRendering(file))
+ .reduce((sum, file) => sum + file.size, 0)
+
+ // Generated documents are resolved before the archive starts: once the first byte
+ // is written the status code is committed, so anything that can still fail the
+ // request has to fail here. Their buffers are held until the archive is assembled,
+ // bounded by what is left of the request's byte budget.
+ const renderedDocuments = new Map()
+ const pendingNames: string[] = []
+ let renderedBytes = 0
+
+ for (const file of filesToZip) {
+ if (!needsRendering(file)) continue
+
+ const remaining = Math.max(0, MAX_ZIP_DOWNLOAD_BYTES - reservedForStreamed - renderedBytes)
+ // A source's declared size says nothing about what it renders to, so the cap is
+ // the per-document ceiling, bounded by what is left of the budget.
+ const allowance = Math.min(remaining, MAX_RENDERED_DOCUMENT_BYTES)
+
+ try {
+ const { buffer } = await fetchServableWorkspaceFileBuffer(file, { maxBytes: allowance })
+ renderedBytes += buffer.length
+ renderedDocuments.set(file.id, buffer)
+ } catch (error) {
+ if (error instanceof PayloadSizeLimitError) {
+ // Blamed on the entry when its own ceiling was the binding cap; otherwise the
+ // documents ahead of it have consumed the budget.
+ return allowance === MAX_RENDERED_DOCUMENT_BYTES
+ ? NextResponse.json(
+ {
+ error: `"${file.name}" renders to more than ${formatFileSize(MAX_RENDERED_DOCUMENT_BYTES)} and is too large to include in a zip; download it on its own instead.`,
+ },
+ { status: 400 }
+ )
+ : archiveTooLargeResponse()
+ }
+ // Pending artifacts are collected so the 409 can name all of them; anything
+ // else dooms the request and waiting cannot fix it.
+ if (!isDocNotReadyError(error)) throw error
+ pendingNames.push(file.name)
+ }
+ }
+
+ if (pendingNames.length > 0) {
+ return NextResponse.json({ error: docNotReadyMessage(pendingNames) }, { status: 409 })
+ }
// Entry paths stay workspace-root-relative so a mixed selection of folders and
// loose files keeps the layout the user sees in the files list.
@@ -103,12 +214,22 @@ export const GET = withRouteHandler(
}))
)
- const zip = new JSZip()
- for (const [index, buffer] of buffers.entries()) {
- zip.file(entryPaths[index], buffer)
- }
+ // Ordinary files are never materialized: each entry opens its storage read only
+ // when the archiver reaches it, so one entry is resident rather than the archive.
+ const archive = new ZipArchive({ store: true })
+ archive.on('warning', (error: Error) => {
+ logger.warn('Archive warning while streaming workspace files', { error })
+ })
- const zipBuffer = await zip.generateAsync({ type: 'nodebuffer' })
+ filesToZip.forEach((file, index) => {
+ const rendered = renderedDocuments.get(file.id)
+ archive.append(rendered ?? lazyWorkspaceFileStream(file), { name: entryPaths[index] })
+ })
+ archive.finalize().catch((error) => {
+ // The archive's `error` event already fails the response stream; this keeps the
+ // same failure from also surfacing as an unhandled rejection.
+ logger.error('Failed to finalize workspace file archive', { error })
+ })
recordAudit({
workspaceId,
@@ -116,7 +237,7 @@ export const GET = withRouteHandler(
action: AuditAction.FILE_DOWNLOADED,
resourceType: AuditResourceType.FILE,
description: `Downloaded ${filesToZip.length} file${filesToZip.length === 1 ? '' : 's'} as zip`,
- metadata: { fileCount: filesToZip.length, totalBytes },
+ metadata: { fileCount: filesToZip.length, totalBytes: declaredBytes },
request,
})
captureServerEvent(
@@ -126,7 +247,8 @@ export const GET = withRouteHandler(
{ groups: { workspace: workspaceId } }
)
- return new NextResponse(new Uint8Array(zipBuffer), {
+ // No Content-Length: the archive size is not known until it has been produced.
+ return new NextResponse(nodeReadableToWebStream(archive), {
headers: {
'Content-Type': 'application/zip',
'Content-Disposition': 'attachment; filename="workspace-files.zip"',
diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/use-editable-file-content.ts b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/use-editable-file-content.ts
index cb6defd8d95..984d2b2e1de 100644
--- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/use-editable-file-content.ts
+++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/use-editable-file-content.ts
@@ -3,6 +3,7 @@
import { useCallback, useEffect, useMemo, useReducer, useRef } from 'react'
import { toast } from '@sim/emcn'
import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace'
+import { GENERATED_DOCUMENT_SOURCE_TYPES } from '@/lib/uploads/utils/file-utils'
import {
useUpdateWorkspaceFileContent,
useWorkspaceFileContent,
@@ -20,13 +21,7 @@ import {
* editable text is the source program, not the compiled artifact. The serve route
* returns that source only when asked for the raw representation.
*/
-const GENERATED_SOURCE_FILE_TYPES = new Set([
- 'text/x-pptxgenjs',
- 'text/x-docxjs',
- 'text/x-pdflibjs',
- 'text/x-python-pdf',
- 'text/x-python-xlsx',
-])
+const GENERATED_SOURCE_FILE_TYPES = GENERATED_DOCUMENT_SOURCE_TYPES
/**
* Poll cadence for the content query while the post-stream reconcile waits for a fetch showing the
diff --git a/apps/sim/app/workspace/[workspaceId]/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/files/files.tsx
index c6bcecde52d..051a807f233 100644
--- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx
@@ -27,7 +27,7 @@ import { usePostHog } from 'posthog-js/react'
import { getDocumentIcon } from '@/components/icons/document-icons'
import { useLimitUpgradeToast } from '@/lib/billing/client'
import { captureEvent } from '@/lib/posthog/client'
-import { triggerFileDownload } from '@/lib/uploads/client/download'
+import { triggerArchiveDownload, triggerFileDownload } from '@/lib/uploads/client/download'
import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace'
import { MAX_WORKSPACE_FILE_SIZE } from '@/lib/uploads/shared/types'
import {
@@ -951,6 +951,7 @@ export function Files() {
})
} catch (err) {
logger.error('Failed to download file:', err)
+ toast.error(getErrorMessage(err, `Failed to download "${file.name}"`))
}
},
[workspaceId]
@@ -1071,25 +1072,44 @@ export function Files() {
setShowDeleteConfirm(true)
}, [selectedFileIds, selectedFolderIds, files, folders])
- const handleBulkDownload = useCallback(() => {
+ const [isDownloadingArchive, setIsDownloadingArchive] = useState(false)
+ // Ref as well as state: two clicks in the same tick would both pass a state check,
+ // and each concurrent archive holds the whole zip in tab memory.
+ const archiveDownloadInFlightRef = useRef(false)
+
+ const downloadArchive = useCallback(
+ async (selection: { fileIds?: string[]; folderIds?: string[] }) => {
+ if (archiveDownloadInFlightRef.current) return
+ archiveDownloadInFlightRef.current = true
+ setIsDownloadingArchive(true)
+ try {
+ await triggerArchiveDownload({ workspaceId, ...selection })
+ } catch (err) {
+ logger.error('Failed to download selection:', err)
+ toast.error(getErrorMessage(err, 'Failed to download the selected files'))
+ } finally {
+ archiveDownloadInFlightRef.current = false
+ setIsDownloadingArchive(false)
+ }
+ },
+ [workspaceId]
+ )
+
+ const handleBulkDownload = useCallback(async () => {
const selectedFiles = files.filter((file) => selectedFileIds.includes(file.id))
if (selectedFiles.length === 1 && selectedFolderIds.length === 0) {
handleDownload(selectedFiles[0])
return
}
- const query = new URLSearchParams()
- for (const fileId of selectedFileIds) query.append('fileIds', fileId)
- for (const folderId of selectedFolderIds) query.append('folderIds', folderId)
-
- if (query.size === 0) return
+ if (selectedFileIds.length === 0 && selectedFolderIds.length === 0) return
captureEvent(posthogRef.current, 'file_downloaded', {
workspace_id: workspaceId,
is_bulk: true,
file_count: selectedFileIds.length + selectedFolderIds.length,
})
- window.location.href = `/api/workspaces/${workspaceId}/files/download?${query.toString()}`
- }, [selectedFileIds, selectedFolderIds, files, handleDownload, workspaceId])
+ await downloadArchive({ fileIds: selectedFileIds, folderIds: selectedFolderIds })
+ }, [selectedFileIds, selectedFolderIds, files, handleDownload, downloadArchive, workspaceId])
const fileDetailBreadcrumbs = useMemo(() => {
if (!selectedFile) return []
@@ -1280,18 +1300,19 @@ export function Files() {
if (!item) return
const rowId = item.kind === 'file' ? fileRowId(item.file.id) : folderRowId(item.folder.id)
if (selectedRowIds.has(rowId) && selectedRowIds.size > 1) {
- handleBulkDownload()
+ void handleBulkDownload()
closeContextMenu()
return
}
if (item.kind === 'folder') {
- window.location.href = `/api/workspaces/${workspaceId}/files/download?folderIds=${encodeURIComponent(item.folder.id)}`
+ const folderId = item.folder.id
closeContextMenu()
+ void downloadArchive({ folderIds: [folderId] })
return
}
handleDownload(item.file)
closeContextMenu()
- }, [selectedRowIds, handleBulkDownload, closeContextMenu, workspaceId, handleDownload])
+ }, [selectedRowIds, handleBulkDownload, closeContextMenu, downloadArchive, handleDownload])
const handleContextMenuRename = useCallback(() => {
const item = contextMenuItemRef.current
@@ -1981,7 +2002,9 @@ export function Files() {
onMove={canEdit ? handleContextMenuMove : undefined}
moveOptions={canEdit ? contextMenuMoveOptions : undefined}
onDelete={canEdit ? handleBulkDelete : undefined}
- isLoading={bulkArchiveItems.isPending || moveItems.isPending}
+ isLoading={
+ bulkArchiveItems.isPending || moveItems.isPending || isDownloadingArchive
+ }
/>
{isDraggingOver ? (
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/activity-log/activity-log.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/activity-log/activity-log.tsx
index 12a3adccf4d..32f9d5dd075 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/components/activity-log/activity-log.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/settings/components/activity-log/activity-log.tsx
@@ -32,6 +32,8 @@ const EVENT_COLUMN_WIDTH_CLASS = {
type EventColumnWidth = keyof typeof EVENT_COLUMN_WIDTH_CLASS
+const ROW_CLASS = 'flex w-full items-center gap-3 px-3 py-2 text-left'
+
function ActivityLogRow({
entry,
eventColumn,
@@ -42,6 +44,39 @@ function ActivityLogRow({
const [expanded, setExpanded] = useState(false)
const expandable = entry.details != null
+ const cells = (
+ <>
+
+ {entry.timestamp}
+
+
+ {entry.event}
+
+
+ {typeof entry.description === 'string' ? (
+
+ ) : (
+ entry.description
+ )}
+
+
+ {typeof entry.actor === 'string' ? (
+
+ ) : (
+ {entry.actor}
+ )}
+ {expandable && (
+
+ )}
+
+ >
+ )
+
return (
-
expandable && setExpanded(!expanded)}
- disabled={!expandable}
- >
-
- {entry.timestamp}
-
-
- {entry.event}
-
-
- {typeof entry.description === 'string' ? (
-
- ) : (
- entry.description
- )}
-
-
- {typeof entry.actor === 'string' ? (
-
- ) : (
- {entry.actor}
- )}
- {expandable && (
-
- )}
-
-
+ {expandable ? (
+
setExpanded(!expanded)}
+ >
+ {cells}
+
+ ) : (
+ // A row with nothing to expand is inert content, not a disabled control:
+ // browsers suppress pointer events over a disabled button AND its
+ // descendants, which would swallow the hover tooltips inside the cells.
+
{cells}
+ )}
{expandable && expanded && (
diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/lock-copy.ts b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/lock-copy.ts
index f87a5bdbcd1..6f559998503 100644
--- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/lock-copy.ts
+++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/lock-copy.ts
@@ -1,6 +1,6 @@
/**
- * Single source of truth for lock vocabulary shared by the lock settings modal,
- * the blocked-action toast, and the table header chip. Kept out of
+ * Single source of truth for lock vocabulary shared by the lock settings modal
+ * and the lock toasts (the on-open announcement and blocked actions). Kept out of
* `lib/table/mutation-locks.ts` — that module is server-tainted (importing it
* from a client component pulls `next/headers` into the browser bundle).
*/
@@ -76,8 +76,8 @@ export function describeLocks(locks: TableLocks): { name: string; detail: string
/**
* Why a locked-table notice was raised. `'status'` is the informational case
- * (a non-admin clicking the header lock chip); the rest are actions the user
- * just tried and couldn't do.
+ * (the announcement shown once when a locked table is opened); the rest are
+ * actions the user just tried and couldn't do.
*/
export type BlockedTableAction = 'add-row' | 'add-column' | 'delete-column' | 'edit-cell' | 'status'
diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx
index 1c0daa5fed7..a76f01abab5 100644
--- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx
@@ -1,6 +1,6 @@
'use client'
-import { useCallback, useMemo, useReducer, useRef, useState } from 'react'
+import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'
import { Chip, ChipConfirmModal, toast } from '@sim/emcn'
import { Download, Lock, Pencil, Table as TableIcon, Trash, Upload } from '@sim/emcn/icons'
import { createLogger } from '@sim/logger'
@@ -61,12 +61,7 @@ import {
import { COLUMN_SIDEBAR_WIDTH } from './components/table-grid/constants'
import { COLUMN_TYPE_ICONS } from './components/table-grid/headers'
import { useTable, useTableEventStream } from './hooks'
-import {
- type BlockedTableAction,
- describeBlockedAction,
- describeLocks,
- lockedNouns,
-} from './lock-copy'
+import { type BlockedTableAction, describeBlockedAction, lockedNouns } from './lock-copy'
import {
DEFAULT_TABLE_DETAIL_SORT_DIRECTION,
tableDetailParsers,
@@ -627,7 +622,10 @@ export function Table({
if (!tableData) return
if (blockedToastIdRef.current) toast.dismiss(blockedToastIdRef.current)
const { title, text } = describeBlockedAction(action, tableData.locks)
- blockedToastIdRef.current = toast.warning(title, {
+ // 'status' is the on-open announcement — nothing was refused, so it reads
+ // as information rather than a warning.
+ const notify = action === 'status' ? toast.info : toast.warning
+ blockedToastIdRef.current = notify(title, {
description: text,
...(canOpenLockSettings
? {
@@ -641,23 +639,48 @@ export function Table({
[tableData, canOpenLockSettings]
)
+ // Announce the lock state once per table on open. Unlike the re-rendering
+ // permission gates, this fires once and can't self-correct, so it waits for
+ // `canAdmin` to settle instead of treating loading as permitted.
+ const announcedLockTableIdRef = useRef
(null)
+ useEffect(() => {
+ if (!tableData || userPermissions.isLoading) return
+ if (announcedLockTableIdRef.current === tableData.id) return
+ announcedLockTableIdRef.current = tableData.id
+ if (lockedNouns(tableData.locks).length === 0) return
+ showBlockedToast('status')
+ }, [tableData, userPermissions.isLoading, showBlockedToast])
+
+ // A notice must not outlive the table it describes — its action targets
+ // whichever table is current. Keyed on `tableId` so an embedded swap that
+ // changes the prop without a route change is covered too. Leaving ends the
+ // visit, so the latch resets and coming back announces again.
+ useEffect(
+ () => () => {
+ announcedLockTableIdRef.current = null
+ if (!blockedToastIdRef.current) return
+ toast.dismiss(blockedToastIdRef.current)
+ blockedToastIdRef.current = null
+ },
+ [tableId]
+ )
+
+ // A toast's action is captured when it is created, so a viewer who loses
+ // admin access mid-toast would keep a Lock settings button that opens
+ // nothing. Dismiss on that transition only — a viewer who never had access
+ // has a legitimate action-less notice that must survive.
+ const couldOpenLockSettingsRef = useRef(canOpenLockSettings)
+ useEffect(() => {
+ const lostAccess = couldOpenLockSettingsRef.current && !canOpenLockSettings
+ couldOpenLockSettingsRef.current = canOpenLockSettings
+ if (!lostAccess || !blockedToastIdRef.current) return
+ toast.dismiss(blockedToastIdRef.current)
+ blockedToastIdRef.current = null
+ }, [canOpenLockSettings])
+
const headerActions = useMemo(() => {
if (!tableData) return undefined
- // Header space is for state, not for settings: the chip appears only once
- // something is actually locked, and names the mode so it reads at a glance.
- // Reaching the panel on an unlocked table is the dropdown's job.
- const anyLocked = lockedNouns(tableData.locks).length > 0
return [
- ...(anyLocked
- ? [
- {
- label: describeLocks(tableData.locks).name,
- icon: Lock,
- onClick: () =>
- userPermissions.canAdmin ? setShowLockSettings(true) : showBlockedToast('status'),
- },
- ]
- : []),
{
label: 'Import CSV',
icon: Upload,
@@ -673,14 +696,7 @@ export function Table({
disabled: tableData.rowCount === 0,
},
]
- }, [
- tableData,
- userPermissions.canEdit,
- userPermissions.canAdmin,
- handleExportCsv,
- onRequestImportCsv,
- showBlockedToast,
- ])
+ }, [tableData, userPermissions.canEdit, handleExportCsv, onRequestImportCsv])
// Adding a column is a schema change. The trigger stays visible when the
// table is schema-locked and explains itself instead of disappearing.
diff --git a/apps/sim/background/cleanup-logs.ts b/apps/sim/background/cleanup-logs.ts
index 343e388ed2b..2ee922fb7e0 100644
--- a/apps/sim/background/cleanup-logs.ts
+++ b/apps/sim/background/cleanup-logs.ts
@@ -256,7 +256,7 @@ async function cleanupLegacyLargeExecutionValues(
SELECT 1
FROM ${pausedExecutions} AS ref_pe
WHERE ref_pe.execution_id = ref.execution_id
- AND ref_pe.status = ANY(${LIVE_PAUSED_REFERENCE_STATUSES}::text[])
+ AND ref_pe.status IN ${LIVE_PAUSED_REFERENCE_STATUSES}
)
)
)
@@ -279,7 +279,7 @@ async function cleanupLegacyLargeExecutionValues(
SELECT 1
FROM ${pausedExecutions} AS parent_owner_pe
WHERE parent_owner_pe.execution_id = parent_value.owner_execution_id
- AND parent_owner_pe.status = ANY(${LIVE_PAUSED_REFERENCE_STATUSES}::text[])
+ AND parent_owner_pe.status IN ${LIVE_PAUSED_REFERENCE_STATUSES}
)
OR EXISTS (
SELECT 1
@@ -300,7 +300,7 @@ async function cleanupLegacyLargeExecutionValues(
SELECT 1
FROM ${pausedExecutions} AS parent_ref_pe
WHERE parent_ref_pe.execution_id = parent_ref.execution_id
- AND parent_ref_pe.status = ANY(${LIVE_PAUSED_REFERENCE_STATUSES}::text[])
+ AND parent_ref_pe.status IN ${LIVE_PAUSED_REFERENCE_STATUSES}
)
)
)
@@ -316,7 +316,7 @@ async function cleanupLegacyLargeExecutionValues(
SELECT 1
FROM ${pausedExecutions} AS pe
WHERE pe.execution_id = split_part(${workspaceFiles.key}, '/', 4)
- AND pe.status = ANY(${LIVE_PAUSED_REFERENCE_STATUSES}::text[])
+ AND pe.status IN ${LIVE_PAUSED_REFERENCE_STATUSES}
)`
)
)
diff --git a/apps/sim/background/fork-content-copy.ts b/apps/sim/background/fork-content-copy.ts
index 7391b78c2c4..8b721cecc62 100644
--- a/apps/sim/background/fork-content-copy.ts
+++ b/apps/sim/background/fork-content-copy.ts
@@ -11,9 +11,15 @@ import {
* non-transactional best-effort (per-row inserts with fresh ids), so a blind
* re-run would duplicate rows; a partial failure simply leaves the fork's content
* incomplete (the workflows themselves committed synchronously).
+ *
+ * Runs on `large-2x` (8 vCPU / 16 GB), matching `knowledge-connector-sync`: the
+ * copy materializes table rows, KB chunks and their embedding vectors, and file
+ * blobs in memory, and `maxAttempts: 1` means an OOM is unrecoverable — a
+ * half-copied fork with no retry.
*/
export const forkContentCopyTask = task({
id: 'fork-content-copy',
+ machine: 'large-2x',
retry: { maxAttempts: 1 },
queue: {
name: 'fork-content-copy',
diff --git a/apps/sim/background/knowledge-connector-sync.ts b/apps/sim/background/knowledge-connector-sync.ts
index f92c440a146..ee17426f2bc 100644
--- a/apps/sim/background/knowledge-connector-sync.ts
+++ b/apps/sim/background/knowledge-connector-sync.ts
@@ -40,7 +40,7 @@ export async function executeConnectorSyncJob(payload: unknown) {
export const knowledgeConnectorSync = task({
id: 'knowledge-connector-sync',
maxDuration: 1800,
- machine: 'large-1x',
+ machine: 'large-2x',
retry: {
maxAttempts: 3,
factor: 2,
diff --git a/apps/sim/background/schedule-execution.ts b/apps/sim/background/schedule-execution.ts
index da02f0ebfaa..d7f0aae3643 100644
--- a/apps/sim/background/schedule-execution.ts
+++ b/apps/sim/background/schedule-execution.ts
@@ -1457,7 +1457,7 @@ export async function executeJobInline(payload: JobExecutionPayload) {
export const scheduleExecutionTaskOptions = {
id: 'schedule-execution',
- machine: 'medium-1x' as const,
+ machine: 'medium-2x' as const,
retry: {
maxAttempts: 1,
},
diff --git a/apps/sim/background/workflow-execution.ts b/apps/sim/background/workflow-execution.ts
index 1be362811d2..cce7d55dd3e 100644
--- a/apps/sim/background/workflow-execution.ts
+++ b/apps/sim/background/workflow-execution.ts
@@ -230,7 +230,7 @@ export async function executeWorkflowJob(payload: WorkflowExecutionPayload) {
export const workflowExecutionTask = task({
id: 'workflow-execution',
- machine: 'medium-1x',
+ machine: 'medium-2x',
queue: {
concurrencyLimit: WORKFLOW_EXECUTION_CONCURRENCY_LIMIT,
},
diff --git a/apps/sim/blocks/blocks/guardrails.ts b/apps/sim/blocks/blocks/guardrails.ts
index 1b893243951..6b857f690f8 100644
--- a/apps/sim/blocks/blocks/guardrails.ts
+++ b/apps/sim/blocks/blocks/guardrails.ts
@@ -76,7 +76,11 @@ export const GuardrailsBlock: BlockConfig = {
enabled: true,
prompt: `Generate a regular expression pattern based on the user's description.
The regex should be:
-- Valid JavaScript regex syntax
+- Valid RE2 syntax: no lookahead ((?=...), (?!...)), no lookbehind ((?<=...), (? {
expect(container.textContent).toMatch(/Invalid regex/)
})
- it('shows an inline error for a catastrophic-backtracking pattern', () => {
- renderEditor([row('(a+)+$')], vi.fn())
- expect(container.textContent).toMatch(/potentially unsafe/)
- })
+ it.each(['(a+)+$', '(?<=id: )\\w+', '(?:https?://)?example\\.com'])(
+ 'accepts %s, which the removed safe-regex2 screen rejected',
+ (pattern) => {
+ // The editor no longer runs a catastrophic-backtracking screen. It caught
+ // `(a+)+$` but passed `a*a*b`, so it deterred only the obvious spelling
+ // while blocking valid Presidio patterns like lookbehind. These patterns
+ // run in Presidio, not in this process.
+ renderEditor([row(pattern)], vi.fn())
+ expect(container.textContent).not.toMatch(/potentially unsafe/)
+ expect(container.textContent).not.toMatch(/Invalid regex/)
+ }
+ )
it('appends an empty row when "Add pattern" is clicked', () => {
const onChange = vi.fn()
diff --git a/apps/sim/components/pii/custom-patterns-editor.tsx b/apps/sim/components/pii/custom-patterns-editor.tsx
index 23ba59f658e..4be1204f5b0 100644
--- a/apps/sim/components/pii/custom-patterns-editor.tsx
+++ b/apps/sim/components/pii/custom-patterns-editor.tsx
@@ -15,9 +15,13 @@ interface CustomPatternsEditorProps {
/**
* Editor for user-supplied custom regex patterns. Each row is a name label, the
- * regex (validated inline for syntax + catastrophic-backtracking safety), and the
- * verbatim replacement token that matches are redacted to. Shared by the Data
- * Retention settings and any other PII-policy surface.
+ * regex (validated inline for syntax only), and the verbatim replacement token
+ * that matches are redacted to. Shared by the Data Retention settings and any
+ * other PII-policy surface.
+ *
+ * Syntax is the only check because these patterns execute in Presidio, not in
+ * this process — see `validateRegexPattern` for why a backtracking screen here
+ * was removed rather than kept.
*/
export function CustomPatternsEditor({ patterns, onChange }: CustomPatternsEditorProps) {
function updateRow(index: number, patch: Partial) {
diff --git a/apps/sim/ee/workspace-forking/components/fork-activity-panel/fork-activity-panel.test.tsx b/apps/sim/ee/workspace-forking/components/fork-activity-panel/fork-activity-panel.test.tsx
new file mode 100644
index 00000000000..af633e4e6cd
--- /dev/null
+++ b/apps/sim/ee/workspace-forking/components/fork-activity-panel/fork-activity-panel.test.tsx
@@ -0,0 +1,171 @@
+/**
+ * @vitest-environment jsdom
+ */
+import { act } from 'react'
+import { createRoot, type Root } from 'react-dom/client'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import type { BackgroundWorkItem } from '@/lib/api/contracts/workspace-fork'
+
+const { mockUseWorkspaceBackgroundWork } = vi.hoisted(() => ({
+ mockUseWorkspaceBackgroundWork: vi.fn(),
+}))
+
+vi.mock('@/ee/workspace-forking/hooks/background-work', () => ({
+ useWorkspaceBackgroundWork: mockUseWorkspaceBackgroundWork,
+}))
+
+vi.mock('@/app/workspace/[workspaceId]/settings/components/settings-empty-state', () => ({
+ SettingsEmptyState: ({ children }: { children: React.ReactNode }) => {children}
,
+}))
+
+vi.mock('@/app/workspace/[workspaceId]/components', () => ({
+ FloatingOverflowText: ({ label, className }: { label: string; className?: string }) => (
+ {label}
+ ),
+}))
+
+import { ForkActivityPanel } from '@/ee/workspace-forking/components/fork-activity-panel/fork-activity-panel'
+
+const WORKSPACE_ID = 'ws-1'
+const PARTNER_ID = 'ws-parent'
+const WORKSPACE_NAMES = new Map([[PARTNER_ID, 'another workspace']])
+
+function makeJob(overrides: Partial = {}): BackgroundWorkItem {
+ return {
+ id: 'job-1',
+ workspaceId: PARTNER_ID,
+ workflowId: null,
+ kind: 'fork_content_copy',
+ status: 'completed',
+ message: null,
+ error: null,
+ metadata: { childWorkspaceId: WORKSPACE_ID, actorName: 'Brandon Tarr' },
+ startedAt: '2026-07-28T15:58:00.000Z',
+ completedAt: null,
+ ...overrides,
+ } as BackgroundWorkItem
+}
+
+let container: HTMLDivElement
+let root: Root
+
+function renderJobs(jobs: BackgroundWorkItem[]) {
+ mockUseWorkspaceBackgroundWork.mockReturnValue({
+ data: { pages: [{ items: jobs, nextCursor: null }] },
+ isPending: false,
+ isError: false,
+ hasNextPage: false,
+ fetchNextPage: vi.fn(),
+ isFetchingNextPage: false,
+ })
+ act(() => {
+ root.render( )
+ })
+}
+
+/** The Event-column badge for the single rendered row (`Badge`'s base class). */
+function badgeElement(): HTMLElement {
+ const badge = container.querySelector('.inline-flex')
+ if (!badge) throw new Error('badge not found')
+ return badge
+}
+
+/** Hover the badge the way React sees it — `onPointerEnter` is delegated from `pointerover`. */
+function hover(element: HTMLElement) {
+ act(() => {
+ element.dispatchEvent(
+ new MouseEvent('pointerover', { bubbles: true, clientX: 100, clientY: 100 })
+ )
+ })
+}
+
+function tooltipText(): string | null {
+ return document.body.querySelector('[role="tooltip"]')?.textContent ?? null
+}
+
+describe('ForkActivityPanel event badge tooltip', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ container = document.createElement('div')
+ document.body.appendChild(container)
+ root = createRoot(container)
+ })
+
+ afterEach(() => {
+ act(() => root.unmount())
+ container.remove()
+ })
+
+ it('shows the failure reason when hovering a failed (red) badge', () => {
+ renderJobs([makeJob({ status: 'failed', error: 'Storage quota exceeded while copying files' })])
+
+ expect(tooltipText()).toBeNull()
+ hover(badgeElement())
+ expect(tooltipText()).toBe('Storage quota exceeded while copying files')
+ })
+
+ it('falls back to a generic label when a failed row carries no error text', () => {
+ renderJobs([makeJob({ status: 'failed', error: null })])
+
+ hover(badgeElement())
+ expect(tooltipText()).toBe('Failed')
+ })
+
+ it('truncates a very long failure reason', () => {
+ renderJobs([makeJob({ status: 'failed', error: 'x'.repeat(500) })])
+
+ hover(badgeElement())
+ const text = tooltipText() ?? ''
+ expect(text.endsWith('...')).toBe(true)
+ expect(text.length).toBeLessThan(260)
+ })
+
+ it('says "In progress" when hovering a processing (grey) badge', () => {
+ renderJobs([makeJob({ status: 'processing' })])
+
+ hover(badgeElement())
+ expect(tooltipText()).toBe('In progress')
+ })
+
+ it('says "Queued" when hovering a pending (grey) badge', () => {
+ renderJobs([makeJob({ status: 'pending' })])
+
+ hover(badgeElement())
+ expect(tooltipText()).toBe('Queued')
+ })
+
+ it('shows nothing when hovering a completed (blue) badge', () => {
+ renderJobs([makeJob({ status: 'completed' })])
+
+ hover(badgeElement())
+ expect(tooltipText()).toBeNull()
+ })
+
+ it('surfaces the partial-copy summary on a completed_with_warnings (amber) badge', () => {
+ renderJobs([
+ makeJob({
+ status: 'completed_with_warnings',
+ message: 'Copied 12 items; 3 could not be copied',
+ }),
+ ])
+
+ hover(badgeElement())
+ expect(tooltipText()).toBe('Copied 12 items; 3 could not be copied')
+ })
+
+ it('keeps a still-running row hoverable by not rendering it as a disabled button', () => {
+ renderJobs([makeJob({ status: 'processing' })])
+
+ // A processing row has no report yet, so it is not expandable. It must still
+ // be a plain row — a disabled button would swallow the badge's hover events.
+ expect(container.querySelector('button[disabled]')).toBeNull()
+ })
+
+ it('still renders an expandable row as a button', () => {
+ renderJobs([makeJob({ status: 'failed', error: 'boom' })])
+
+ const row = container.querySelector('button')
+ expect(row).not.toBeNull()
+ expect(row?.getAttribute('aria-expanded')).toBe('false')
+ })
+})
diff --git a/apps/sim/ee/workspace-forking/components/fork-activity-panel/fork-activity-panel.tsx b/apps/sim/ee/workspace-forking/components/fork-activity-panel/fork-activity-panel.tsx
index 81faf69ef2c..d0d5d7f4ceb 100644
--- a/apps/sim/ee/workspace-forking/components/fork-activity-panel/fork-activity-panel.tsx
+++ b/apps/sim/ee/workspace-forking/components/fork-activity-panel/fork-activity-panel.tsx
@@ -1,9 +1,10 @@
'use client'
import { useCallback, useMemo } from 'react'
-import { Badge, Button } from '@sim/emcn'
+import { Badge, Button, Tooltip } from '@sim/emcn'
import { createLogger } from '@sim/logger'
import { formatDateTime } from '@sim/utils/formatting'
+import { truncate } from '@sim/utils/string'
import type { BackgroundWorkItem } from '@/lib/api/contracts/workspace-fork'
import {
ActivityLog,
@@ -14,6 +15,12 @@ import { useWorkspaceBackgroundWork } from '@/ee/workspace-forking/hooks/backgro
const logger = createLogger('ForkActivityPanel')
+/**
+ * Errors can carry a full driver message; the badge tooltip is a glance-level
+ * summary, so cap it. The untruncated text stays in the expanded detail box.
+ */
+const TOOLTIP_MAX_LENGTH = 240
+
const plural = (n: number, noun: string) => `${n} ${noun}${n === 1 ? '' : 's'}`
/** Join "N verb" segments (verbs like "updated" aren't pluralized), dropping zero counts. */
@@ -122,6 +129,27 @@ function jobBadgeVariant(job: BackgroundWorkItem) {
}
}
+/**
+ * Hover text for the Event badge, explaining what its color means. Successful rows
+ * (the per-operation colors) return null — the color already says "done", and the
+ * breakdown is one click away in the expanded row — so only the states a reader
+ * can't act on from color alone get a tooltip.
+ */
+function jobStatusTooltip(job: BackgroundWorkItem): string | null {
+ switch (job.status) {
+ case 'pending':
+ return 'Queued'
+ case 'processing':
+ return 'In progress'
+ case 'failed':
+ return truncate(job.error ?? 'Failed', TOOLTIP_MAX_LENGTH)
+ case 'completed_with_warnings':
+ return truncate(job.message ?? 'Completed with warnings', TOOLTIP_MAX_LENGTH)
+ default:
+ return null
+ }
+}
+
/** Build a job's report (named groups + plain notes) from its metadata. */
function jobReport(job: BackgroundWorkItem): JobReport {
const m = job.metadata
@@ -239,13 +267,24 @@ function jobDetails(job: BackgroundWorkItem, report: JobReport) {
function toActivityEntry(job: BackgroundWorkItem, view: ActivityView): ActivityLogEntry {
const report = jobReport(job)
const hasDetails = report.groups.length > 0 || report.notes.length > 0 || Boolean(job.error)
+ const tooltip = jobStatusTooltip(job)
+ const badge = (
+
+ {jobEventLabel(job)}
+
+ )
return {
id: job.id,
timestamp: formatDateTime(new Date(job.startedAt)),
- event: (
-
- {jobEventLabel(job)}
-
+ event: tooltip ? (
+
+ {badge}
+
+ {tooltip}
+
+
+ ) : (
+ badge
),
description: jobTitle(job, view),
actor: job.metadata?.actorName || 'System',
diff --git a/apps/sim/lib/api/contracts/primitives.test.ts b/apps/sim/lib/api/contracts/primitives.test.ts
index 82e5e425140..727a42a65f1 100644
--- a/apps/sim/lib/api/contracts/primitives.test.ts
+++ b/apps/sim/lib/api/contracts/primitives.test.ts
@@ -35,10 +35,22 @@ describe('customPatternSchema', () => {
}
})
- it('rejects a catastrophic-backtracking regex at the boundary', () => {
- expect(
- customPatternSchema.safeParse({ name: 'evil', regex: '(a+)+$', replacement: '' }).success
- ).toBe(false)
+ it('no longer screens for catastrophic backtracking, which never worked here', () => {
+ // This used to reject `(a+)+$` via `safe-regex2`. The screen was removed:
+ // it caught that shape but passed `a*a*b`, which is just as catastrophic,
+ // so it only ever deterred the obvious spelling of a misconfiguration a
+ // user can inflict on their own workspace. It also rejected valid patterns
+ // (lookbehind, optional groups) that Presidio accepts.
+ //
+ // These patterns run in Presidio, not in this process, so they cannot
+ // stall this event loop; Presidio's own timeout is the bound. In-process
+ // matching uses `compileLinearRegex`, which cannot backtrack at all.
+ for (const regex of ['(a+)+$', 'a*a*b', '(?<=id: )\\w+']) {
+ expect(
+ customPatternSchema.safeParse({ name: 'p', regex, replacement: '' }).success,
+ `pattern ${regex} should be accepted`
+ ).toBe(true)
+ }
})
})
diff --git a/apps/sim/lib/api/contracts/v1/workflows.ts b/apps/sim/lib/api/contracts/v1/workflows.ts
index d0c1469e189..aa5a78c901d 100644
--- a/apps/sim/lib/api/contracts/v1/workflows.ts
+++ b/apps/sim/lib/api/contracts/v1/workflows.ts
@@ -4,9 +4,9 @@ import {
deploymentOperationSummarySchema,
deploymentVersionMetadataFieldsSchema,
} from '@/lib/api/contracts/deployments'
-import { booleanQueryFlagSchema } from '@/lib/api/contracts/primitives'
+import { booleanQueryFlagSchema, workspaceIdSchema } from '@/lib/api/contracts/primitives'
import { defineRouteContract } from '@/lib/api/contracts/types'
-import { workflowIdParamsSchema } from '@/lib/api/contracts/workflows'
+import { workflowIdParamsSchema, workflowStateSchema } from '@/lib/api/contracts/workflows'
export const v1ListWorkflowsQuerySchema = z.object({
workspaceId: z.string().min(1),
@@ -158,3 +158,144 @@ export const v1RollbackWorkflowContract = defineRouteContract({
schema: withV1Limits(v1RollbackWorkflowDataSchema),
},
})
+
+/** Workflow variable as carried inside an export payload's `state.variables`. */
+const v1WorkflowExportVariableSchema = z.object({
+ id: z.string(),
+ name: z.string(),
+ type: z.enum(['string', 'number', 'boolean', 'object', 'array', 'plain']),
+ value: z.unknown(),
+})
+
+/**
+ * Workflow state emitted by the public export endpoint.
+ *
+ * Structurally mirrors the admin export state (`adminWorkflowExportStateSchema`
+ * in `@/lib/api/contracts/admin`) so both payloads round-trip through the same
+ * importer. The surfaces are deliberately kept as separate schemas because the
+ * data differs: public exports are secret-sanitized via `sanitizeForExport`
+ * (credentials stripped, `{{ENV_VAR}}` references preserved), while admin
+ * exports are raw for backup/restore.
+ */
+export const v1WorkflowExportStateSchema = workflowStateSchema
+ /**
+ * `sanitizeForExport` builds its payload from `{blocks, edges, loops,
+ * parallels, metadata, variables}` only, so these three are structurally
+ * unreachable on this wire. `deployedAt` in particular is a `z.coerce.date()`
+ * whose output type is a `Date` — never a valid JSON value — so leaving it
+ * inherited would put an unrepresentable type in a response contract.
+ */
+ .omit({ lastSaved: true, isDeployed: true, deployedAt: true })
+ .extend({
+ metadata: z
+ .object({
+ name: z.string().optional(),
+ description: z.string().optional(),
+ sortOrder: z.number().optional(),
+ exportedAt: z.string().optional(),
+ })
+ .optional(),
+ variables: z.record(z.string(), v1WorkflowExportVariableSchema).optional(),
+ })
+
+export const v1WorkflowExportPayloadSchema = z.object({
+ version: z.literal('1.0'),
+ exportedAt: z.string(),
+ workflow: z.object({
+ id: z.string(),
+ name: z.string(),
+ description: z.string().nullable(),
+ workspaceId: z.string().nullable(),
+ folderId: z.string().nullable(),
+ }),
+ state: v1WorkflowExportStateSchema,
+})
+
+export type V1WorkflowExportPayload = z.output
+
+export const v1ExportWorkflowContract = defineRouteContract({
+ method: 'GET',
+ path: '/api/v1/workflows/[id]/export',
+ params: workflowIdParamsSchema,
+ response: {
+ mode: 'json',
+ schema: withV1Limits(v1WorkflowExportPayloadSchema),
+ },
+})
+
+/**
+ * Upper bound on an imported workflow name. `workflow.name` is an unbounded
+ * `text` column, so this is a product limit rather than a storage one — it
+ * keeps a name renderable in the sidebar and leaves headroom for the
+ * deduplication suffix appended on insert. Exported because the route applies
+ * the same bound to names read out of the payload, so the declared limit is
+ * also the effective one.
+ */
+export const V1_IMPORT_NAME_MAX_LENGTH = 200
+
+/** Upper bound on an imported workflow description. See above. */
+export const V1_IMPORT_DESCRIPTION_MAX_LENGTH = 2000
+
+/**
+ * Import request body. `workflow` accepts either the envelope emitted by
+ * {@link v1ExportWorkflowContract} (`{ version, exportedAt, workflow, state }`),
+ * a bare workflow state (`{ blocks, edges, ... }`), or a JSON string of either
+ * — the same three forms the admin importer accepts, so payloads are portable
+ * between the two surfaces. `name` and `description` override whatever the
+ * payload's own metadata carries.
+ */
+export const v1ImportWorkflowBodySchema = z.object({
+ workspaceId: workspaceIdSchema,
+ folderId: z.string().min(1, 'folderId cannot be empty').optional(),
+ name: z
+ .string()
+ .min(1, 'name cannot be empty')
+ .max(V1_IMPORT_NAME_MAX_LENGTH, `name must be at most ${V1_IMPORT_NAME_MAX_LENGTH} characters`)
+ .optional(),
+ description: z
+ .string()
+ .max(
+ V1_IMPORT_DESCRIPTION_MAX_LENGTH,
+ `description must be at most ${V1_IMPORT_DESCRIPTION_MAX_LENGTH} characters`
+ )
+ .optional(),
+ workflow: z.union(
+ [
+ z.string().min(1, 'workflow cannot be empty'),
+ z.record(z.string(), z.unknown()).refine((value) => Object.keys(value).length > 0, {
+ error: 'workflow cannot be empty',
+ }),
+ ],
+ { error: 'workflow must be a workflow export object or a JSON string' }
+ ),
+})
+
+export type V1ImportWorkflowBody = z.input
+
+/**
+ * Created workflow, shaped as the subset of the v1 workflow resource that is
+ * knowable at import time (an imported workflow is never deployed and has no
+ * run history), so callers can feed it straight back into
+ * `GET /api/v1/workflows/{id}`.
+ */
+export const v1ImportWorkflowDataSchema = z.object({
+ id: z.string(),
+ name: z.string(),
+ description: z.string().nullable(),
+ workspaceId: z.string(),
+ folderId: z.string().nullable(),
+ createdAt: z.string(),
+ updatedAt: z.string(),
+})
+
+export type V1ImportWorkflowData = z.output
+
+export const v1ImportWorkflowContract = defineRouteContract({
+ method: 'POST',
+ path: '/api/v1/workflows/import',
+ body: v1ImportWorkflowBodySchema,
+ response: {
+ mode: 'json',
+ schema: withV1Limits(v1ImportWorkflowDataSchema),
+ },
+})
diff --git a/apps/sim/lib/audio/extractor.ts b/apps/sim/lib/audio/extractor.ts
index f7e722bb578..47f461fc46b 100644
--- a/apps/sim/lib/audio/extractor.ts
+++ b/apps/sim/lib/audio/extractor.ts
@@ -1,10 +1,8 @@
import { execSync } from 'node:child_process'
-import fsSync from 'node:fs'
import fs from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import { createLogger } from '@sim/logger'
-import ffmpegStatic from 'ffmpeg-static'
import ffmpeg from 'fluent-ffmpeg'
import type {
AudioExtractionOptions,
@@ -32,19 +30,6 @@ function ensureFfmpeg(): void {
ffmpegInitialized = true
- // Try ffmpeg-static binary
- if (ffmpegStatic && typeof ffmpegStatic === 'string') {
- try {
- fsSync.accessSync(ffmpegStatic, fsSync.constants.X_OK)
- ffmpegPath = ffmpegStatic
- ffmpeg.setFfmpegPath(ffmpegPath)
- logger.info('[FFmpeg] Using ffmpeg-static:', ffmpegPath)
- return
- } catch {
- // Binary doesn't exist or not executable
- }
- }
-
// Try system ffmpeg (cross-platform)
try {
const cmd = process.platform === 'win32' ? 'where ffmpeg' : 'which ffmpeg'
diff --git a/apps/sim/lib/chunkers/regex-chunker.test.ts b/apps/sim/lib/chunkers/regex-chunker.test.ts
index f4f55112e55..96d84cc5ed4 100644
--- a/apps/sim/lib/chunkers/regex-chunker.test.ts
+++ b/apps/sim/lib/chunkers/regex-chunker.test.ts
@@ -387,5 +387,39 @@ describe('RegexChunker', () => {
)
}
})
+ it.concurrent(
+ 'chunks with a catastrophic pattern without hanging',
+ async () => {
+ // `a*a*b` defeats every syntactic backtracking screen. The previous
+ // guard probed it against 'a'.repeat(10000) and measured the elapsed
+ // time only after the match returned, so the guard itself hung for
+ // ~213s. RE2 has no backtracking, so this simply completes.
+ const chunker = new RegexChunker({ pattern: 'a*a*b', chunkSize: 100, chunkOverlap: 0 })
+
+ const start = Date.now()
+ await chunker.chunk(`${'a'.repeat(10000)}!`)
+
+ expect(Date.now() - start).toBeLessThan(2000)
+ },
+ 10000
+ )
+
+ it.concurrent('still supports lookahead split patterns', async () => {
+ // RE2 has no lookaround; compileLookaroundSplit expresses this as slicing
+ // at each match start, so the idiom keeps working on the linear engine.
+ // (No `m` flag is applied, so anchor the lookahead on the delimiter
+ // itself rather than on `^`.)
+ const chunker = new RegexChunker({
+ pattern: '(?=#\\s)',
+ chunkSize: 1024,
+ chunkOverlap: 0,
+ strictBoundaries: true,
+ })
+ const chunks = await chunker.chunk('# One\nalpha\n# Two\nbeta')
+
+ expect(chunks.length).toBe(2)
+ expect(chunks[0].text).toContain('# One')
+ expect(chunks[1].text).toContain('# Two')
+ })
})
})
diff --git a/apps/sim/lib/chunkers/regex-chunker.ts b/apps/sim/lib/chunkers/regex-chunker.ts
index 0cafa47b0d3..b294a504eee 100644
--- a/apps/sim/lib/chunkers/regex-chunker.ts
+++ b/apps/sim/lib/chunkers/regex-chunker.ts
@@ -10,6 +10,11 @@ import {
splitAtWordBoundaries,
tokensToChars,
} from '@/lib/chunkers/utils'
+import {
+ compileLinearRegex,
+ compileLookaroundSplit,
+ type LinearRegex,
+} from '@/lib/core/security/linear-regex'
const logger = createLogger('RegexChunker')
@@ -56,7 +61,7 @@ function toNonCapturing(pattern: string): string {
export class RegexChunker {
private readonly chunkSize: number
private readonly chunkOverlap: number
- private readonly regex: RegExp
+ private readonly regex: LinearRegex
private readonly strictBoundaries: boolean
constructor(options: RegexChunkerOptions) {
@@ -67,7 +72,24 @@ export class RegexChunker {
this.strictBoundaries = options.strictBoundaries ?? false
}
- private compilePattern(pattern: string): RegExp {
+ /**
+ * Compile the caller's split pattern on an engine that cannot backtrack.
+ *
+ * This previously screened for catastrophic backtracking by running the
+ * pattern against six probe strings — including `'a'.repeat(10000)` — and
+ * rejecting anything slower than 50ms. It measured the elapsed time *after*
+ * the match returned, so the screen was the denial of service it existed to
+ * prevent: `a*a*b` against that probe measured 213s on JSC. RE2 removes the
+ * failure mode outright, so the probe is gone rather than repaired.
+ *
+ * Keeping the delimiter — `(?=X)` before a chunk, `(?<=X)` after one — is the
+ * reason a split pattern reaches for lookaround, and `compileLookaroundSplit`
+ * runs both on RE2 without it. Anything else RE2 cannot represent is rejected
+ * rather than run on the built-in engine: no probe can tell a safe pattern
+ * from an unsafe one without running it, which is what made the old guard
+ * hang, so there is nothing to fall back *to*.
+ */
+ private compilePattern(pattern: string): LinearRegex {
if (!pattern) {
throw new Error('Regex pattern is required')
}
@@ -77,34 +99,18 @@ export class RegexChunker {
}
try {
- const regex = new RegExp(toNonCapturing(pattern), 'g')
-
- const testStrings = [
- 'a'.repeat(10000),
- ' '.repeat(10000),
- 'a '.repeat(5000),
- 'aB1 xY2\n'.repeat(1250),
- `${'a'.repeat(30)}!`,
- `${'a b '.repeat(25)}!`,
- ]
- for (const testStr of testStrings) {
- regex.lastIndex = 0
- const start = Date.now()
- regex.test(testStr)
- const elapsed = Date.now() - start
- if (elapsed > 50) {
- throw new Error('Regex pattern appears to have catastrophic backtracking')
- }
- }
-
- regex.lastIndex = 0
- return regex
+ new RegExp(pattern)
} catch (error) {
- if (error instanceof Error && error.message.includes('catastrophic')) {
- throw error
- }
throw new Error(`Invalid regex pattern "${pattern}": ${toError(error).message}`)
}
+
+ const source = toNonCapturing(pattern)
+ const compiled = compileLinearRegex(source) ?? compileLookaroundSplit(source)
+ if (compiled) return compiled
+
+ throw new Error(
+ `Regex pattern "${pattern}" uses syntax that cannot be evaluated safely. Unsupported: negative lookaround ("(?!...)", "(? {
@@ -119,8 +125,7 @@ export class RegexChunker {
return buildChunks([cleaned], 0)
}
- this.regex.lastIndex = 0
- const segments = cleaned.split(this.regex).filter((s) => s.trim().length > 0)
+ const segments = this.regex.split(cleaned).filter((s) => s.trim().length > 0)
if (segments.length <= 1) {
if (this.strictBoundaries) {
diff --git a/apps/sim/lib/copilot/tools/handlers/function-execute.test.ts b/apps/sim/lib/copilot/tools/handlers/function-execute.test.ts
index f6494b14aa0..51dabd8de14 100644
--- a/apps/sim/lib/copilot/tools/handlers/function-execute.test.ts
+++ b/apps/sim/lib/copilot/tools/handlers/function-execute.test.ts
@@ -2,6 +2,7 @@
* @vitest-environment node
*/
import { beforeEach, describe, expect, it, vi } from 'vitest'
+import { PayloadSizeLimitError } from '@/lib/core/utils/stream-limits'
const {
mockIsFeatureEnabled,
@@ -16,6 +17,7 @@ const {
mockListWorkspaceFiles,
mockFindWorkspaceFileRecord,
mockFetchWorkspaceFileBuffer,
+ mockFetchServableWorkspaceFileBuffer,
mockGetSandboxWorkspaceFilePath,
mockListWorkspaceFileFolders,
} = vi.hoisted(() => ({
@@ -31,6 +33,7 @@ const {
mockListWorkspaceFiles: vi.fn(),
mockFindWorkspaceFileRecord: vi.fn(),
mockFetchWorkspaceFileBuffer: vi.fn(),
+ mockFetchServableWorkspaceFileBuffer: vi.fn(),
mockGetSandboxWorkspaceFilePath: vi.fn(),
mockListWorkspaceFileFolders: vi.fn(),
}))
@@ -52,6 +55,7 @@ vi.mock('@/lib/uploads/core/storage-service', () => ({
}))
vi.mock('@/tools', () => ({ executeTool: mockExecuteTool }))
vi.mock('@/lib/uploads/contexts/workspace/workspace-file-manager', () => ({
+ fetchServableWorkspaceFileBuffer: mockFetchServableWorkspaceFileBuffer,
fetchWorkspaceFileBuffer: mockFetchWorkspaceFileBuffer,
findWorkspaceFileRecord: mockFindWorkspaceFileRecord,
getSandboxWorkspaceFilePath: mockGetSandboxWorkspaceFilePath,
@@ -309,6 +313,61 @@ describe('executeFunctionExecute file mounts', () => {
expect(file.type).toBeUndefined()
})
+ describe('generated documents', () => {
+ const docRecord = {
+ ...fileRecord,
+ name: 'report.docx',
+ key: 'workspace/ws_1/report.docx',
+ // The stored bytes are the generator source, so the record declares its size.
+ type: 'text/x-docxjs',
+ size: 6_242,
+ }
+
+ beforeEach(() => {
+ mockFindWorkspaceFileRecord.mockReturnValue(docRecord)
+ mockListWorkspaceFiles.mockResolvedValue([docRecord])
+ mockGetSandboxWorkspaceFilePath.mockReturnValue('/home/user/files/report.docx')
+ mockFetchServableWorkspaceFileBuffer.mockResolvedValue({
+ buffer: Buffer.from('PK\u0003\u0004rendered-docx'),
+ contentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ })
+ })
+
+ it('never presigns the raw key, even on cloud storage', async () => {
+ mockHasCloudStorage.mockReturnValue(true)
+
+ await executeFunctionExecute({ inputFiles: ['files/report.docx'] }, context as never)
+
+ // Presigning record.key would hand the sandbox the generator source.
+ expect(mockGeneratePresignedDownloadUrl).not.toHaveBeenCalled()
+ expect(mockFetchWorkspaceFileBuffer).not.toHaveBeenCalled()
+ expect(mockFetchServableWorkspaceFileBuffer).toHaveBeenCalledTimes(1)
+ })
+
+ it('mounts the rendered bytes as base64, not utf-8', async () => {
+ mockHasCloudStorage.mockReturnValue(true)
+
+ await executeFunctionExecute({ inputFiles: ['files/report.docx'] }, context as never)
+
+ const file = mountedFiles()[0]
+ // record.type is text/x-docxjs; keying off it would utf-8 decode a binary.
+ expect(file.encoding).toBe('base64')
+ expect(Buffer.from(file.content as string, 'base64').toString()).toContain('rendered-docx')
+ })
+
+ it('budgets the mount on rendered length, not the declared source size', async () => {
+ mockHasCloudStorage.mockReturnValue(true)
+ // A tiny source that renders past the aggregate mount budget.
+ mockFetchServableWorkspaceFileBuffer.mockRejectedValue(
+ new PayloadSizeLimitError({ label: 'servable file download', maxBytes: 1 })
+ )
+
+ await expect(
+ executeFunctionExecute({ inputFiles: ['files/report.docx'] }, context as never)
+ ).rejects.toThrow(/mount limit/)
+ })
+ })
+
it('cloud storage: throws when a file exceeds the per-file URL mount limit', async () => {
mockFindWorkspaceFileRecord.mockReturnValue({ ...fileRecord, size: 600 * 1024 * 1024 })
diff --git a/apps/sim/lib/copilot/tools/handlers/function-execute.ts b/apps/sim/lib/copilot/tools/handlers/function-execute.ts
index 0fbb4cebafd..828e27b5956 100644
--- a/apps/sim/lib/copilot/tools/handlers/function-execute.ts
+++ b/apps/sim/lib/copilot/tools/handlers/function-execute.ts
@@ -3,6 +3,7 @@ import { decodeVfsPathSegments, encodeVfsPathSegments } from '@/lib/copilot/vfs/
import { resolveWorkflowAliasForWorkspace } from '@/lib/copilot/vfs/workflow-alias-resolver'
import { isPlanAliasPath, workflowAliasSandboxPath } from '@/lib/copilot/vfs/workflow-aliases'
import { isFeatureEnabled } from '@/lib/core/config/feature-flags'
+import { isPayloadSizeLimitError } from '@/lib/core/utils/stream-limits'
import { getColumnId } from '@/lib/table/column-keys'
import { formatCsvCell, neutralizeCsvFormula, toCsvRow } from '@/lib/table/export-format'
import { queryRows } from '@/lib/table/rows/service'
@@ -10,6 +11,7 @@ import { getTableById, listTables } from '@/lib/table/service'
import { getOrCreateTableSnapshot, SNAPSHOT_MAX_BYTES } from '@/lib/table/snapshot-cache'
import { listWorkspaceFileFolders } from '@/lib/uploads/contexts/workspace/workspace-file-folder-manager'
import {
+ fetchServableWorkspaceFileBuffer,
fetchWorkspaceFileBuffer,
findWorkspaceFileRecord,
getSandboxWorkspaceFilePath,
@@ -21,6 +23,7 @@ import {
generatePresignedDownloadUrl,
hasCloudStorage,
} from '@/lib/uploads/core/storage-service'
+import { isGeneratedDocumentSourceType } from '@/lib/uploads/utils/file-utils'
import { executeTool as executeAppTool } from '@/tools'
import type { ToolExecutionContext, ToolExecutionResult } from '../../tool-executor/types'
@@ -87,7 +90,14 @@ async function pushWorkspaceFileMount(
mountPath: string,
mounted: MountedBytes
): Promise {
- if (hasCloudStorage()) {
+ // A generated document stores its generator source, so a presigned URL for
+ // `record.key` would hand the sandbox source text under a `.docx` name and the
+ // user's script would fail on a file that looks fine. Those resolve through the
+ // servable reader instead — they are bounded by the render ceiling, so routing them
+ // through the web process rather than presigning is affordable.
+ const rendersFromSource = isGeneratedDocumentSourceType(record.type)
+
+ if (hasCloudStorage() && !rendersFromSource) {
if (record.size > MOUNT_URL_MAX_BYTES) {
throw new Error(
`Input file "${mountPath}" is ${Math.round(record.size / 1024 / 1024)}MB, over the ${MOUNT_URL_MAX_BYTES / 1024 / 1024}MB per-file mount limit.`
@@ -108,19 +118,38 @@ async function pushWorkspaceFileMount(
return
}
- if (record.size > MAX_FILE_SIZE) {
- throw new Error(
- `Input file "${mountPath}" is ${Math.round(record.size / 1024 / 1024)}MB, over the ${MAX_FILE_SIZE / 1024 / 1024}MB per-file mount limit.`
- )
- }
- if (mounted.buffered + record.size > MAX_TOTAL_SIZE) {
- throw new Error(
- `Mounting "${mountPath}" would exceed the ${MAX_TOTAL_SIZE / 1024 / 1024}MB total mount limit. Mount fewer or smaller files.`
- )
+ const remainingBudget = Math.max(0, MAX_TOTAL_SIZE - mounted.buffered)
+
+ // A source-backed document declares the size of its generator, not of the document,
+ // so these pre-checks say nothing about what is about to be mounted. Its read is
+ // capped instead, and the real length is checked once it is known.
+ if (!rendersFromSource) {
+ if (record.size > MAX_FILE_SIZE) {
+ throw new Error(
+ `Input file "${mountPath}" is ${Math.round(record.size / 1024 / 1024)}MB, over the ${MAX_FILE_SIZE / 1024 / 1024}MB per-file mount limit.`
+ )
+ }
+ if (record.size > remainingBudget) {
+ throw new Error(
+ `Mounting "${mountPath}" would exceed the ${MAX_TOTAL_SIZE / 1024 / 1024}MB total mount limit. Mount fewer or smaller files.`
+ )
+ }
}
- const buffer = await fetchWorkspaceFileBuffer(record)
+
+ const { buffer, contentType } = rendersFromSource
+ ? await fetchServableWorkspaceFileBuffer(record, {
+ maxBytes: Math.min(MAX_FILE_SIZE, remainingBudget),
+ }).catch((error) => {
+ if (!isPayloadSizeLimitError(error)) throw error
+ throw new Error(
+ `Input file "${mountPath}" renders to more than the ${MAX_FILE_SIZE / 1024 / 1024}MB per-file mount limit, or than the mount budget left. Mount fewer or smaller files.`
+ )
+ })
+ : { buffer: await fetchWorkspaceFileBuffer(record), contentType: record.type }
+ // Keyed off the resolved type: a rendered document's source MIME is `text/x-…`, and
+ // decoding the binary as UTF-8 would corrupt it just as surely as shipping the source.
const isText = /^text\/|application\/json|application\/xml|application\/csv/.test(
- record.type || ''
+ contentType || ''
)
sandboxFiles.push({
path: mountPath,
diff --git a/apps/sim/lib/copilot/tools/server/workflow/query-logs.ts b/apps/sim/lib/copilot/tools/server/workflow/query-logs.ts
index 4e4af600e85..9ab073f7dd8 100644
--- a/apps/sim/lib/copilot/tools/server/workflow/query-logs.ts
+++ b/apps/sim/lib/copilot/tools/server/workflow/query-logs.ts
@@ -145,12 +145,17 @@ export const queryLogsServerTool: BaseServerTool = {
if (args.pattern) {
logger.info('query_logs grep', { workspaceId, executionId: args.executionId })
- const { matches, truncated } = await grepSpans(traceSpans, args.pattern, viewCtx)
+ const { matches, truncated, patternNotice } = await grepSpans(
+ traceSpans,
+ args.pattern,
+ viewCtx
+ )
return {
executionId: detail.executionId,
workflowId: detail.workflowId,
status: detail.status,
pattern: args.pattern,
+ ...(patternNotice ? { patternNotice } : {}),
matches,
truncated,
}
diff --git a/apps/sim/lib/copilot/vfs/operations.test.ts b/apps/sim/lib/copilot/vfs/operations.test.ts
index 32d1ad39007..e626b702842 100644
--- a/apps/sim/lib/copilot/vfs/operations.test.ts
+++ b/apps/sim/lib/copilot/vfs/operations.test.ts
@@ -138,3 +138,35 @@ describe('grep', () => {
expect(hits).toHaveLength(1)
})
})
+
+describe('grep regex safety', () => {
+ it('runs a catastrophic pattern in linear time', () => {
+ // `a*a*b` takes minutes on a backtracking engine against this content;
+ // both the pattern and the file content are caller-supplied.
+ const files = vfsFromEntries([['notes.md', `${'a'.repeat(10000)}!`]])
+
+ const start = Date.now()
+ grep(files, 'a*a*b')
+
+ expect(Date.now() - start).toBeLessThan(2000)
+ })
+
+ it('still interprets regex syntax', () => {
+ const files = vfsFromEntries([['log.txt', 'req finished status=503']])
+ expect(grep(files, 'status=\\d+')).toHaveLength(1)
+ expect(grep(files, 'status=\\d+', undefined, { outputMode: 'count' })).toEqual([
+ { path: 'log.txt', count: 1 },
+ ])
+ })
+
+ it('matches syntax RE2 cannot represent literally instead of not at all', () => {
+ const files = vfsFromEntries([['log.txt', 'contains (?=x) verbatim']])
+ expect(grep(files, '(?=x)')).toHaveLength(1)
+ })
+
+ it('honours ignoreCase across repeated line tests', () => {
+ const files = vfsFromEntries([['log.txt', 'Alpha\nALPHA\nalpha']])
+ expect(grep(files, 'alpha', undefined, { ignoreCase: true })).toHaveLength(3)
+ expect(grep(files, 'alpha')).toHaveLength(1)
+ })
+})
diff --git a/apps/sim/lib/copilot/vfs/operations.ts b/apps/sim/lib/copilot/vfs/operations.ts
index bd7208719b8..624707b43e9 100644
--- a/apps/sim/lib/copilot/vfs/operations.ts
+++ b/apps/sim/lib/copilot/vfs/operations.ts
@@ -1,4 +1,13 @@
+import { createLogger } from '@sim/logger'
import micromatch from 'micromatch'
+import {
+ compileLinearRegex,
+ isPlainText,
+ type LinearRegex,
+ literalRegex,
+} from '@/lib/core/security/linear-regex'
+
+const logger = createLogger('VfsOperations')
export interface GrepMatch {
path: string
@@ -125,7 +134,16 @@ export function pathWithinGrepScope(filePath: string, scope: string): boolean {
}
/**
- * Regex search over VFS file contents using ECMAScript `RegExp` syntax.
+ * Regex search over VFS file contents using RE2 syntax — a subset of
+ * ECMAScript `RegExp` (see `@/lib/core/security/linear-regex`).
+ *
+ * A pattern RE2 cannot represent — negative lookaround, backreferences — is
+ * matched as a literal instead of on the backtracking engine, as is a pattern
+ * that does not compile at all (which previously returned no results). Both
+ * cases log a warning: the return shape carries results only, so there is
+ * nowhere to tell the caller inline, and a literal fallback can match the
+ * pattern's own text when grepping source that contains regexes.
+ *
* `content` and `count` are line-oriented (split on newline, CR stripped per line).
* `files_with_matches` tests the entire file string once, so multiline patterns can match there
* but not in line modes.
@@ -142,19 +160,27 @@ export function grep(
const showLineNumbers = opts?.lineNumbers ?? true
const contextLines = opts?.context ?? 0
- const flags = ignoreCase ? 'gi' : 'g'
- let regex: RegExp
- try {
- regex = new RegExp(pattern, flags)
- } catch {
- return []
+ // Caller-supplied pattern over caller-supplied file content on the shared
+ // event loop — matched by RE2 so it cannot backtrack. Syntax RE2 cannot
+ // represent degrades to a literal rather than to the backtracking engine.
+ let regex: LinearRegex
+ if (isPlainText(pattern)) {
+ regex = literalRegex(pattern, { ignoreCase })
+ } else {
+ const linear = compileLinearRegex(pattern, { ignoreCase })
+ if (!linear) {
+ // The return shape carries results only, so the caller cannot be told
+ // inline that its regex was taken literally — log it, since silently
+ // returning "no matches" reads as "not in the file".
+ logger.warn('Grep pattern is not RE2-representable; matching it literally', { pattern })
+ }
+ regex = linear ?? literalRegex(pattern, { ignoreCase })
}
if (outputMode === 'files_with_matches') {
const matchingFiles: string[] = []
for (const [filePath, content] of files) {
if (path && !pathWithinGrepScope(filePath, path)) continue
- regex.lastIndex = 0
if (regex.test(content)) {
matchingFiles.push(filePath)
if (matchingFiles.length >= maxResults) break
@@ -170,7 +196,6 @@ export function grep(
const lines = splitLinesForGrep(content)
let count = 0
for (const line of lines) {
- regex.lastIndex = 0
if (regex.test(line)) count++
}
if (count > 0) {
@@ -188,7 +213,6 @@ export function grep(
const lines = splitLinesForGrep(content)
for (let i = 0; i < lines.length; i++) {
- regex.lastIndex = 0
if (regex.test(lines[i])) {
if (contextLines > 0) {
const start = Math.max(0, i - contextLines)
diff --git a/apps/sim/lib/core/security/input-validation.server.ts b/apps/sim/lib/core/security/input-validation.server.ts
index e8ea4071577..02c44a1ed97 100644
--- a/apps/sim/lib/core/security/input-validation.server.ts
+++ b/apps/sim/lib/core/security/input-validation.server.ts
@@ -18,6 +18,7 @@ import {
} from 'undici'
import { isHosted, isPrivateDatabaseHostsAllowed } from '@/lib/core/config/env-flags'
import { type ValidationResult, validateExternalUrl } from '@/lib/core/security/input-validation'
+import { nodeReadableToWebStream } from '@/lib/core/utils/node-stream'
import { PayloadSizeLimitError } from '@/lib/core/utils/stream-limits'
const logger = createLogger('InputValidation')
@@ -717,67 +718,6 @@ function contentEncodingDecoder(
}
}
-/**
- * Bridges an undici `request()` Node `Readable` into a WHATWG `ReadableStream` for a
- * `Response` body. Node's built-in `Readable.toWeb` is NOT used: its adapter throws an
- * unhandled `ERR_INVALID_STATE` ("Controller is already closed") when the web stream is
- * cancelled while the Node stream is still flowing — which `followRedirectsGuarded` does
- * on every redirect hop (`response.body.cancel()`). This bridge instead swallows a late
- * enqueue after close and destroys the source on cancel, so cancelling a live body frees
- * its socket cleanly. `maxResponseSize` overruns surface as the source's `error` event and
- * reject the read, preserving the DoS backstop.
- */
-function nodeReadableToWebStream(nodeStream: Readable): ReadableStream {
- let settled = false
- return new ReadableStream({
- start(controller) {
- nodeStream.on('data', (chunk: Buffer) => {
- try {
- // Copy, not a view: undici may recycle the pooled buffer backing `chunk` after
- // this handler returns, which would corrupt a chunk still queued for a slow
- // consumer. `new Uint8Array(chunk)` allocates a fresh backing buffer.
- controller.enqueue(new Uint8Array(chunk))
- } catch {
- // Controller already closed (consumer cancelled) — stop the source, drop the chunk.
- nodeStream.destroy()
- return
- }
- if ((controller.desiredSize ?? 1) <= 0) nodeStream.pause()
- })
- nodeStream.once('end', () => {
- settled = true
- try {
- controller.close()
- } catch {}
- })
- nodeStream.once('error', (err) => {
- settled = true
- try {
- controller.error(err)
- } catch {}
- })
- // An abort (signal) or upstream reset can `destroy()` the source with no `error`
- // event; without this the reader would hang forever. `close` fires after every
- // terminal path, so only act when `end`/`error` didn't already settle the stream.
- nodeStream.once('close', () => {
- if (settled) return
- settled = true
- try {
- controller.error(new Error('MCP transport stream closed before completing'))
- } catch {}
- })
- // Start paused so nothing buffers before the consumer pulls (backpressure).
- nodeStream.pause()
- },
- pull() {
- nodeStream.resume()
- },
- cancel(reason) {
- nodeStream.destroy(reason instanceof Error ? reason : undefined)
- },
- })
-}
-
/**
* Streaming-safe replacement for `undiciFetch(url, { ...init, dispatcher })`.
*
diff --git a/apps/sim/lib/core/security/linear-regex.differential.test.ts b/apps/sim/lib/core/security/linear-regex.differential.test.ts
new file mode 100644
index 00000000000..02c5abbf516
--- /dev/null
+++ b/apps/sim/lib/core/security/linear-regex.differential.test.ts
@@ -0,0 +1,241 @@
+/**
+ * @vitest-environment node
+ *
+ * Differential test: every pattern the linear engine accepts must behave like
+ * the built-in engine.
+ *
+ * Every defect found in this module has been a silent semantic divergence, not
+ * a crash — `\s` losing Unicode whitespace, a decomposed lookaround binding to
+ * the wrong alternation branch, a consumed lookahead swallowing boundaries.
+ * Each was caught by comparing engines over a corpus, and each would have
+ * shipped otherwise, because the code was self-consistent and every
+ * hand-written example passed.
+ *
+ * So the comparison lives here rather than in a scratch script. Any future
+ * change to `translateToRe2` or the split decomposition is checked against
+ * `RegExp` across the corpus below, with the known divergences enumerated
+ * explicitly — a divergence that is not on that list is a bug.
+ */
+
+import { describe, expect, it } from 'vitest'
+import { compileLinearRegex, compileLookaroundSplit } from '@/lib/core/security/linear-regex'
+
+/** How a caller compiles: linear engine first, split decomposition second. */
+function compile(pattern: string) {
+ return compileLinearRegex(pattern) ?? compileLookaroundSplit(pattern)
+}
+
+/**
+ * Delimiters people actually write, plus the shapes that have broken before.
+ * Anything here that the linear engine accepts must split like `RegExp`.
+ */
+const SPLIT_PATTERNS = [
+ // Plain delimiters
+ '\\n\\n',
+ '\\n\\n+',
+ '\\s+',
+ '\\s{2,}',
+ '\\.\\s+',
+ '[.!?]\\s+',
+ '---+',
+ '\\|',
+ ',\\s*',
+ '\\t',
+ // Structural
+ '\\n#{1,6}\\s',
+ '',
+ ' ',
+ '\\r?\\n',
+ // Lookaround: keep the delimiter
+ '(?=#\\s)',
+ '(?=#{1,6}\\s)',
+ '(?<=\\.)',
+ '(?<=)',
+ '(?<=\\.)\\s+',
+ '(?<=[.!?])\\s+',
+ '(?<=[.!?])\\s+(?=[A-Z])',
+ '(?<=\\w)\\s+(?=[A-Z])',
+ '(?<=\\w)\\s+(?=\\w)',
+ '\\n(?=Chapter )',
+ '\\n(?=\\d+\\.)',
+ '(?<=;)\\s*',
+ // Top-level alternation beside an assertion: must be declined, never
+ // reshaped — `(?<=X)A|B` is not `(?:X)(A|B)`.
+ '(?<=\\.)\\s+|\\n\\n',
+ '(?<=)| ',
+ 'a|b(?=c)',
+ '\\n\\n|(?<=\\.)\\s',
+ // Quantifier and class shapes
+ '[-=]{3,}',
+ '\\s*\\n\\s*\\n\\s*',
+ '(?:\\r\\n|\\n){2}',
+ '[\\s\\u00b7]+',
+]
+
+/**
+ * Documents chosen to exercise the divergences that have bitten: non-ASCII
+ * whitespace from PDF/HTML extraction, CJK, and adjacent delimiters.
+ */
+const DOCUMENTS = [
+ 'Section one\n\nSection two\n\nSection three',
+ 'One. Two. Three. Four.',
+ 'A B C D',
+ 'Heading\n\nAlpha beta.\nGamma delta.',
+ '# One\nalpha\n## Two\nbeta\n### Three',
+ 'one two three ',
+ 'Chapter 1\nintro\nChapter 2\nmore',
+ // Non-breaking and exotic whitespace — the stored-data divergence
+ 'Section 1. Overview The agreement',
+ 'Bullet one Bullet two Bullet three',
+ 'Prix : 100 EUR. Livraison offerte.',
+ '第一章 概要 第二章 詳細',
+ 'line
separator
paragraph',
+ 'tab\tseparated\tvalues',
+ // Degenerate
+ '',
+ ' ',
+ '\n\n\n',
+ 'nodelimiterhere',
+ 'a,b,',
+ 'trailing delimiter.\n\n',
+ 'Heading\n\nAlpha beta.\nGamma delta.\n\nMore.',
+ 'onetwo three',
+ 'zabzabz',
+]
+
+/**
+ * Divergences that are known, documented on the API, and accepted.
+ *
+ * Keep this list short and specific — it is the honest boundary of the
+ * guarantee, so every entry needs a reason, not just a pattern.
+ */
+const KNOWN_DIVERGENCES: Array<{ pattern: string; doc: string; reason: string }> = [
+ {
+ pattern: '(?<=aa)',
+ doc: 'aaaaa',
+ reason:
+ 'Lookbehind whose body self-overlaps. Matching every position would mean restarting the scan one character past each match start, which is quadratic on a multi-megabyte document and forfeits the linear guarantee this module exists for. Delimiters that do not self-overlap — punctuation, tags, whitespace — are exact.',
+ },
+]
+
+function isKnownDivergence(pattern: string): boolean {
+ return KNOWN_DIVERGENCES.some((entry) => entry.pattern === pattern)
+}
+
+/**
+ * `LinearRegex.split` omits the trailing empty segment `RegExp` produces, and
+ * every caller filters empties anyway — so compare on the filtered forms.
+ */
+function normalize(segments: string[]): string[] {
+ return segments.filter((segment) => segment !== '')
+}
+
+describe('differential: split parity with the built-in engine', () => {
+ const cases = SPLIT_PATTERNS.flatMap((pattern) =>
+ DOCUMENTS.map((doc) => ({ pattern, doc }))
+ ).filter(({ pattern }) => !isKnownDivergence(pattern))
+
+ it(`covers ${cases.length} pattern/document pairs`, () => {
+ expect(cases.length).toBeGreaterThan(400)
+ })
+
+ it.each(SPLIT_PATTERNS.filter((pattern) => !isKnownDivergence(pattern)))(
+ 'splits %s identically to RegExp across every document',
+ (pattern) => {
+ const compiled = compile(pattern)
+ // A pattern the linear engine declines is handled by the caller (notice,
+ // literal fallback, or a thrown config error) — not a parity concern.
+ if (!compiled) return
+
+ for (const doc of DOCUMENTS) {
+ expect(
+ normalize(compiled.split(doc)),
+ `pattern ${pattern} on ${JSON.stringify(doc)}`
+ ).toEqual(normalize(doc.split(new RegExp(pattern, 'g'))))
+ }
+ }
+ )
+})
+
+describe('differential: test/find parity with the built-in engine', () => {
+ /** Grep-style patterns, where `test` and the match index are what matter. */
+ const MATCH_PATTERNS = [
+ 'timeout',
+ 'ECONNREFUSED',
+ 'status=\\d+',
+ 'status=5\\d\\d',
+ '^Agent',
+ 'agent$',
+ '(openai|anthropic)',
+ '\\bstatus\\b',
+ '\\d{4}-\\d{2}-\\d{2}',
+ '[Ee]xception',
+ 'https?://[^\\s"]+',
+ '\\$\\{[^}]*\\}',
+ 'block_\\d+.*output',
+ '\\s+$',
+ '^\\s*\\{.*\\}\\s*$',
+ 'a.*b.*c',
+ '.*',
+ '.+',
+ 'sk-[A-Za-z0-9]{20,}',
+ 'rate.?limit',
+ '\\w+@\\w+\\.\\w+',
+ '[0-9a-f]{8}-[0-9a-f]{4}',
+ '\\s',
+ '\\S+',
+ ]
+
+ const HAYSTACKS = [
+ 'Agent 1 called api.openai.com -> status=503 at 2026-01-01',
+ 'request timeout occurred after 30s',
+ 'ECONNREFUSED connecting to db',
+ 'user@example.com signed in',
+ 'sk-abcdefghijklmnopqrstuvwxyz012345',
+ 'value with non-breaking space',
+ '第一章 概要',
+ '{ "ok": true }',
+ '',
+ ' ',
+ ]
+
+ it.each(MATCH_PATTERNS)('matches %s identically to RegExp', (pattern) => {
+ const compiled = compile(pattern)
+ if (!compiled) return
+
+ const oracle = new RegExp(pattern)
+ for (const text of HAYSTACKS) {
+ const label = `pattern ${pattern} on ${JSON.stringify(text)}`
+ expect(compiled.test(text), label).toBe(oracle.test(text))
+
+ const match = oracle.exec(text)
+ expect(compiled.find(text), label).toBe(match ? match.index : -1)
+ }
+ })
+
+ it.each(MATCH_PATTERNS)('matches %s identically to RegExp when ignoring case', (pattern) => {
+ const compiled = compileLinearRegex(pattern, { ignoreCase: true })
+ if (!compiled) return
+
+ const oracle = new RegExp(pattern, 'i')
+ for (const text of HAYSTACKS) {
+ expect(compiled.test(text), `pattern ${pattern} on ${JSON.stringify(text)}`).toBe(
+ oracle.test(text)
+ )
+ }
+ })
+})
+
+describe('differential: every known divergence is still exactly as documented', () => {
+ // Pinned so the list cannot rot. If a divergence is silently fixed this
+ // fails and the entry must be deleted; if one spreads, the parity suites
+ // above fail. Either way the list stays honest about the real boundary.
+ it.each(KNOWN_DIVERGENCES)('$pattern still diverges on $doc — $reason', ({ pattern, doc }) => {
+ const compiled = compile(pattern)
+ expect(compiled).not.toBeNull()
+
+ expect(normalize(compiled?.split(doc) ?? [])).not.toEqual(
+ normalize(doc.split(new RegExp(pattern, 'g')))
+ )
+ })
+})
diff --git a/apps/sim/lib/core/security/linear-regex.test.ts b/apps/sim/lib/core/security/linear-regex.test.ts
new file mode 100644
index 00000000000..c6edab05414
--- /dev/null
+++ b/apps/sim/lib/core/security/linear-regex.test.ts
@@ -0,0 +1,200 @@
+/**
+ * @vitest-environment node
+ */
+
+import { describe, expect, it } from 'vitest'
+import {
+ compileLinearRegex,
+ compileLookaroundSplit,
+ isPlainText,
+ literalRegex,
+} from '@/lib/core/security/linear-regex'
+
+/**
+ * Patterns that take exponential time on a backtracking engine. `a*a*b` is the
+ * important one: it defeats `safe-regex2`'s star-height screen and a
+ * quantified-group screen alike, and measured 213s on JSC / 132s on V8 against
+ * the input below.
+ */
+const CATASTROPHIC = ['(a+)+$', '(a|a)*b', 'a*a*b', '(x+x+)+y', '(\\w+\\s?)*$', '^(\\d+)*$']
+
+describe('compileLinearRegex', () => {
+ it.each(CATASTROPHIC)('matches %s in linear time on adversarial input', (pattern) => {
+ const regex = compileLinearRegex(pattern)
+ expect(regex).not.toBeNull()
+
+ const adversarial = `${'a'.repeat(50000)}!`
+ const start = Date.now()
+ regex?.test(adversarial)
+ expect(Date.now() - start).toBeLessThan(2000)
+ })
+
+ it('interprets regex syntax rather than matching it literally', () => {
+ const regex = compileLinearRegex('status=\\d+')
+ expect(regex?.test('http status=503 here')).toBe(true)
+ expect(regex?.test('status=abc')).toBe(false)
+ expect(regex?.find('http status=503')).toBe(5)
+ })
+
+ it('honours ignoreCase only when asked', () => {
+ expect(compileLinearRegex('ERROR', { ignoreCase: true })?.test('an error here')).toBe(true)
+ expect(compileLinearRegex('ERROR')?.test('an error here')).toBe(false)
+ })
+
+ it('splits equivalently to String.prototype.split, minus the trailing empty', () => {
+ const doc = '# One\ntext a\n\n# Two\ntext b'
+ expect(compileLinearRegex('\\n\\n+')?.split(doc)).toEqual(doc.split(/\n\n+/g))
+
+ // The documented divergence: a trailing delimiter yields no empty tail.
+ expect(compileLinearRegex(',')?.split('a,b,')).toEqual(['a', 'b'])
+ expect('a,b,'.split(/,/g)).toEqual(['a', 'b', ''])
+ })
+
+ it.each([
+ ['non-breaking space', '\u00a0'],
+ ['narrow no-break space', '\u202f'],
+ ['ideographic space', '\u3000'],
+ ['line separator', '\u2028'],
+ ['vertical tab', '\v'],
+ ])('treats %s as whitespace, matching the built-in engine', (_label, ws) => {
+ // RE2's own \\s is ASCII-only. Untranslated, a \\s document splitter stops
+ // splitting on the whitespace that PDF/HTML extraction emits, silently
+ // changing stored chunk boundaries.
+ const doc = `alpha${ws}beta`
+ expect(compileLinearRegex('\\s+')?.split(doc)).toEqual(doc.split(/\s+/g))
+ expect(compileLinearRegex('\\s')?.test(doc)).toBe(true)
+ })
+
+ it.each([
+ ['lookahead', '(?=foo)bar'],
+ ['lookbehind', '(?<=id: )\\w+'],
+ ['backreference', '(ab)\\1'],
+ ['invalid syntax', '('],
+ ])('returns null for %s so the caller must choose how to degrade', (_label, pattern) => {
+ expect(compileLinearRegex(pattern)).toBeNull()
+ })
+
+ it('returns -1 from find when there is no match', () => {
+ expect(compileLinearRegex('zzz')?.find('abc')).toBe(-1)
+ })
+})
+
+describe('literalRegex', () => {
+ it('treats regex syntax as ordinary characters', () => {
+ const regex = literalRegex('a+b')
+ expect(regex.test('xxa+bxx')).toBe(true)
+ expect(regex.test('aaab')).toBe(false)
+ })
+
+ it('is unaffected by repeated calls (no lastIndex carry-over)', () => {
+ const regex = literalRegex('needle')
+ const text = 'needle here and needle again'
+ expect([regex.test(text), regex.test(text), regex.test(text)]).toEqual([true, true, true])
+ expect([regex.find(text), regex.find(text)]).toEqual([0, 0])
+ })
+
+ it('matches case-insensitively when asked', () => {
+ expect(literalRegex('Needle', { ignoreCase: true }).test('a NEEDLE')).toBe(true)
+ expect(literalRegex('Needle').test('a NEEDLE')).toBe(false)
+ })
+})
+
+describe('isPlainText / escapeRegExp', () => {
+ it.each(['timeout', 'ECONNREFUSED', 'status=503', 'GET /api/logs'])(
+ 'treats %s as plain text',
+ (pattern) => expect(isPlainText(pattern)).toBe(true)
+ )
+
+ it.each(['example.com', 'a+b', '^x', '(a|b)', '[abc]'])(
+ 'treats %s as containing metacharacters',
+ (pattern) => expect(isPlainText(pattern)).toBe(false)
+ )
+
+ it.each(['.', '*', '+', '?', '^', '$', '{', '}', '(', ')', '|', '[', ']', '\\'])(
+ 'escapes %s so a literal pattern matches only itself',
+ (meta) => {
+ const regex = literalRegex(`a${meta}b`)
+ expect(regex.test(`a${meta}b`)).toBe(true)
+ // Under-escaping shows up here: an unescaped metacharacter would let the
+ // pattern match text that does not contain it verbatim.
+ expect(regex.test('aXb')).toBe(false)
+ expect(regex.test('ab')).toBe(false)
+ }
+ )
+})
+
+describe('compileLookaroundSplit', () => {
+ it('splits before each delimiter for (?=X), matching String.split', () => {
+ const doc = '# One\nalpha\n# Two\nbeta'
+ expect(compileLookaroundSplit('(?=#\\s)')?.split(doc)).toEqual(
+ doc.split(/(?=#\s)/g).filter(Boolean)
+ )
+ })
+
+ it('splits after each delimiter for (?<=X)', () => {
+ const doc = 'one two three '
+ expect(compileLookaroundSplit('(?<=)')?.split(doc)).toEqual([
+ 'one ',
+ 'two ',
+ 'three ',
+ ])
+ })
+
+ it('stays linear on a catastrophic body', () => {
+ const regex = compileLookaroundSplit('(?=a*a*b)')
+ expect(regex).not.toBeNull()
+
+ const start = Date.now()
+ regex?.split(`${'a'.repeat(20000)}!`)
+ expect(Date.now() - start).toBeLessThan(2000)
+ })
+
+ it.each([
+ ['split after a period', '(?<=\\.)\\s+', 'One. Two. Three.'],
+ ['split before a heading', '\\n(?=Chapter )', 'intro\nChapter 1\nChapter 2'],
+ ['sentence splitter', '(?<=[.!?])\\s+(?=[A-Z])', 'One. Two! Three? four.'],
+ ])('handles %s, where the assertion has an affix', (_label, pattern, doc) => {
+ // These are the common shapes: a lookaround combined with other syntax.
+ // Handling only a whole-pattern assertion would reject them outright.
+ const split = compileLookaroundSplit(pattern)?.split(doc)
+ expect(split).toEqual(doc.split(new RegExp(pattern, 'g')))
+ })
+
+ it.each([
+ ['negative lookahead', '(?!x)y'],
+ ['negative lookbehind', '(? {
+ expect(compileLookaroundSplit(pattern)).toBeNull()
+ })
+
+ it.each([
+ ['leading assertion', '(?<=\\.)\\s+|\\n\\n'],
+ ['empty middle', '(?<=)| '],
+ ['trailing assertion', 'a|b(?=c)'],
+ ])('rejects %s with top-level alternation rather than reshaping it', (_label, pattern) => {
+ // `(?<=X)A|B` means `((?<=X)A)|B`; rebuilt as `(?:X)(A|B)` it would demand
+ // the assertion before both branches. No grouping recovers that, so the
+ // only correct answer is to decline the pattern.
+ expect(compileLookaroundSplit(pattern)).toBeNull()
+ })
+
+ it('is unaffected by a capturing group inside the lookbehind', () => {
+ // The middle is captured by name, so group numbering cannot shift.
+ expect(compileLookaroundSplit('(?<=(a))b')?.split('xaby')).toEqual(['xa', 'y'])
+
+ const optional = compileLookaroundSplit('(?<=(a)|b)c')
+ expect(optional?.find('bc')).toBe(1)
+ expect(optional?.test('bc')).toBe(true)
+ })
+
+ it.each([
+ ['(?<=\\w)\\s+(?=[A-Z])', 'A B C D'],
+ ['(?<=\\w)\\s+(?=\\w)', 'a b c d e'],
+ ])('does not consume assertion text between boundaries (%s)', (pattern, doc) => {
+ // The lookahead of one boundary is the lookbehind of the next. Consuming
+ // it would swallow every other split.
+ expect(compileLookaroundSplit(pattern)?.split(doc)).toEqual(doc.split(new RegExp(pattern, 'g')))
+ })
+})
diff --git a/apps/sim/lib/core/security/linear-regex.ts b/apps/sim/lib/core/security/linear-regex.ts
new file mode 100644
index 00000000000..a22134473cb
--- /dev/null
+++ b/apps/sim/lib/core/security/linear-regex.ts
@@ -0,0 +1,367 @@
+import { RE2JS } from 're2js'
+
+/**
+ * Linear-time matching for caller-supplied regex patterns.
+ *
+ * The built-in engine backtracks, so a pattern chosen by a caller can take
+ * exponential time on input the same caller controls — `a*a*b` against a 10k
+ * run of `a` measured 213s on JSC and 132s on V8. Anywhere that runs on a
+ * shared event loop, that is a denial of service against every other tenant.
+ *
+ * Screening the pattern instead does not work. `safe-regex2` documents itself
+ * as having false negatives and passes `(a|a)*b`; rejecting quantified groups
+ * on top of it still passes `a*a*b`. Every syntactic rule only excludes the
+ * shapes someone thought to enumerate, so the engine has to change instead.
+ *
+ * RE2 has no backtracking and matches in time linear in the input. Two costs
+ * follow, and this module works to keep both off the caller:
+ *
+ * - **Syntax.** RE2 implements neither lookaround nor backreferences, and
+ * spells some escapes differently. `translateToRe2` bridges the mechanical
+ * differences and `compileLookaroundSplit` recovers the lookaround *split*
+ * idioms; genuine gaps return `null` so each caller decides how to degrade.
+ * - **Throughput.** Measured 0.5–270ms per megabyte depending on the pattern,
+ * against roughly 0.04ms/MB for the built-in engine. Callers matching a
+ * pattern with no metacharacter should take `literalRegex` instead.
+ */
+export interface LinearRegexOptions {
+ ignoreCase?: boolean
+}
+
+export interface LinearRegex {
+ /** Whether the pattern matches anywhere in `text`. */
+ test(text: string): boolean
+ /** Index of the first match in `text`, or -1. */
+ find(text: string): number
+ /**
+ * Split `text` around every match.
+ *
+ * Follows `String.prototype.split` except that a trailing empty segment is
+ * omitted — RE2 drops it, and every caller here discards empties anyway.
+ */
+ split(text: string): string[]
+}
+
+const METACHARACTERS = /[.*+?^${}()|[\]\\]/
+
+/** Escape every regex metacharacter so `input` matches only itself. */
+function escapeRegExp(input: string): string {
+ return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
+}
+
+/** True when `pattern` has no metacharacter, so both engines behave identically. */
+export function isPlainText(pattern: string): boolean {
+ return !METACHARACTERS.test(pattern)
+}
+
+/**
+ * ECMAScript's `\s` as an RE2 character-class body.
+ *
+ * RE2's `\s` is ASCII-only (`[\t\n\f\r ]`), while ECMAScript's also covers
+ * `\v`, NBSP, U+1680, U+2000–U+200A, U+2028, U+2029, U+202F, U+205F, U+3000
+ * and U+FEFF. Left untranslated, a `\s`-based document splitter silently stops
+ * splitting on the non-breaking spaces that PDF, DOCX and HTML extraction put
+ * everywhere — producing different chunks, and so different embeddings, for a
+ * document that has not changed. Verified equivalent to ECMAScript `\s` across
+ * a full sweep of the BMP.
+ */
+const JS_WHITESPACE_BODY = '\\t\\n\\v\\f\\r\\x{2028}\\x{2029}\\x{feff}\\p{Zs}'
+
+/**
+ * Rewrite the mechanical ECMAScript-vs-RE2 spelling differences.
+ *
+ * Two substitutions, both verified equivalent:
+ * - `\s`/`\S` → the Unicode set above, so whitespace splitting is unchanged.
+ * - `\uXXXX` → `\x{XXXX}`, RE2's spelling. Untranslated, RE2 rejects the
+ * pattern outright, which turns the ordinary way of writing a non-ASCII
+ * delimiter (`•`) into a hard failure.
+ *
+ * Scans with escape and character-class state so `\\s` (a literal backslash
+ * followed by `s`) is left alone and `[\s\d]` splices the set body rather than
+ * nesting a class. `\S` *inside* a class is left as RE2's ASCII form: negation
+ * within a set cannot be spliced, and `[^\S\n]` is rare enough not to justify
+ * a full class parser.
+ */
+function translateToRe2(pattern: string): string {
+ let out = ''
+ let inClass = false
+ for (let i = 0; i < pattern.length; i++) {
+ const char = pattern[i]
+
+ if (char === '\\') {
+ const next = pattern[i + 1]
+ if (next === 'u' && /^[0-9a-fA-F]{4}$/.test(pattern.slice(i + 2, i + 6))) {
+ out += `\\x{${pattern.slice(i + 2, i + 6)}}`
+ i += 5
+ continue
+ }
+ if (next === 's') {
+ out += inClass ? JS_WHITESPACE_BODY : `[${JS_WHITESPACE_BODY}]`
+ i += 1
+ continue
+ }
+ if (next === 'S' && !inClass) {
+ out += `[^${JS_WHITESPACE_BODY}]`
+ i += 1
+ continue
+ }
+ out += next === undefined ? char : char + next
+ i += 1
+ continue
+ }
+
+ if (inClass) {
+ if (char === ']') inClass = false
+ } else if (char === '[') {
+ inClass = true
+ }
+ out += char
+ }
+ return out
+}
+
+/** Index of the `)` closing the group opened at `open`, or -1. */
+function closingParen(pattern: string, open: number): number {
+ let depth = 0
+ let inClass = false
+ for (let i = open; i < pattern.length; i++) {
+ const char = pattern[i]
+ if (char === '\\') {
+ i += 1
+ continue
+ }
+ if (inClass) {
+ if (char === ']') inClass = false
+ continue
+ }
+ if (char === '[') inClass = true
+ else if (char === '(') depth += 1
+ else if (char === ')') {
+ depth -= 1
+ if (depth === 0) return i
+ }
+ }
+ return -1
+}
+
+interface SplitShape {
+ behind: string
+ middle: string
+ ahead: string
+}
+
+/**
+ * True when `pattern` has a `|` outside any group or character class.
+ *
+ * A split shape cannot be decomposed when its middle alternates at the top
+ * level: `(?<=\.)\s+|\n\n` means `((?<=\.)\s+)|(\n\n)`, so the assertion binds
+ * to the first branch only. Rebuilding it as `(?:\.)(\s+|\n\n)` would require
+ * the period before *either* branch — a silently different pattern. No grouping
+ * fixes that, so such patterns are rejected rather than reshaped.
+ */
+function hasTopLevelAlternation(pattern: string): boolean {
+ let depth = 0
+ let inClass = false
+ for (let i = 0; i < pattern.length; i++) {
+ const char = pattern[i]
+ if (char === '\\') {
+ i += 1
+ continue
+ }
+ if (inClass) {
+ if (char === ']') inClass = false
+ continue
+ }
+ if (char === '[') inClass = true
+ else if (char === '(') depth += 1
+ else if (char === ')') depth -= 1
+ else if (char === '|' && depth === 0) return true
+ }
+ return false
+}
+
+/**
+ * Decompose a split pattern into `(?<=behind) middle (?=ahead)`, with either
+ * assertion optional. Returns `null` when neither is present (the caller should
+ * compile normally) or when the shape is anything else.
+ */
+function parseSplitShape(pattern: string): SplitShape | null {
+ let rest = pattern
+ let behind = ''
+
+ if (rest.startsWith('(?<=')) {
+ const close = closingParen(rest, 0)
+ if (close === -1) return null
+ behind = rest.slice(4, close)
+ rest = rest.slice(close + 1)
+ }
+
+ let ahead = ''
+ let depth = 0
+ let inClass = false
+ for (let i = 0; i < rest.length; i++) {
+ const char = rest[i]
+ if (char === '\\') {
+ i += 1
+ continue
+ }
+ if (inClass) {
+ if (char === ']') inClass = false
+ continue
+ }
+ if (char === '[') {
+ inClass = true
+ continue
+ }
+ if (char === ')') depth -= 1
+ if (char !== '(') continue
+ depth += 1
+ if (depth !== 1 || !rest.startsWith('(?=', i)) continue
+ const close = closingParen(rest, i)
+ if (close !== rest.length - 1) continue
+ ahead = rest.slice(i + 3, close)
+ rest = rest.slice(0, i)
+ break
+ }
+
+ if (!behind && !ahead) return null
+ if (hasTopLevelAlternation(rest)) return null
+ return { behind, middle: rest, ahead }
+}
+
+/**
+ * Compile a lookaround *split* pattern onto RE2, which has no lookaround.
+ *
+ * Splitting never needs the assertion itself — only the span the delimiter
+ * consumes. `(?<=X)Y(?=Z)` becomes `(?:X)(Y)(?:Z)`, and the span of group 1 is
+ * exactly what `String.prototype.split` would remove. That covers every
+ * combination in one rule: `(?=Z)` alone splits before a delimiter and keeps
+ * it, `(?<=X)` alone splits after one, and `(?<=[.!?])\s+(?=[A-Z])` — the
+ * sentence splitter — consumes the whitespace between them.
+ *
+ * Returns `null` for negative lookaround, backreferences, a middle that
+ * alternates at the top level, or any body RE2 cannot represent.
+ *
+ * Two details make the reconstruction faithful rather than approximate. The
+ * middle is captured by *name*, so a capturing group inside `behind` cannot
+ * shift the index out from under it. And each iteration resumes the search at
+ * the end of the previous delimiter rather than at the end of the whole match,
+ * so the assertion text is not consumed — without that, every boundary whose
+ * lookahead text doubles as the next boundary's lookbehind would be swallowed
+ * (`(?<=\w)\s+(?=[A-Z])` over `A B C D` would split only half the gaps).
+ *
+ * Known divergence: a delimiter that self-overlaps — `(?<=aa)` over `aaaaa`,
+ * or a single-character middle whose matches abut as in `(?<=\w).(?=\w)` —
+ * yields fewer boundaries than the built-in engine. Matching every position
+ * would mean restarting the scan one character past each match start, which is
+ * quadratic on a multi-megabyte document and forfeits the linear guarantee
+ * this module exists for. Delimiters that do not self-overlap — punctuation,
+ * tags, whitespace between tokens — are exact, and
+ * `linear-regex.differential.test.ts` pins that.
+ */
+export function compileLookaroundSplit(
+ pattern: string,
+ options: LinearRegexOptions = {}
+): LinearRegex | null {
+ const shape = parseSplitShape(pattern)
+ if (!shape) return null
+
+ const behind = shape.behind ? `(?:${shape.behind})` : ''
+ const ahead = shape.ahead ? `(?:${shape.ahead})` : ''
+ const source = translateToRe2(`${behind}(?P${shape.middle})${ahead}`)
+
+ let compiled: ReturnType
+ try {
+ compiled = RE2JS.compile(source, options.ignoreCase ? RE2JS.CASE_INSENSITIVE : 0)
+ } catch {
+ return null
+ }
+
+ /** Span of the delimiter itself — what `String.prototype.split` removes. */
+ const delimiterAt = (text: string, from: number): { start: number; end: number } | null => {
+ const matcher = compiled.matcher(text)
+ if (!matcher.find(from)) return null
+ return { start: matcher.start('mid'), end: matcher.end('mid') }
+ }
+
+ return {
+ test: (text) => compiled.matcher(text).find(),
+ find: (text) => delimiterAt(text, 0)?.start ?? -1,
+ split: (text) => {
+ const segments: string[] = []
+ let cursor = 0
+ let searchFrom = 0
+ while (searchFrom <= text.length) {
+ const span = delimiterAt(text, searchFrom)
+ if (!span) break
+ // Always advance, so a zero-width delimiter cannot spin.
+ searchFrom = span.end > span.start ? span.end : span.start + 1
+ // Skip a boundary behind the cursor, or one that would only emit an
+ // empty leading or trailing segment.
+ if (span.start < cursor || span.start >= text.length) continue
+ if (span.start === cursor && span.end === cursor) continue
+ segments.push(text.slice(cursor, span.start))
+ cursor = span.end
+ }
+ segments.push(text.slice(cursor))
+ return segments
+ },
+ }
+}
+
+/**
+ * Match `pattern` as an escaped literal on the built-in engine.
+ *
+ * Safe because an escaped literal cannot backtrack, and far quicker than RE2 —
+ * worth taking whenever the pattern has no metacharacter to interpret, or as a
+ * degradation path when RE2 rejects the syntax.
+ */
+export function literalRegex(pattern: string, options: LinearRegexOptions = {}): LinearRegex {
+ const source = escapeRegExp(pattern)
+ const caseFlag = options.ignoreCase ? 'i' : ''
+ // Non-global, so `exec`/`test` keep no `lastIndex` between calls and one
+ // instance is reusable — callers scan line-by-line, and recompiling per line
+ // would dominate the cost.
+ const scanner = new RegExp(source, caseFlag)
+ return {
+ test: (text) => scanner.test(text),
+ find: (text) => {
+ const match = scanner.exec(text)
+ return match ? match.index : -1
+ },
+ // Built lazily: no caller splits on a literal, so the second compile is
+ // only paid if one ever does.
+ split: (text) => text.split(new RegExp(source, `g${caseFlag}`)),
+ }
+}
+
+/**
+ * Compile `pattern` into a matcher that cannot backtrack.
+ *
+ * Returns `null` when RE2 cannot represent the pattern — invalid syntax, or
+ * constructs RE2 does not implement (lookaround, backreferences, repeat counts
+ * above 1000). Callers must handle `null` explicitly rather than silently
+ * falling back to the built-in engine, which would reintroduce the exposure
+ * this exists to remove.
+ */
+export function compileLinearRegex(
+ pattern: string,
+ options: LinearRegexOptions = {}
+): LinearRegex | null {
+ try {
+ const compiled = RE2JS.compile(
+ translateToRe2(pattern),
+ options.ignoreCase ? RE2JS.CASE_INSENSITIVE : 0
+ )
+ return {
+ test: (text) => compiled.matcher(text).find(),
+ find: (text) => {
+ const matcher = compiled.matcher(text)
+ return matcher.find() ? matcher.start() : -1
+ },
+ split: (text) => compiled.split(text),
+ }
+ } catch {
+ return null
+ }
+}
diff --git a/apps/sim/lib/core/utils/node-stream.ts b/apps/sim/lib/core/utils/node-stream.ts
new file mode 100644
index 00000000000..1288a2280d7
--- /dev/null
+++ b/apps/sim/lib/core/utils/node-stream.ts
@@ -0,0 +1,63 @@
+import type { Readable } from 'node:stream'
+
+/**
+ * Bridges a Node `Readable` into a WHATWG `ReadableStream` suitable for a `Response`
+ * body. Node's built-in `Readable.toWeb` is NOT used: its adapter throws an unhandled
+ * `ERR_INVALID_STATE` ("Controller is already closed") when the web stream is cancelled
+ * while the Node stream is still flowing — which happens whenever a consumer aborts a
+ * live body (a redirect hop cancelling the previous response, a browser cancelling a
+ * download mid-transfer). This bridge instead swallows a late enqueue after close and
+ * destroys the source on cancel, so cancelling a live body frees its socket cleanly.
+ * Source errors — a size-limit overrun, a storage read failing mid-archive — surface as
+ * the source's `error` event and reject the read.
+ */
+export function nodeReadableToWebStream(nodeStream: Readable): ReadableStream {
+ let settled = false
+ return new ReadableStream({
+ start(controller) {
+ nodeStream.on('data', (chunk: Buffer) => {
+ try {
+ // Copy, not a view: the producer may recycle the pooled buffer backing `chunk`
+ // after this handler returns, which would corrupt a chunk still queued for a
+ // slow consumer. `new Uint8Array(chunk)` allocates a fresh backing buffer.
+ controller.enqueue(new Uint8Array(chunk))
+ } catch {
+ // Controller already closed (consumer cancelled) — stop the source, drop the chunk.
+ nodeStream.destroy()
+ return
+ }
+ if ((controller.desiredSize ?? 1) <= 0) nodeStream.pause()
+ })
+ nodeStream.once('end', () => {
+ settled = true
+ try {
+ controller.close()
+ } catch {}
+ })
+ nodeStream.once('error', (err) => {
+ settled = true
+ try {
+ controller.error(err)
+ } catch {}
+ })
+ // An abort or upstream reset can `destroy()` the source with no `error` event;
+ // without this the reader would hang forever. `close` fires after every terminal
+ // path, so only act when `end`/`error` didn't already settle the stream.
+ nodeStream.once('close', () => {
+ if (settled) return
+ settled = true
+ try {
+ controller.error(new Error('Stream closed before completing'))
+ } catch {}
+ })
+ // Start paused so nothing buffers before the consumer pulls (backpressure).
+ nodeStream.pause()
+ },
+ pull() {
+ nodeStream.resume()
+ },
+ cancel(reason) {
+ nodeStream.destroy(reason instanceof Error ? reason : undefined)
+ },
+ })
+}
diff --git a/apps/sim/lib/execution/payloads/large-value-metadata.ts b/apps/sim/lib/execution/payloads/large-value-metadata.ts
index 41d56fbf2e2..c98f4756c5e 100644
--- a/apps/sim/lib/execution/payloads/large-value-metadata.ts
+++ b/apps/sim/lib/execution/payloads/large-value-metadata.ts
@@ -412,7 +412,7 @@ async function pruneStaleReferences(
SELECT 1
FROM ${pausedExecutions} AS pe
WHERE pe.execution_id = ref.execution_id
- AND pe.status = ANY(${LIVE_PAUSED_REFERENCE_STATUSES}::text[])
+ AND pe.status IN ${LIVE_PAUSED_REFERENCE_STATUSES}
)
)
OR ref.source NOT IN ('execution_log', 'paused_snapshot')
@@ -568,7 +568,7 @@ export function unreferencedLargeValuePredicate() {
SELECT 1
FROM ${pausedExecutions} AS pe
WHERE pe.execution_id = elvr.execution_id
- AND pe.status = ANY(${LIVE_PAUSED_REFERENCE_STATUSES}::text[])
+ AND pe.status IN ${LIVE_PAUSED_REFERENCE_STATUSES}
)
)
)
@@ -582,7 +582,7 @@ export function unreferencedLargeValuePredicate() {
SELECT 1
FROM ${pausedExecutions} AS owner_pe
WHERE owner_pe.execution_id = ${executionLargeValues.ownerExecutionId}
- AND owner_pe.status = ANY(${LIVE_PAUSED_REFERENCE_STATUSES}::text[])
+ AND owner_pe.status IN ${LIVE_PAUSED_REFERENCE_STATUSES}
)
AND NOT EXISTS (
SELECT 1
@@ -602,7 +602,7 @@ export function unreferencedLargeValuePredicate() {
SELECT 1
FROM ${pausedExecutions} AS parent_owner_pe
WHERE parent_owner_pe.execution_id = parent_value.owner_execution_id
- AND parent_owner_pe.status = ANY(${LIVE_PAUSED_REFERENCE_STATUSES}::text[])
+ AND parent_owner_pe.status IN ${LIVE_PAUSED_REFERENCE_STATUSES}
)
OR EXISTS (
SELECT 1
@@ -623,7 +623,7 @@ export function unreferencedLargeValuePredicate() {
SELECT 1
FROM ${pausedExecutions} AS parent_ref_pe
WHERE parent_ref_pe.execution_id = parent_ref.execution_id
- AND parent_ref_pe.status = ANY(${LIVE_PAUSED_REFERENCE_STATUSES}::text[])
+ AND parent_ref_pe.status IN ${LIVE_PAUSED_REFERENCE_STATUSES}
)
)
)
diff --git a/apps/sim/lib/execution/payloads/live-paused-statuses-sql.test.ts b/apps/sim/lib/execution/payloads/live-paused-statuses-sql.test.ts
new file mode 100644
index 00000000000..812b1d64944
--- /dev/null
+++ b/apps/sim/lib/execution/payloads/live-paused-statuses-sql.test.ts
@@ -0,0 +1,40 @@
+/**
+ * @vitest-environment node
+ */
+
+// Renders the real predicate against the real drizzle dialect and schema: the
+// bug this guards against is a SQL *rendering* bug, so the global drizzle-orm
+// and schema mocks would hide it entirely.
+import { describe, expect, it, vi } from 'vitest'
+
+vi.unmock('drizzle-orm')
+vi.unmock('@sim/db')
+vi.unmock('@sim/db/schema')
+
+process.env.DATABASE_URL ??= 'postgresql://user:pass@localhost:5432/test'
+
+const { PgDialect } = await import('drizzle-orm/pg-core')
+const { LIVE_PAUSED_REFERENCE_STATUSES, unreferencedLargeValuePredicate } = await import(
+ '@/lib/execution/payloads/large-value-metadata'
+)
+
+describe('unreferencedLargeValuePredicate SQL', () => {
+ const { sql: text, params } = new PgDialect().sqlToQuery(unreferencedLargeValuePredicate())
+
+ it('compares paused status against an IN list', () => {
+ expect(text).toMatch(/\.status IN \(\$\d+, \$\d+, \$\d+\)/)
+ expect(params).toEqual(expect.arrayContaining([...LIVE_PAUSED_REFERENCE_STATUSES]))
+ })
+
+ it('never emits ANY() over drizzle-expanded statuses', () => {
+ // Drizzle renders a JS array as `($1, $2, $3)` — correct for IN, but
+ // `ANY((...)::text[])` makes Postgres reject the whole statement with
+ // "cannot cast type record to text[]", which killed cleanup-logs for two months.
+ expect(text).not.toMatch(/ANY\(\(\$\d+/)
+ })
+
+ it('never emits a dangling comparison operator before IN', () => {
+ // `status = IN (...)` is a syntax error Postgres only reports at execution time.
+ expect(text).not.toMatch(/=\s*IN\s*\(/)
+ })
+})
diff --git a/apps/sim/lib/guardrails/validate_regex.test.ts b/apps/sim/lib/guardrails/validate_regex.test.ts
new file mode 100644
index 00000000000..905b869edb2
--- /dev/null
+++ b/apps/sim/lib/guardrails/validate_regex.test.ts
@@ -0,0 +1,67 @@
+/**
+ * @vitest-environment node
+ */
+
+import { describe, expect, it } from 'vitest'
+import { validateRegex, validateRegexPattern } from '@/lib/guardrails/validate_regex'
+
+describe('validateRegex', () => {
+ it('passes when the input matches', () => {
+ expect(validateRegex('order 12345 shipped', '\\d{5}')).toEqual({ passed: true })
+ })
+
+ it('fails with a reason when the input does not match', () => {
+ expect(validateRegex('no digits', '\\d{5}')).toEqual({
+ passed: false,
+ error: 'Input does not match regex pattern',
+ })
+ })
+
+ it('runs a catastrophic pattern in linear time', () => {
+ // Both the guardrail pattern and the text it checks are caller-influenced,
+ // and this executes on the shared event loop. `a*a*b` against this input
+ // measured 213s on JSC before the engine change.
+ const start = Date.now()
+ const result = validateRegex(`${'a'.repeat(10000)}!`, 'a*a*b')
+
+ expect(Date.now() - start).toBeLessThan(2000)
+ expect(result.passed).toBe(false)
+ })
+
+ it('reports syntax RE2 cannot evaluate rather than running it', () => {
+ const result = validateRegex('anything', '(?=foo)bar')
+ expect(result.passed).toBe(false)
+ expect(result.error).toContain('lookahead')
+ })
+
+ it('reports invalid syntax distinctly from unsupported syntax', () => {
+ const result = validateRegex('anything', '(')
+ expect(result.passed).toBe(false)
+ expect(result.error).toContain('Invalid regex pattern')
+ })
+})
+
+describe('validateRegexPattern', () => {
+ it('accepts a valid pattern', () => {
+ expect(validateRegexPattern('\\d{3}-\\d{4}')).toEqual({ valid: true })
+ })
+
+ it('rejects an empty pattern', () => {
+ expect(validateRegexPattern('')).toMatchObject({ valid: false })
+ })
+
+ it('rejects invalid syntax', () => {
+ expect(validateRegexPattern('(')).toMatchObject({ valid: false })
+ })
+
+ it.each([
+ ['lookbehind', '(?<=id: )\\w+'],
+ ['optional group', '(?:https?://)?example\\.com'],
+ ['nested quantifier', '(a+)+$'],
+ ])('accepts %s, which the removed safe-regex2 screen rejected', (_label, pattern) => {
+ // These are valid Presidio patterns that could not be saved before. The
+ // screen that blocked them caught no real ReDoS (it passes `a*a*b`), and
+ // these run in Presidio rather than in this process.
+ expect(validateRegexPattern(pattern)).toEqual({ valid: true })
+ })
+})
diff --git a/apps/sim/lib/guardrails/validate_regex.ts b/apps/sim/lib/guardrails/validate_regex.ts
index 442daa7b3ee..f8a90dc6e5e 100644
--- a/apps/sim/lib/guardrails/validate_regex.ts
+++ b/apps/sim/lib/guardrails/validate_regex.ts
@@ -1,4 +1,8 @@
-import safe from 'safe-regex2'
+import { createLogger } from '@sim/logger'
+import { getErrorMessage } from '@sim/utils/errors'
+import { compileLinearRegex } from '@/lib/core/security/linear-regex'
+
+const logger = createLogger('ValidateRegex')
/**
* Validate if input matches regex pattern
@@ -15,9 +19,24 @@ export interface RegexPatternValidation {
}
/**
- * Validate a regex pattern's syntax and safety without matching it against input:
- * it must compile (`new RegExp`) and pass `safe-regex2`'s catastrophic-backtracking
- * screen. Shared by the custom-pattern editor UI and any pre-flight boundary check.
+ * Validate a PII custom pattern's syntax before it is persisted and handed to
+ * Presidio. Shared by the custom-pattern editor UI and the write boundary.
+ *
+ * Syntax only, deliberately. This previously also ran a `safe-regex2`
+ * catastrophic-backtracking screen, which was removed because it was pure cost:
+ * it screens star height alone and is documented as having false negatives — it
+ * passes `(a|a)*b`, and `a*a*b` defeats it and every syntactic rule of its kind
+ * — while rejecting patterns that work perfectly well, including lookbehind
+ * (`(?<=id: )\w+`) and optional groups (`(?:https?://)?example\.com`). It
+ * blocked valid rules and stopped nothing.
+ *
+ * Nor could a screen here be made sound: these patterns execute in Presidio's
+ * Python engine, which backtracks on shapes RE2 accepts, so RE2-representability
+ * says nothing about their runtime there. Presidio's own request timeout is the
+ * real bound — note it fails open on timeout, leaving PII unredacted.
+ *
+ * Anything that matches a caller-supplied pattern *in this process* must use
+ * `compileLinearRegex` from `@/lib/core/security/linear-regex` instead.
*/
export function validateRegexPattern(pattern: string): RegexPatternValidation {
if (pattern.length === 0) {
@@ -26,34 +45,42 @@ export function validateRegexPattern(pattern: string): RegexPatternValidation {
try {
new RegExp(pattern)
} catch (error) {
- return { valid: false, error: `Invalid regex: ${(error as Error).message}` }
- }
- if (!safe(pattern)) {
- return {
- valid: false,
- error: 'Pattern rejected: potentially unsafe (catastrophic backtracking)',
- }
+ return { valid: false, error: `Invalid regex: ${getErrorMessage(error)}` }
}
return { valid: true }
}
+/**
+ * Match `inputStr` against a caller-defined guardrail `pattern`.
+ *
+ * Both the pattern and the input are caller-influenced and this runs on the
+ * shared event loop, so matching goes through RE2 — a backtracking engine here
+ * lets one guardrail rule stall every other request on the instance. Patterns
+ * RE2 cannot represent (lookaround, backreferences) are reported rather than
+ * run on the built-in engine, which would reintroduce that exposure.
+ */
export function validateRegex(inputStr: string, pattern: string): ValidationResult {
- let regex: RegExp
try {
- regex = new RegExp(pattern)
- } catch (error: any) {
- return { passed: false, error: `Invalid regex pattern: ${error.message}` }
+ new RegExp(pattern)
+ } catch (error) {
+ return { passed: false, error: `Invalid regex pattern: ${getErrorMessage(error)}` }
}
- if (!safe(pattern)) {
+ const regex = compileLinearRegex(pattern)
+ if (!regex) {
+ // A rule that used lookaround worked before this became RE2-only and now
+ // fails closed, which reads to the workspace as the guardrail tripping on
+ // every input. Log it so an operator can find and rewrite the rule from
+ // logs rather than from user reports.
+ logger.warn('Guardrail regex uses syntax RE2 cannot evaluate; failing closed', { pattern })
return {
passed: false,
- error: 'Regex pattern rejected: potentially unsafe (catastrophic backtracking)',
+ error:
+ 'Regex pattern uses syntax that cannot be evaluated safely (lookahead, lookbehind and backreferences are unsupported). Rewrite it without those constructs.',
}
}
- const match = regex.test(inputStr)
- if (match) {
+ if (regex.test(inputStr)) {
return { passed: true }
}
return { passed: false, error: 'Input does not match regex pattern' }
diff --git a/apps/sim/lib/logs/log-views.test.ts b/apps/sim/lib/logs/log-views.test.ts
index 9b31ed4feb5..de444485adc 100644
--- a/apps/sim/lib/logs/log-views.test.ts
+++ b/apps/sim/lib/logs/log-views.test.ts
@@ -32,6 +32,7 @@ vi.mock('@/lib/execution/payloads/store', () => ({
materializeLargeValueRef: materializeLargeValueRefMock,
}))
+import { sleep } from '@sim/utils/helpers'
import type { TraceSpan } from '@/lib/logs/types'
import { grepSpans, type LogViewContext, toFull, toOverview } from './log-views'
@@ -169,10 +170,114 @@ describe('grepSpans', () => {
expect(result.matches.some((m) => m.field === 'output')).toBe(true)
})
- it('falls back to literal substring on invalid regex', async () => {
- const spans = [span({ output: { v: 'value a(b found' } })]
- const result = await grepSpans(spans, '(', ctx)
+ it.each([
+ ['character class', 'status=\\d+'],
+ ['anchor', '^Agent'],
+ ['alternation', '(openai|anthropic)'],
+ ['word boundary', '\\bstatus\\b'],
+ ['bounded quantifier', '\\d{4}-\\d{2}-\\d{2}'],
+ ['wildcard', 'called.*status'],
+ ])('interprets %s regex syntax', async (_label, pattern) => {
+ // None of these patterns occur literally in the span, so a match proves the
+ // regex was interpreted. `^Agent` anchors against the name field, the rest
+ // against output — hence the field-agnostic assertion.
+ const spans = [span({ output: { v: 'called api.openai.com -> status=503 on 2026-01-01' } })]
+ const result = await grepSpans(spans, pattern, ctx)
+ expect(result.matches.length).toBeGreaterThan(0)
+ expect(result.patternNotice).toBeUndefined()
+ })
+
+ it('falls back to a literal, with a notice, for syntax RE2 does not implement', async () => {
+ const spans = [span({ output: { v: 'id: abc and (?=x) literally here' } })]
+
+ const lookahead = await grepSpans(spans, '(?=x)', ctx)
+ expect(lookahead.matches.some((m) => m.field === 'output')).toBe(true)
+ expect(lookahead.patternNotice).toContain('RE2')
+
+ // Unbalanced paren is invalid in both engines; still degrades to a literal.
+ const invalid = await grepSpans([span({ output: { v: 'value a(b' } })], '(', ctx)
+ expect(invalid.matches.some((m) => m.field === 'output')).toBe(true)
+ expect(invalid.patternNotice).toContain('RE2')
+ })
+
+ it('takes the built-in engine for a metacharacter-free pattern', async () => {
+ const spans = [span({ output: { v: 'saw ECONNREFUSED here' } })]
+ const result = await grepSpans(spans, 'ECONNREFUSED', ctx)
expect(result.matches.some((m) => m.field === 'output')).toBe(true)
+ expect(result.patternNotice).toBeUndefined()
+ })
+
+ it.each([
+ ['nested quantifier', '(a+)+$'],
+ ['duplicate alternation, passes safe-regex2', '(a|a)*b'],
+ ['adjacent quantifiers, passes every structural screen', 'a*a*b'],
+ ])('runs a catastrophic pattern in linear time (%s)', async (_label, pattern) => {
+ // Each blocks the event loop for minutes on a backtracking engine:
+ // `a*a*b` measured 213s on JSC / 132s on V8 over a 10k-character run.
+ // RE2 has no backtracking, so these are matched normally and stay fast.
+ const spans = [span({ output: { v: `${'a'.repeat(10000)}!` } })]
+
+ const start = Date.now()
+ const result = await grepSpans(spans, pattern, ctx)
+ const elapsedMs = Date.now() - start
+
+ expect(elapsedMs).toBeLessThan(1000)
+ expect(result.truncated).toBe(false)
+ })
+
+ it('matches a long pattern literally without a length cap', async () => {
+ const pattern = `${'x'.repeat(600)}needle`
+ const spans = [span({ output: { v: pattern } })]
+ const result = await grepSpans(spans, pattern, ctx)
+ expect(result.matches.some((m) => m.field === 'output')).toBe(true)
+ })
+
+ it('stops scanning and marks truncated once the character budget is exhausted', async () => {
+ const spans = [
+ span({ id: 'a', output: { v: 'x'.repeat(400) } }),
+ span({ id: 'b', output: { v: 'needle' } }),
+ ]
+ const result = await grepSpans(spans, 'needle', ctx, { maxScannedChars: 100 })
+ expect(result.matches).toEqual([])
+ expect(result.truncated).toBe(true)
+ })
+
+ it('stops scanning and marks truncated once the match-time budget is exhausted', async () => {
+ const spans = [span({ output: { v: 'needle' } })]
+ const result = await grepSpans(spans, 'needle', ctx, { matchTimeBudgetMs: 0 })
+ expect(result.matches).toEqual([])
+ expect(result.truncated).toBe(true)
+ })
+
+ it('accumulates match time and truncates once the budget is spent', async () => {
+ // Guards the accumulation in `findTimed`: with that line removed,
+ // matchTimeMs stays 0, the budget never trips, and every span is scanned.
+ const big = 'x'.repeat(400_000)
+ const spans = [
+ span({ id: 'a', output: { v: big } }),
+ span({ id: 'b', output: { v: big } }),
+ span({ id: 'c', output: { v: `${big} needle` } }),
+ ]
+
+ const result = await grepSpans(spans, 'needle|nomatch', ctx, { matchTimeBudgetMs: 1 })
+
+ expect(result.truncated).toBe(true)
+ expect(result.matches).toEqual([])
+ })
+
+ it('does not charge blob-store I/O to the match-time budget', async () => {
+ // Each slice read sleeps well past the budget: only time spent matching
+ // counts, so a slow-but-legitimate grep must still return complete results.
+ readLargeArrayManifestSliceMock.mockImplementation(async (_m: unknown, start: number) => {
+ await sleep(30)
+ return start === 400 ? [{ v: 'found the needle here' }] : [{ v: 'nothing' }]
+ })
+ const spans = [span({ output: manifest(500) as any })]
+
+ const result = await grepSpans(spans, 'needle', ctx, { matchTimeBudgetMs: 50 })
+
+ expect(result.matches.some((m) => m.field === 'output')).toBe(true)
+ expect(result.truncated).toBe(false)
})
it('returns empty for empty traceSpans', async () => {
diff --git a/apps/sim/lib/logs/log-views.ts b/apps/sim/lib/logs/log-views.ts
index e7e4feaceee..1f679ce53b0 100644
--- a/apps/sim/lib/logs/log-views.ts
+++ b/apps/sim/lib/logs/log-views.ts
@@ -1,3 +1,4 @@
+import { compileLinearRegex, isPlainText, literalRegex } from '@/lib/core/security/linear-regex'
import {
materializeLargeArrayManifest,
readLargeArrayManifestSlice,
@@ -23,6 +24,27 @@ const DEFAULT_MAX_MATCHES = 50
const DEFAULT_MAX_SNIPPET_CHARS = 500
const DEFAULT_MAX_SLICES_SCANNED = 200
+/**
+ * Cumulative time the pattern itself may spend matching, across all spans/fields.
+ *
+ * Deliberately counts only time spent matching, not the grep's wall clock: the
+ * scan awaits blob-store reads (array slices, large-value refs) between matches,
+ * and charging that I/O to the budget would truncate slow-but-legitimate greps
+ * under load. Matching is the only part that occupies the event loop, so it is
+ * the only part worth bounding.
+ *
+ * RE2JS trades throughput for its linear-time guarantee — roughly 100x slower
+ * than the built-in engine, ~25ms per megabyte — so on a very large trace this
+ * budget is what actually caps the scan rather than a formality.
+ */
+const DEFAULT_MATCH_TIME_BUDGET_MS = 5_000
+/**
+ * Total characters a single grep may run the pattern over. Bounds the work one
+ * request can demand across every span and slice; set well above any realistic
+ * trace so normal greps never trip it.
+ */
+const DEFAULT_MAX_SCANNED_CHARS = 64 * 1024 * 1024
+
// ---------------------------------------------------------------------------
// Overview (Level 2): block tree with timing + cost, NO input/output.
// ---------------------------------------------------------------------------
@@ -171,40 +193,86 @@ export interface GrepSpanMatch {
export interface GrepSpansResult {
matches: GrepSpanMatch[]
+ /**
+ * Whether the scan stopped early — because a budget was exhausted, the slice
+ * cap was hit, or `maxMatches` was reached. It is a "there may be more" flag,
+ * not proof that trace was left unread: reaching `maxMatches` on the final
+ * match sets it even when nothing remained. Treat it as a prompt to narrow
+ * the pattern, never as a count.
+ */
truncated: boolean
+ /**
+ * Present only when the pattern used syntax RE2 does not implement and was
+ * therefore matched literally. The tool catalog cannot warn up front — it is
+ * generated from a contract in another repository — so the caller is told
+ * here rather than reading zero matches as "not present in the trace".
+ */
+ patternNotice?: string
}
export interface GrepSpansOptions {
maxMatches?: number
maxSnippetChars?: number
maxSlicesScanned?: number
+ maxScannedChars?: number
+ matchTimeBudgetMs?: number
}
interface GrepState {
matches: GrepSpanMatch[]
slicesScanned: number
+ scannedChars: number
+ matchTimeMs: number
truncated: boolean
maxMatches: number
maxSnippetChars: number
maxSlicesScanned: number
- regex: RegExp
+ maxScannedChars: number
+ matchTimeBudgetMs: number
+ find: FindMatch
}
-function escapeRegExp(input: string): string {
- return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
+/** Index of the first case-insensitive match in `text`, or -1. */
+type FindMatch = (text: string) => number
+
+/**
+ * Compile a caller-supplied grep pattern into a matcher that cannot backtrack.
+ *
+ * Trace text is attacker-influenced — a workflow can emit arbitrarily long
+ * uniform runs into its own block outputs — and matching runs synchronously on
+ * the shared event loop, so a backtracking engine lets one request stall every
+ * other request on the instance. See `@/lib/core/security/linear-regex` for why
+ * the engine changed rather than the pattern being screened.
+ *
+ * A pattern with no metacharacter takes the built-in engine, which is ~100x
+ * quicker and identical in meaning when there is nothing to interpret. Syntax
+ * RE2 cannot represent degrades to a literal with a notice, so the caller knows
+ * its regex was not applied instead of reading zero matches as "not present".
+ */
+function compilePattern(pattern: string): { find: FindMatch; notice?: string } {
+ if (isPlainText(pattern)) return { find: literalRegex(pattern, { ignoreCase: true }).find }
+
+ const compiled = compileLinearRegex(pattern, { ignoreCase: true })
+ if (compiled) return { find: compiled.find }
+
+ return {
+ find: literalRegex(pattern, { ignoreCase: true }).find,
+ notice:
+ 'Pattern is not valid RE2 syntax (lookahead, lookbehind and backreferences are unsupported), so it was matched as a literal string. Rewrite it without those constructs to search by regex.',
+ }
}
-function buildRegex(pattern: string): RegExp {
+function findTimed(text: string, state: GrepState): number {
+ const started = performance.now()
try {
- return new RegExp(pattern, 'i')
- } catch {
- return new RegExp(escapeRegExp(pattern), 'i')
+ return state.find(text)
+ } finally {
+ state.matchTimeMs += performance.now() - started
}
}
-function snippetAround(text: string, regex: RegExp, maxChars: number): string {
- const m = regex.exec(text)
- const index = m ? m.index : 0
+function snippetAround(text: string, index: number, state: GrepState): string {
+ const maxChars = state.maxSnippetChars
const half = Math.floor(maxChars / 2)
const start = Math.max(0, index - half)
const end = Math.min(text.length, start + maxChars)
@@ -214,7 +282,12 @@ function snippetAround(text: string, regex: RegExp, maxChars: number): string {
}
function done(state: GrepState): boolean {
- return state.truncated || state.matches.length >= state.maxMatches
+ if (state.truncated || state.matches.length >= state.maxMatches) return true
+ if (state.matchTimeMs >= state.matchTimeBudgetMs) {
+ state.truncated = true
+ return true
+ }
+ return false
}
function recordIfMatch(
@@ -224,15 +297,19 @@ function recordIfMatch(
state: GrepState
): void {
if (done(state)) return
- state.regex.lastIndex = 0
- if (!state.regex.test(text)) return
- state.regex.lastIndex = 0
+ if (state.scannedChars + text.length > state.maxScannedChars) {
+ state.truncated = true
+ return
+ }
+ state.scannedChars += text.length
+ const index = findTimed(text, state)
+ if (index < 0) return
state.matches.push({
spanId: span.id,
blockId: span.blockId,
name: span.name,
field,
- snippet: snippetAround(text, state.regex, state.maxSnippetChars),
+ snippet: snippetAround(text, index, state),
})
if (state.matches.length >= state.maxMatches) state.truncated = true
}
@@ -305,6 +382,12 @@ function safeStringify(value: unknown): string {
* directly; large-array I/O is streamed slice-by-slice (each released before the
* next); single large refs are materialized under a byte cap (falling back to
* the ref preview). Only bounded match snippets are accumulated.
+ *
+ * `pattern` is matched by a non-backtracking engine — see `compilePattern` — so
+ * no pattern can blow up on any input. Two budgets bound total work on top of
+ * that: a character budget and a cumulative match-time budget. Neither counts
+ * the blob-store I/O this scan awaits, so a slow-but-legitimate grep is not
+ * truncated merely for being slow.
*/
export async function grepSpans(
spans: TraceSpan[],
@@ -312,14 +395,19 @@ export async function grepSpans(
ctx: LogViewContext,
opts?: GrepSpansOptions
): Promise {
+ const compiled = compilePattern(pattern)
const state: GrepState = {
matches: [],
slicesScanned: 0,
+ scannedChars: 0,
+ matchTimeMs: 0,
truncated: false,
maxMatches: opts?.maxMatches ?? DEFAULT_MAX_MATCHES,
maxSnippetChars: opts?.maxSnippetChars ?? DEFAULT_MAX_SNIPPET_CHARS,
maxSlicesScanned: opts?.maxSlicesScanned ?? DEFAULT_MAX_SLICES_SCANNED,
- regex: buildRegex(pattern),
+ maxScannedChars: opts?.maxScannedChars ?? DEFAULT_MAX_SCANNED_CHARS,
+ matchTimeBudgetMs: opts?.matchTimeBudgetMs ?? DEFAULT_MATCH_TIME_BUDGET_MS,
+ find: compiled.find,
}
const walk = async (list: TraceSpan[]): Promise => {
@@ -335,5 +423,9 @@ export async function grepSpans(
}
await walk(spans)
- return { matches: state.matches, truncated: state.truncated }
+ return {
+ matches: state.matches,
+ truncated: state.truncated,
+ ...(compiled.notice ? { patternNotice: compiled.notice } : {}),
+ }
}
diff --git a/apps/sim/lib/media/ffmpeg.ts b/apps/sim/lib/media/ffmpeg.ts
index 2f3bd000285..bcfa0b6adf0 100644
--- a/apps/sim/lib/media/ffmpeg.ts
+++ b/apps/sim/lib/media/ffmpeg.ts
@@ -1,10 +1,8 @@
import { execSync } from 'node:child_process'
-import fsSync from 'node:fs'
import fs from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import { createLogger } from '@sim/logger'
-import ffmpegStatic from 'ffmpeg-static'
import ffmpeg from 'fluent-ffmpeg'
const logger = createLogger('MediaFfmpeg')
@@ -12,7 +10,7 @@ const logger = createLogger('MediaFfmpeg')
let ffmpegInitialized = false
let ffmpegPath: string | null = null
-/** Lazy FFmpeg binary resolution (ffmpeg-static, then system), mirroring lib/audio/extractor.ts. */
+/** Lazy system FFmpeg binary resolution, mirroring lib/audio/extractor.ts. */
function ensureFfmpeg(): void {
if (ffmpegInitialized) {
if (!ffmpegPath) {
@@ -24,17 +22,6 @@ function ensureFfmpeg(): void {
}
ffmpegInitialized = true
- if (ffmpegStatic && typeof ffmpegStatic === 'string') {
- try {
- fsSync.accessSync(ffmpegStatic, fsSync.constants.X_OK)
- ffmpegPath = ffmpegStatic
- ffmpeg.setFfmpegPath(ffmpegPath)
- return
- } catch {
- // fall through to system ffmpeg
- }
- }
-
try {
const cmd = process.platform === 'win32' ? 'where ffmpeg' : 'which ffmpeg'
ffmpegPath = execSync(cmd, { encoding: 'utf-8' }).trim().split('\n')[0]
diff --git a/apps/sim/lib/uploads/client/download.ts b/apps/sim/lib/uploads/client/download.ts
index 9cbdd88d263..ac873f66ae8 100644
--- a/apps/sim/lib/uploads/client/download.ts
+++ b/apps/sim/lib/uploads/client/download.ts
@@ -1,5 +1,35 @@
+import { requestRaw } from '@/lib/api/client/request'
+import { downloadWorkspaceFileItemsContract } from '@/lib/api/contracts/workspace-file-folders'
import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace'
+export function saveBlob(blob: Blob, fileName: string): void {
+ const objectUrl = URL.createObjectURL(blob)
+ const anchor = document.createElement('a')
+ anchor.href = objectUrl
+ anchor.download = fileName
+ // Attached before clicking: a detached anchor works in current browsers, but every
+ // other download helper in the app attaches, and a silent no-op here would look
+ // exactly like a download that never started.
+ document.body.appendChild(anchor)
+ anchor.click()
+ document.body.removeChild(anchor)
+ // Deferred: revoking synchronously after click() can race the download starting.
+ setTimeout(() => URL.revokeObjectURL(objectUrl), 0)
+}
+
+function fileNameFromDisposition(response: Response, fallback: string): string {
+ const disposition = response.headers.get('Content-Disposition') ?? ''
+ const encoded = disposition.match(/filename\*=UTF-8''([^;]+)/i)?.[1]
+ if (encoded) {
+ try {
+ return decodeURIComponent(encoded)
+ } catch {
+ // Fall through to the plain form.
+ }
+ }
+ return disposition.match(/filename="([^"]+)"/)?.[1] ?? fallback
+}
+
export async function triggerFileDownload(record: WorkspaceFileRecord): Promise {
const isMarkdown =
record.type === 'text/markdown' ||
@@ -10,17 +40,32 @@ export async function triggerFileDownload(record: WorkspaceFileRecord): Promise<
? `/api/files/export/${encodeURIComponent(record.id)}`
: `/api/files/serve/${encodeURIComponent(record.key)}?context=workspace&t=${Date.now()}`
+ // boundary-raw-fetch: binary download read as a blob; these paths have no contract
const response = await fetch(url, { cache: 'no-store' })
- if (!response.ok) throw new Error(`Failed to download file: ${response.statusText}`)
+ if (!response.ok) throw new Error(`Failed to download "${record.name}"`)
- const blob = await response.blob()
- const objectUrl = URL.createObjectURL(blob)
- const a = document.createElement('a')
- a.href = objectUrl
- a.download =
- response.headers.get('Content-Disposition')?.match(/filename="([^"]+)"/)?.[1] ?? record.name
- document.body.appendChild(a)
- a.click()
- document.body.removeChild(a)
- URL.revokeObjectURL(objectUrl)
+ saveBlob(await response.blob(), fileNameFromDisposition(response, record.name))
+}
+
+/**
+ * Download a selection of files as a zip. Fetched rather than navigated to, so a
+ * rejection — a document still compiling, an entry too large — surfaces as an error the
+ * caller can show in place instead of replacing the page with raw JSON. `requestRaw`
+ * throws an `ApiClientError` carrying the route's own message.
+ */
+export async function triggerArchiveDownload(input: {
+ workspaceId: string
+ fileIds?: string[]
+ folderIds?: string[]
+}): Promise {
+ const response = await requestRaw(
+ downloadWorkspaceFileItemsContract,
+ {
+ params: { id: input.workspaceId },
+ query: { fileIds: input.fileIds ?? [], folderIds: input.folderIds ?? [] },
+ },
+ { cache: 'no-store' }
+ )
+
+ saveBlob(await response.blob(), fileNameFromDisposition(response, 'workspace-files.zip'))
}
diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts
index 6967a7d313f..b1ae9b45da2 100644
--- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts
+++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts
@@ -21,6 +21,7 @@ import { normalizeVfsSegment } from '@/lib/copilot/vfs/normalize-segment'
import { canonicalWorkspaceFilePath, decodeVfsPathSegments } from '@/lib/copilot/vfs/path-utils'
import { resolveWorkflowAliasForWorkspace } from '@/lib/copilot/vfs/workflow-alias-resolver'
import { isReservedWorkflowAliasBackingDisplayPath } from '@/lib/copilot/vfs/workflow-aliases'
+import { generateRequestId } from '@/lib/core/utils/request'
import { generateRestoreName } from '@/lib/core/utils/restore-name'
import type { DbOrTx } from '@/lib/db/types'
import { getServePathPrefix } from '@/lib/uploads'
@@ -968,7 +969,42 @@ export async function getWorkspaceFile(
}
/**
- * Download workspace file content
+ * Download the bytes a user should actually receive for a workspace file.
+ *
+ * Generated docs (docx/pptx/pdf/xlsx) store their GENERATION SOURCE as the primary
+ * file, so {@link fetchWorkspaceFileBuffer} hands back JavaScript/Python text under
+ * a `.docx` name. This resolves the rendered artifact instead, and is what every
+ * download/attachment surface should call. Reach for the raw reader only when the
+ * source itself is wanted (style extraction, compile checks, the copilot VFS).
+ *
+ * Throws `DocCompileUserError` when a generated doc's artifact is still compiling —
+ * callers turn that into a retryable 409 rather than shipping source.
+ */
+export async function fetchServableWorkspaceFileBuffer(
+ fileRecord: WorkspaceFileRecord,
+ options: { maxBytes?: number; signal?: AbortSignal; requestId?: string } = {}
+): Promise<{ buffer: Buffer; contentType: string }> {
+ const { downloadServableFileFromStorage } = await import('@/lib/uploads/utils/file-utils.server')
+
+ return downloadServableFileFromStorage(
+ {
+ id: fileRecord.id,
+ name: fileRecord.name,
+ url: fileRecord.url ?? fileRecord.path,
+ size: fileRecord.size,
+ type: fileRecord.type,
+ key: fileRecord.key,
+ context: fileRecord.storageContext ?? 'workspace',
+ },
+ options.requestId ?? generateRequestId(),
+ logger,
+ options
+ )
+}
+
+/**
+ * Download raw workspace file content. For generated docs this is the GENERATION
+ * SOURCE, not the rendered document — see {@link fetchServableWorkspaceFileBuffer}.
*/
export async function fetchWorkspaceFileBuffer(
fileRecord: WorkspaceFileRecord,
diff --git a/apps/sim/lib/uploads/utils/file-utils.server.ts b/apps/sim/lib/uploads/utils/file-utils.server.ts
index cc230df8108..97013439e77 100644
--- a/apps/sim/lib/uploads/utils/file-utils.server.ts
+++ b/apps/sim/lib/uploads/utils/file-utils.server.ts
@@ -20,6 +20,7 @@ import {
getMimeTypeFromExtension,
inferContextFromKey,
isInternalFileUrl,
+ isRenderableDocumentName,
processSingleFileToUserFile,
type RawFileInput,
resolveTrustedFileContext,
@@ -375,8 +376,8 @@ export async function downloadServableFileFromStorage(
// Cheap pre-filter so only generated-doc candidates pay for the heavier resolver
// import below.
- const ext = getFileExtension(userFile.name)
- if (ext !== 'pdf' && ext !== 'docx' && ext !== 'pptx' && ext !== 'xlsx') {
+ if (!isRenderableDocumentName(userFile.name)) {
+ const ext = getFileExtension(userFile.name)
return { buffer, contentType: userFile.type || getMimeTypeFromExtension(ext) }
}
diff --git a/apps/sim/lib/uploads/utils/file-utils.ts b/apps/sim/lib/uploads/utils/file-utils.ts
index 7837617a0cc..cd36052fdbe 100644
--- a/apps/sim/lib/uploads/utils/file-utils.ts
+++ b/apps/sim/lib/uploads/utils/file-utils.ts
@@ -210,6 +210,44 @@ export function getFileExtension(filename: string): string {
return lastDot !== -1 ? filename.slice(lastDot + 1).toLowerCase() : ''
}
+/**
+ * Extensions whose stored bytes may be a generation source that renders to a larger
+ * binary. Everything else stores exactly what it serves, so its declared size is
+ * an accurate byte budget.
+ */
+const RENDERABLE_DOCUMENT_EXTENSIONS = new Set(['pdf', 'docx', 'pptx', 'xlsx'])
+
+/**
+ * Content types under which a generated document's *generation source* is stored. A
+ * file carrying one of these renders to something other than its stored bytes, so any
+ * surface that hands out the file itself has to resolve it first. Both PDF generators
+ * are here: the E2B path stores Python, the isolated-vm path stores pdf-lib JS.
+ */
+export const GENERATED_DOCUMENT_SOURCE_TYPES = new Set([
+ 'text/x-docxjs',
+ 'text/x-pptxgenjs',
+ 'text/x-pdflibjs',
+ 'text/x-python-pdf',
+ 'text/x-python-xlsx',
+])
+
+/** True when the stored bytes for `contentType` are a generation source. */
+export function isGeneratedDocumentSourceType(contentType: string | undefined | null): boolean {
+ return contentType ? GENERATED_DOCUMENT_SOURCE_TYPES.has(contentType) : false
+}
+
+/**
+ * Ceiling on a single rendered generated document. A generator source is text and is
+ * orders of magnitude smaller than the document it produces, so the declared size is no
+ * bound at all and the rendered bytes need a cap of their own.
+ */
+export const MAX_RENDERED_DOCUMENT_BYTES = 50 * 1024 * 1024
+
+/** True when `fileName` may be backed by a generation source rather than final bytes. */
+export function isRenderableDocumentName(fileName: string): boolean {
+ return RENDERABLE_DOCUMENT_EXTENSIONS.has(getFileExtension(fileName))
+}
+
const ARCHIVE_EXTENSIONS = new Set(SUPPORTED_ARCHIVE_EXTENSIONS)
/**
diff --git a/apps/sim/lib/uploads/utils/servable-file-response.ts b/apps/sim/lib/uploads/utils/servable-file-response.ts
index 1e7d1a6124b..63ffb0223f0 100644
--- a/apps/sim/lib/uploads/utils/servable-file-response.ts
+++ b/apps/sim/lib/uploads/utils/servable-file-response.ts
@@ -1,6 +1,23 @@
import { NextResponse } from 'next/server'
import { DocCompileUserError } from '@/lib/copilot/tools/server/files/doc-compile'
+/** True when `error` means a generated document's artifact is still compiling. */
+export function isDocNotReadyError(error: unknown): error is DocCompileUserError {
+ return error instanceof DocCompileUserError
+}
+
+/**
+ * Message for a still-compiling generated document. Batch callers pass the names
+ * they resolved so the copy says which documents to wait on.
+ */
+export function docNotReadyMessage(fileNames?: string[]): string {
+ if (!fileNames || fileNames.length === 0) {
+ return 'A document is still being generated. Wait for it to finish, then try again.'
+ }
+ const subject = fileNames.length === 1 ? 'A document is' : `${fileNames.length} documents are`
+ return `${subject} still being generated: ${fileNames.join(', ')}. Wait for them to finish, then try again.`
+}
+
/**
* Canonical retryable response for an attachment/upload whose generated-document
* artifact is still compiling. Returns the 409 when `error` is a
@@ -8,16 +25,13 @@ import { DocCompileUserError } from '@/lib/copilot/tools/server/files/doc-compil
* otherwise `null` so the caller falls through to its own error handling. Shared
* by every tool route that downloads workspace files so the status, body shape,
* and user-facing copy stay identical instead of being re-typed per route.
+ *
+ * Routes whose error envelope differs, or that resolved a batch and want the pending
+ * files named, build the 409 themselves from {@link docNotReadyMessage}.
*/
export function docNotReadyResponse(error: unknown): NextResponse | null {
- if (error instanceof DocCompileUserError) {
- return NextResponse.json(
- {
- success: false,
- error: 'A document is still being generated. Wait for it to finish, then try again.',
- },
- { status: 409 }
- )
+ if (isDocNotReadyError(error)) {
+ return NextResponse.json({ success: false, error: docNotReadyMessage() }, { status: 409 })
}
return null
}
diff --git a/apps/sim/lib/workflows/operations/import-export-roundtrip.test.ts b/apps/sim/lib/workflows/operations/import-export-roundtrip.test.ts
new file mode 100644
index 00000000000..3735775f21c
--- /dev/null
+++ b/apps/sim/lib/workflows/operations/import-export-roundtrip.test.ts
@@ -0,0 +1,152 @@
+/**
+ * @vitest-environment node
+ *
+ * End-to-end round trip through the REAL sanitizer and importer — no mocks of
+ * either — pinning the property the export/import API pair exists to provide:
+ * a workflow exported by `GET /api/v1/workflows/[id]/export` must re-import
+ * through `POST /api/v1/workflows/import` with its graph intact.
+ *
+ * Guards specifically against silent structural loss (child blocks inside loop
+ * and parallel containers, dangling references after id regeneration) that
+ * mock-heavy route tests cannot catch.
+ */
+
+import { describe, expect, it } from 'vitest'
+import { parseWorkflowJson } from '@/lib/workflows/operations/import-export'
+import { sanitizeForExport } from '@/lib/workflows/sanitization/json-sanitizer'
+
+function makeSourceState() {
+ return {
+ blocks: {
+ starter: {
+ id: 'starter',
+ type: 'starter',
+ name: 'Start',
+ position: { x: 0, y: 0 },
+ subBlocks: { input: { id: 'input', type: 'short-input', value: 'hello' } },
+ outputs: {},
+ enabled: true,
+ },
+ loop1: {
+ id: 'loop1',
+ type: 'loop',
+ name: 'Loop 1',
+ position: { x: 200, y: 0 },
+ subBlocks: {},
+ outputs: {},
+ enabled: true,
+ data: { width: 500, height: 300, count: 3, loopType: 'for' },
+ },
+ childInLoop: {
+ id: 'childInLoop',
+ type: 'function',
+ name: 'Child',
+ position: { x: 50, y: 50 },
+ subBlocks: { code: { id: 'code', type: 'code', value: 'return 1' } },
+ outputs: {},
+ enabled: true,
+ data: { parentId: 'loop1', extent: 'parent' },
+ },
+ par1: {
+ id: 'par1',
+ type: 'parallel',
+ name: 'Parallel 1',
+ position: { x: 800, y: 0 },
+ subBlocks: {},
+ outputs: {},
+ enabled: true,
+ data: { width: 500, height: 300, count: 2, parallelType: 'count' },
+ },
+ childInPar: {
+ id: 'childInPar',
+ type: 'function',
+ name: 'Child P',
+ position: { x: 60, y: 60 },
+ subBlocks: {},
+ outputs: {},
+ enabled: true,
+ data: { parentId: 'par1', extent: 'parent' },
+ },
+ } as Record,
+ edges: [
+ { id: 'e1', source: 'starter', target: 'loop1', sourceHandle: 'source', targetHandle: null },
+ { id: 'e2', source: 'loop1', target: 'par1' },
+ ] as any[],
+ loops: {
+ loop1: { id: 'loop1', nodes: ['childInLoop'], iterations: 3, loopType: 'for' as const },
+ } as Record,
+ parallels: {
+ par1: { id: 'par1', nodes: ['childInPar'], count: 2, parallelType: 'count' as const },
+ } as Record,
+ variables: {
+ v1: { id: 'v1', name: 'apiHost', type: 'string' as const, value: 'https://example.com' },
+ },
+ metadata: { name: 'Round Trip', description: 'probe' },
+ }
+}
+
+describe('workflow export -> import round trip', () => {
+ const exported = sanitizeForExport(makeSourceState() as any)
+ const { data: reimported, errors } = parseWorkflowJson(JSON.stringify(exported))
+
+ it('re-imports without validation errors', () => {
+ expect(errors).toEqual([])
+ expect(reimported).not.toBeNull()
+ })
+
+ it('preserves every block, including children inside loop and parallel containers', () => {
+ expect(Object.keys(exported.state.blocks).sort()).toEqual([
+ 'childInLoop',
+ 'childInPar',
+ 'loop1',
+ 'par1',
+ 'starter',
+ ])
+ expect(Object.keys(reimported!.blocks)).toHaveLength(5)
+ })
+
+ it('preserves every edge', () => {
+ expect(reimported!.edges).toHaveLength(2)
+ })
+
+ it('leaves no dangling parent, edge, loop or parallel reference after id regeneration', () => {
+ const ids = new Set(Object.values(reimported!.blocks).map((b: any) => b.id))
+
+ for (const b of Object.values(reimported!.blocks) as any[]) {
+ if (b.data?.parentId) expect(ids.has(b.data.parentId)).toBe(true)
+ }
+ for (const e of reimported!.edges as any[]) {
+ expect(ids.has(e.source)).toBe(true)
+ expect(ids.has(e.target)).toBe(true)
+ }
+ for (const loop of Object.values(reimported!.loops ?? {}) as any[]) {
+ for (const n of loop.nodes ?? []) expect(ids.has(n)).toBe(true)
+ }
+ for (const p of Object.values(reimported!.parallels ?? {}) as any[]) {
+ for (const n of p.nodes ?? []) expect(ids.has(n)).toBe(true)
+ }
+ })
+
+ it('regenerates ids so a payload can be imported alongside its source', () => {
+ expect(Object.keys(reimported!.blocks)).not.toContain('starter')
+ })
+
+ it('preserves workflow variables', () => {
+ expect(reimported!.variables).toMatchObject({
+ v1: { id: 'v1', name: 'apiHost', type: 'string', value: 'https://example.com' },
+ })
+ })
+
+ it('does not hang on a block name containing regex metacharacters', () => {
+ const hostile = makeSourceState()
+ hostile.blocks.starter.name = 'a*a*a*a*a*a*a*a*b'
+ hostile.blocks.starter.subBlocks.input.value = `<${'a'.repeat(120)}`
+
+ const started = performance.now()
+ const { data } = parseWorkflowJson(JSON.stringify(sanitizeForExport(hostile as any)))
+ const elapsed = performance.now() - started
+
+ expect(data).not.toBeNull()
+ expect(elapsed).toBeLessThan(2000)
+ })
+})
diff --git a/apps/sim/lib/workflows/operations/import-export.ts b/apps/sim/lib/workflows/operations/import-export.ts
index 06967ab7969..13b658cca63 100644
--- a/apps/sim/lib/workflows/operations/import-export.ts
+++ b/apps/sim/lib/workflows/operations/import-export.ts
@@ -1,6 +1,5 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
-import { generateId } from '@sim/utils/id'
import { isRecordLike } from '@sim/utils/object'
import { ApiClientError } from '@/lib/api/client/errors'
import { requestJson } from '@/lib/api/client/request'
@@ -18,6 +17,7 @@ import {
sanitizeForExport,
} from '@/lib/workflows/sanitization/json-sanitizer'
import { sanitizeMalformedSubBlocks } from '@/lib/workflows/sanitization/subblocks'
+import { normalizeImportedVariables } from '@/lib/workflows/variables/parse'
import { regenerateWorkflowIds } from '@/stores/workflows/utils'
import type { Variable, WorkflowState } from '@/stores/workflows/workflow/types'
@@ -724,37 +724,15 @@ export async function persistImportedWorkflow({
throw new Error(`Failed to save workflow state for ${newWorkflowId}`)
}
- if (workflowData.variables) {
- const variablesArray = Array.isArray(workflowData.variables)
- ? workflowData.variables
- : Object.values(workflowData.variables)
-
- if (variablesArray.length > 0) {
- type WorkflowVariablesBodyInput = NonNullable<
- Parameters>[1]['body']
- >
- const variablesRecord: WorkflowVariablesBodyInput['variables'] = {}
-
- for (const variable of variablesArray) {
- const id =
- typeof variable.id === 'string' && variable.id.trim() ? variable.id : generateId()
-
- variablesRecord[id] = {
- id,
- name: variable.name,
- type: variable.type,
- value: variable.value,
- }
- }
-
- try {
- await requestJson(workflowVariablesContract, {
- params: { id: newWorkflowId },
- body: { variables: variablesRecord },
- })
- } catch {
- throw new Error(`Failed to save variables for ${newWorkflowId}`)
- }
+ const variablesRecord = normalizeImportedVariables(workflowData.variables)
+ if (Object.keys(variablesRecord).length > 0) {
+ try {
+ await requestJson(workflowVariablesContract, {
+ params: { id: newWorkflowId },
+ body: { variables: variablesRecord },
+ })
+ } catch {
+ throw new Error(`Failed to save variables for ${newWorkflowId}`)
}
}
diff --git a/apps/sim/lib/workflows/persistence/prepare-state.test.ts b/apps/sim/lib/workflows/persistence/prepare-state.test.ts
new file mode 100644
index 00000000000..98405fe82cf
--- /dev/null
+++ b/apps/sim/lib/workflows/persistence/prepare-state.test.ts
@@ -0,0 +1,101 @@
+/**
+ * @vitest-environment node
+ *
+ * Tests the single normalization pipeline shared by `PUT /api/workflows/[id]/state`
+ * and `POST /api/v1/workflows/import`. Both write paths must land identical data
+ * for identical input, so this is where that behavior is pinned.
+ */
+
+import { describe, expect, it } from 'vitest'
+import { prepareWorkflowStateForPersistence } from '@/lib/workflows/persistence/prepare-state'
+import type { BlockState } from '@/stores/workflows/workflow/types'
+
+function block(overrides: Partial & { id: string }): BlockState {
+ return {
+ type: 'function',
+ name: `Block ${overrides.id}`,
+ position: { x: 0, y: 0 },
+ subBlocks: {},
+ outputs: {},
+ enabled: true,
+ ...overrides,
+ } as BlockState
+}
+
+describe('prepareWorkflowStateForPersistence', () => {
+ it('drops edges whose endpoints do not resolve', () => {
+ const { state, warnings } = prepareWorkflowStateForPersistence({
+ blocks: { a: block({ id: 'a' }), b: block({ id: 'b' }) },
+ edges: [
+ { id: 'good', source: 'a', target: 'b' },
+ { id: 'dangling', source: 'a', target: 'ghost' },
+ ] as never,
+ })
+
+ expect(state.edges.map((e) => e.id)).toEqual(['good'])
+ expect(warnings.some((w) => w.includes('dangling'))).toBe(true)
+ })
+
+ it('drops blocks missing type or name', () => {
+ const { state } = prepareWorkflowStateForPersistence({
+ blocks: {
+ ok: block({ id: 'ok' }),
+ noType: block({ id: 'noType', type: '' }),
+ noName: block({ id: 'noName', name: '' }),
+ },
+ edges: [] as never,
+ })
+
+ expect(Object.keys(state.blocks)).toEqual(['ok'])
+ })
+
+ it('backfills the columns the normalized tables require', () => {
+ const { state } = prepareWorkflowStateForPersistence({
+ blocks: {
+ a: {
+ id: 'a',
+ type: 'function',
+ name: 'A',
+ position: { x: 1, y: 2 },
+ } as unknown as BlockState,
+ },
+ edges: [] as never,
+ })
+
+ expect(state.blocks.a).toMatchObject({
+ enabled: true,
+ horizontalHandles: true,
+ height: 0,
+ subBlocks: {},
+ outputs: {},
+ })
+ })
+
+ it('recomputes loop and parallel containers from the blocks', () => {
+ const { state } = prepareWorkflowStateForPersistence({
+ blocks: {
+ loop1: block({ id: 'loop1', type: 'loop', data: { count: 3, loopType: 'for' } as never }),
+ child: block({ id: 'child', data: { parentId: 'loop1', extent: 'parent' } as never }),
+ par1: block({
+ id: 'par1',
+ type: 'parallel',
+ data: { count: 2, parallelType: 'count' } as never,
+ }),
+ childP: block({ id: 'childP', data: { parentId: 'par1', extent: 'parent' } as never }),
+ },
+ edges: [] as never,
+ })
+
+ expect(state.loops.loop1?.nodes).toEqual(['child'])
+ expect(state.parallels.par1?.nodes).toEqual(['childP'])
+ })
+
+ it('does not mutate the caller-supplied blocks', () => {
+ const blocks = { a: block({ id: 'a' }) }
+ const snapshot = structuredClone(blocks)
+
+ prepareWorkflowStateForPersistence({ blocks, edges: [] as never })
+
+ expect(blocks).toEqual(snapshot)
+ })
+})
diff --git a/apps/sim/lib/workflows/persistence/prepare-state.ts b/apps/sim/lib/workflows/persistence/prepare-state.ts
new file mode 100644
index 00000000000..e6d4ec74a86
--- /dev/null
+++ b/apps/sim/lib/workflows/persistence/prepare-state.ts
@@ -0,0 +1,71 @@
+import { sanitizeAgentToolsInBlocks } from '@/lib/workflows/sanitization/validation'
+import { validateEdges } from '@/stores/workflows/workflow/edge-validation'
+import type { BlockState, WorkflowState } from '@/stores/workflows/workflow/types'
+import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils'
+
+export interface PreparedWorkflowState {
+ blocks: Record
+ edges: WorkflowState['edges']
+ loops: ReturnType
+ parallels: ReturnType
+}
+
+export interface PrepareWorkflowStateResult {
+ state: PreparedWorkflowState
+ /** Human-readable notes about what was dropped or rewritten, for logging. */
+ warnings: string[]
+}
+
+/**
+ * Normalizes a workflow graph into the exact shape the normalized tables expect,
+ * shared by every server-side write path so they cannot drift apart.
+ *
+ * Callers today: `PUT /api/workflows/[id]/state` (the editor and the in-app zip
+ * importer, which reaches it over HTTP) and `POST /api/v1/workflows/import`.
+ *
+ * The steps are order-dependent:
+ * 1. Strip secrets from inline agent-tool definitions.
+ * 2. Drop blocks missing `type`/`name` and backfill the columns the tables
+ * require, so a partial block cannot violate a NOT NULL constraint.
+ * 3. Drop edges whose endpoints no longer resolve — `workflow_edges` has
+ * foreign keys onto `workflow_blocks`, so a dangling edge would otherwise
+ * abort the whole transaction with an opaque database error.
+ * 4. Recompute loop and parallel containers from the surviving blocks, which
+ * are the canonical source for both.
+ */
+export function prepareWorkflowStateForPersistence(state: {
+ blocks: Record
+ edges: WorkflowState['edges']
+}): PrepareWorkflowStateResult {
+ const { blocks: sanitizedBlocks, warnings: sanitizationWarnings } = sanitizeAgentToolsInBlocks(
+ state.blocks
+ )
+
+ const blocks: Record = {}
+ for (const [blockId, block] of Object.entries(sanitizedBlocks)) {
+ if (!block.type || !block.name) continue
+ blocks[blockId] = {
+ ...block,
+ enabled: block.enabled !== undefined ? block.enabled : true,
+ horizontalHandles: block.horizontalHandles !== undefined ? block.horizontalHandles : true,
+ height: block.height !== undefined ? block.height : 0,
+ subBlocks: block.subBlocks || {},
+ outputs: block.outputs || {},
+ }
+ }
+
+ const validatedEdges = validateEdges(state.edges, blocks)
+
+ return {
+ state: {
+ blocks,
+ edges: validatedEdges.valid,
+ loops: generateLoopBlocks(blocks),
+ parallels: generateParallelBlocks(blocks),
+ },
+ warnings: [
+ ...sanitizationWarnings,
+ ...validatedEdges.dropped.map(({ edge, reason }) => `Dropped edge "${edge.id}": ${reason}`),
+ ],
+ }
+}
diff --git a/apps/sim/lib/workflows/variables/parse.test.ts b/apps/sim/lib/workflows/variables/parse.test.ts
new file mode 100644
index 00000000000..1484fa8604d
--- /dev/null
+++ b/apps/sim/lib/workflows/variables/parse.test.ts
@@ -0,0 +1,119 @@
+/**
+ * @vitest-environment node
+ *
+ * Pins the contract between the two halves of every workflow export/import
+ * round trip: `parseWorkflowVariables` produces what exports emit, and
+ * `normalizeImportedVariables` consumes what imports receive. A shape the first
+ * emits that the second drops is silent data loss on a restore, which is
+ * exactly the bug the workspace importer shipped with.
+ */
+
+import { describe, expect, it } from 'vitest'
+import { normalizeImportedVariables, parseWorkflowVariables } from '@/lib/workflows/variables/parse'
+
+const VARIABLE = {
+ id: 'var-1',
+ name: 'apiHost',
+ type: 'string' as const,
+ value: 'https://example.com',
+}
+
+describe('parseWorkflowVariables', () => {
+ it('returns undefined for null so callers can omit the field', () => {
+ expect(parseWorkflowVariables(null)).toBeUndefined()
+ })
+
+ it('reads the current record form', () => {
+ expect(parseWorkflowVariables({ 'var-1': VARIABLE })).toEqual({ 'var-1': VARIABLE })
+ })
+
+ it('re-keys the legacy array form by variable id', () => {
+ expect(parseWorkflowVariables([VARIABLE] as never)).toEqual({ 'var-1': VARIABLE })
+ })
+
+ it('parses a JSON string column value', () => {
+ expect(parseWorkflowVariables(JSON.stringify({ 'var-1': VARIABLE }) as never)).toEqual({
+ 'var-1': VARIABLE,
+ })
+ })
+
+ it('skips legacy array rows without a usable id instead of emitting an "undefined" key', () => {
+ const result = parseWorkflowVariables([VARIABLE, { name: 'orphan' }, null] as never)
+
+ expect(Object.keys(result ?? {})).toEqual(['var-1'])
+ })
+})
+
+describe('normalizeImportedVariables', () => {
+ it('accepts the record form', () => {
+ expect(normalizeImportedVariables({ 'var-1': VARIABLE })).toEqual({ 'var-1': VARIABLE })
+ })
+
+ it('accepts the legacy array form', () => {
+ expect(normalizeImportedVariables([VARIABLE])).toEqual({ 'var-1': VARIABLE })
+ })
+
+ it('returns an empty record for null, undefined and non-objects', () => {
+ expect(normalizeImportedVariables(null)).toEqual({})
+ expect(normalizeImportedVariables(undefined)).toEqual({})
+ expect(normalizeImportedVariables('nope')).toEqual({})
+ })
+
+ it('falls back to the map key when an entry carries no id', () => {
+ expect(normalizeImportedVariables({ fromKey: { name: 'x', value: 1 } })).toMatchObject({
+ fromKey: { id: 'fromKey', name: 'x' },
+ })
+ })
+
+ it('coerces an unrecognized type to string rather than persisting it', () => {
+ const result = normalizeImportedVariables({ v: { ...VARIABLE, type: 'secret' } })
+
+ expect(result['var-1'].type).toBe('string')
+ })
+
+ it('keeps a __proto__-keyed variable as an ordinary own property', () => {
+ /**
+ * Built via `JSON.parse` rather than a literal: an object literal's
+ * `__proto__` sets the prototype, while `JSON.parse` creates a real own
+ * key — and `JSON.parse` is how an import payload actually arrives.
+ */
+ const payload = JSON.parse('{"__proto__": {"name": "sneaky", "value": 1}}')
+
+ const result = normalizeImportedVariables(payload)
+
+ expect(Object.keys(result)).toContain('__proto__')
+ expect(Object.getPrototypeOf(result)).toBe(Object.prototype)
+ })
+
+ it('trims a padded id so the key is referenceable', () => {
+ expect(Object.keys(normalizeImportedVariables([{ ...VARIABLE, id: ' var-1 ' }]))).toEqual([
+ 'var-1',
+ ])
+ })
+
+ it('skips non-object entries instead of writing junk variables', () => {
+ expect(normalizeImportedVariables(['nope', null, VARIABLE])).toEqual({ 'var-1': VARIABLE })
+ })
+})
+
+describe('export -> import variable round trip', () => {
+ /**
+ * The regression guard. Workspace export writes whatever
+ * `parseWorkflowVariables` returns — the record form — and the importer used
+ * to guard on `Array.isArray`, so every variable was silently dropped on
+ * restore.
+ */
+ it('survives the record form that exports actually emit', () => {
+ const exported = parseWorkflowVariables({ 'var-1': VARIABLE })
+
+ expect(exported).toBeDefined()
+ expect(Array.isArray(exported)).toBe(false)
+ expect(normalizeImportedVariables(exported)).toEqual({ 'var-1': VARIABLE })
+ })
+
+ it('survives a legacy array-form export', () => {
+ const exported = parseWorkflowVariables([VARIABLE] as never)
+
+ expect(normalizeImportedVariables(exported)).toEqual({ 'var-1': VARIABLE })
+ })
+})
diff --git a/apps/sim/lib/workflows/variables/parse.ts b/apps/sim/lib/workflows/variables/parse.ts
new file mode 100644
index 00000000000..58c5a41cfa2
--- /dev/null
+++ b/apps/sim/lib/workflows/variables/parse.ts
@@ -0,0 +1,114 @@
+import type { workflow } from '@sim/db/schema'
+import { generateId } from '@sim/utils/id'
+import type { Variable } from '@sim/workflow-types/workflow'
+import type { InferSelectModel } from 'drizzle-orm'
+
+type DbWorkflowVariables = InferSelectModel['variables']
+
+const VARIABLE_TYPES = new Set([
+ 'string',
+ 'number',
+ 'boolean',
+ 'object',
+ 'array',
+ 'plain',
+])
+
+/**
+ * Parses the persisted `workflow.variables` JSONB column into the canonical
+ * `Record` shape.
+ *
+ * Tolerates the three forms the column has carried over time: a JSON string, a
+ * legacy `Variable[]` array (re-keyed by variable id), and the current record.
+ * Returns `undefined` for null/unparseable values so callers can omit the field
+ * entirely rather than emitting an empty object.
+ */
+export function parseWorkflowVariables(
+ dbVariables: DbWorkflowVariables
+): Record | undefined {
+ if (!dbVariables) return undefined
+
+ try {
+ const varsObj = typeof dbVariables === 'string' ? JSON.parse(dbVariables) : dbVariables
+
+ if (Array.isArray(varsObj)) {
+ const result: Record = {}
+ for (const v of varsObj) {
+ /**
+ * Legacy array rows without a usable id would otherwise key the record
+ * under the literal string "undefined" and emit `id: undefined`, which
+ * violates the export contract's `id: z.string()`.
+ */
+ if (!v || typeof v !== 'object' || typeof v.id !== 'string' || !v.id) continue
+ result[v.id] = {
+ id: v.id,
+ name: v.name,
+ type: v.type,
+ value: v.value,
+ }
+ }
+ return result
+ }
+
+ if (typeof varsObj === 'object' && varsObj !== null) {
+ const result: Record = {}
+ for (const [key, v] of Object.entries(varsObj)) {
+ const variable = v as Variable
+ result[key] = {
+ id: variable.id,
+ name: variable.name,
+ type: variable.type,
+ value: variable.value,
+ }
+ }
+ return result
+ }
+ } catch {
+ return undefined
+ }
+
+ return undefined
+}
+
+/**
+ * Normalizes variables arriving from an *untrusted* import payload into the
+ * persisted `Record` shape, keyed by variable id.
+ *
+ * Distinct from {@link parseWorkflowVariables}, which reads data this system
+ * already wrote: nothing here is assumed well-formed. Accepts both the record
+ * form and the legacy array form, and is built on a null-prototype object so a
+ * payload keyed `__proto__` is stored as an ordinary own key instead of hitting
+ * the `Object.prototype` setter, which would silently discard the variable. An
+ * unrecognized `type` falls back to `'string'` rather than being written
+ * through to the JSONB column verbatim.
+ */
+export function normalizeImportedVariables(variables: unknown): Record {
+ /**
+ * Assembled on a null-prototype object so a `__proto__` key lands as an
+ * ordinary own property instead of invoking the prototype setter, then
+ * spread back onto a plain object (spread defines, never assigns, so the key
+ * survives) — callers and JSON serialization get an ordinary object.
+ */
+ const record: Record = Object.create(null)
+ if (!variables || typeof variables !== 'object') return { ...record }
+
+ const entries: Array<[string | undefined, unknown]> = Array.isArray(variables)
+ ? variables.map((value) => [undefined, value])
+ : Object.entries(variables)
+
+ for (const [key, value] of entries) {
+ if (!value || typeof value !== 'object') continue
+ const raw = value as Partial
+ const rawId = typeof raw.id === 'string' ? raw.id.trim() : ''
+ const id = rawId || key || generateId()
+
+ record[id] = {
+ id,
+ name: typeof raw.name === 'string' ? raw.name : id,
+ type: raw.type && VARIABLE_TYPES.has(raw.type) ? raw.type : 'string',
+ value: raw.value,
+ }
+ }
+
+ return { ...record }
+}
diff --git a/apps/sim/next.config.ts b/apps/sim/next.config.ts
index e3b5f52fdbd..1ee1b2389e9 100644
--- a/apps/sim/next.config.ts
+++ b/apps/sim/next.config.ts
@@ -124,7 +124,6 @@ const nextConfig: NextConfig = {
serverExternalPackages: [
'@1password/sdk',
'unpdf',
- 'ffmpeg-static',
'fluent-ffmpeg',
'ws',
'isolated-vm',
@@ -143,7 +142,6 @@ const nextConfig: NextConfig = {
],
},
experimental: {
- optimizeCss: !isDev,
turbopackFileSystemCacheForDev: false,
/**
* Turbopack's persistent build cache (beta) — opt-in via env so only the
diff --git a/apps/sim/package.json b/apps/sim/package.json
index 61079933f1b..09b0780b222 100644
--- a/apps/sim/package.json
+++ b/apps/sim/package.json
@@ -37,6 +37,7 @@
"@1password/sdk": "0.3.1",
"@a2a-js/sdk": "1.0.0-alpha.0",
"@anthropic-ai/sdk": "0.114.0",
+ "@aws-sdk/client-appconfig": "3.1032.0",
"@aws-sdk/client-appconfigdata": "3.1032.0",
"@aws-sdk/client-athena": "3.1032.0",
"@aws-sdk/client-bedrock-runtime": "3.1032.0",
@@ -78,14 +79,15 @@
"@modelcontextprotocol/sdk": "1.29.0",
"@monaco-editor/react": "4.7.0",
"@opentelemetry/api": "^1.9.0",
- "@opentelemetry/exporter-logs-otlp-http": "^0.217.0",
- "@opentelemetry/exporter-metrics-otlp-http": "^0.217.0",
- "@opentelemetry/exporter-trace-otlp-http": "^0.217.0",
- "@opentelemetry/resources": "^2.7.0",
- "@opentelemetry/sdk-metrics": "^2.7.0",
- "@opentelemetry/sdk-node": "^0.217.0",
- "@opentelemetry/sdk-trace-base": "^2.7.0",
- "@opentelemetry/sdk-trace-node": "^2.7.0",
+ "@opentelemetry/core": "2.7.1",
+ "@opentelemetry/exporter-logs-otlp-http": "0.217.0",
+ "@opentelemetry/exporter-metrics-otlp-http": "0.217.0",
+ "@opentelemetry/exporter-trace-otlp-http": "0.217.0",
+ "@opentelemetry/resources": "2.7.1",
+ "@opentelemetry/sdk-metrics": "2.7.1",
+ "@opentelemetry/sdk-node": "0.217.0",
+ "@opentelemetry/sdk-trace-base": "2.7.1",
+ "@opentelemetry/sdk-trace-node": "2.7.1",
"@opentelemetry/semantic-conventions": "^1.32.0",
"@radix-ui/react-avatar": "1.1.10",
"@radix-ui/react-checkbox": "^1.1.3",
@@ -130,6 +132,7 @@
"@trigger.dev/sdk": "4.4.3",
"@typescript/typescript6": "^6.0.2",
"ajv": "8.18.0",
+ "archiver": "8.0.0",
"better-auth": "1.6.23",
"binary-extensions": "3.1.0",
"browser-image-compression": "^2.0.2",
@@ -148,7 +151,6 @@
"drizzle-orm": "^0.45.2",
"echarts": "6.1.0",
"es-toolkit": "1.45.1",
- "ffmpeg-static": "5.3.0",
"fluent-ffmpeg": "2.1.3",
"framer-motion": "^12.5.0",
"free-email-domains": "1.2.25",
@@ -159,7 +161,7 @@
"http-proxy-agent": "7.0.2",
"https-proxy-agent": "7.0.6",
"idb-keyval": "6.2.2",
- "image-size": "1.2.1",
+ "image-size": "2.0.2",
"imapflow": "1.2.4",
"input-otp": "^1.4.2",
"ioredis": "^5.6.0",
@@ -168,10 +170,9 @@
"jose": "6.0.11",
"js-tiktoken": "1.0.21",
"js-yaml": "4.3.0",
- "json5": "2.2.3",
"jszip": "3.10.1",
"lru-cache": "11.3.6",
- "lucide-react": "^0.479.0",
+ "lucide-react": "^0.511.0",
"mammoth": "^1.9.0",
"mermaid": "11.15.0",
"micromatch": "4.0.8",
@@ -194,6 +195,7 @@
"posthog-node": "5.28.9",
"pptxgenjs": "4.0.1",
"prismjs": "^1.30.0",
+ "re2js": "2.8.6",
"react": "19.2.4",
"react-dom": "19.2.4",
"react-hook-form": "^7.54.2",
@@ -206,7 +208,6 @@
"remark-gfm": "4.0.1",
"resend": "^4.1.2",
"rss-parser": "3.13.0",
- "safe-regex2": "5.1.0",
"sharp": "0.35.3",
"socket.io-client": "4.8.1",
"ssh2": "^1.17.0",
@@ -231,6 +232,7 @@
"@tailwindcss/typography": "0.5.19",
"@testing-library/jest-dom": "^6.6.3",
"@trigger.dev/build": "4.4.3",
+ "@types/archiver": "8.0.0",
"@types/busboy": "1.5.4",
"@types/fluent-ffmpeg": "2.1.28",
"@types/html-to-text": "9.0.4",
@@ -248,7 +250,6 @@
"@vitejs/plugin-react": "^4.3.4",
"@vitest/coverage-v8": "^4.1.0",
"autoprefixer": "10.4.21",
- "critters": "0.0.25",
"jsdom": "^26.0.0",
"postcss": "^8",
"react-email": "4.3.2",
@@ -256,14 +257,5 @@
"typescript": "^7.0.2",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^4.1.0"
- },
- "overrides": {
- "next": "16.2.11",
- "@next/env": "16.2.11",
- "mermaid": "11.15.0",
- "react-floater": {
- "react": "$react",
- "react-dom": "$react-dom"
- }
}
}
diff --git a/apps/sim/stores/variables/store.ts b/apps/sim/stores/variables/store.ts
index 3b1e0e91b39..ae955b5b5c4 100644
--- a/apps/sim/stores/variables/store.ts
+++ b/apps/sim/stores/variables/store.ts
@@ -1,7 +1,6 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
-import JSON5 from 'json5'
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { normalizeName } from '@/executor/constants'
@@ -32,7 +31,7 @@ function validateVariable(variable: Variable): string | undefined {
return 'Not a valid object format'
}
- const parsed = JSON5.parse(valueToEvaluate)
+ const parsed = JSON.parse(valueToEvaluate)
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
return 'Not a valid object'
@@ -45,7 +44,7 @@ function validateVariable(variable: Variable): string | undefined {
}
case 'array':
try {
- const parsed = JSON5.parse(String(variable.value))
+ const parsed = JSON.parse(String(variable.value))
if (!Array.isArray(parsed)) {
return 'Not a valid array'
}
diff --git a/apps/sim/stores/workflows/utils.ts b/apps/sim/stores/workflows/utils.ts
index feaf871af1b..eaa60d714bd 100644
--- a/apps/sim/stores/workflows/utils.ts
+++ b/apps/sim/stores/workflows/utils.ts
@@ -10,7 +10,7 @@ import { createDefaultInputFormatField } from '@/lib/workflows/input-format'
import { buildDefaultCanonicalModes } from '@/lib/workflows/subblocks/visibility'
import { hasTriggerCapability } from '@/lib/workflows/triggers/trigger-utils'
import { getBlock } from '@/blocks'
-import { normalizeName } from '@/executor/constants'
+import { escapeRegExp, normalizeName } from '@/executor/constants'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { validateEdges } from '@/stores/workflows/workflow/edge-validation'
@@ -218,7 +218,22 @@ function updateValueReferences(value: unknown, nameMap: Map): un
if (typeof value === 'string') {
let updatedValue = value
nameMap.forEach((newName, oldName) => {
- const regex = new RegExp(`<${oldName}\\.`, 'g')
+ /**
+ * A rename to itself is a no-op, so skip the scan entirely. This is the
+ * whole map on the import path (`regenerateWorkflowIds` seeds it with
+ * `name -> name`), which turns an O(names x values) rescan of every
+ * sub-block string into nothing.
+ */
+ if (oldName === newName) return
+
+ /**
+ * `oldName` is a block name, which reaches this function straight from
+ * imported workflow JSON — `normalizeWorkflowBlockName` only lowercases
+ * and strips whitespace/dots, so regex metacharacters survive. Without
+ * escaping, a name like `a*a*a*a*b` compiles to a catastrophically
+ * backtracking pattern that pins the event loop on a sub-kilobyte input.
+ */
+ const regex = new RegExp(`<${escapeRegExp(oldName)}\\.`, 'g')
updatedValue = updatedValue.replace(regex, `<${newName}.`)
})
return updatedValue
diff --git a/bun.lock b/bun.lock
index e86c75c0379..872e1e433f8 100644
--- a/bun.lock
+++ b/bun.lock
@@ -4,9 +4,6 @@
"workspaces": {
"": {
"name": "simstudio",
- "dependencies": {
- "@aws-sdk/client-appconfig": "3.1032.0",
- },
"devDependencies": {
"@biomejs/biome": "2.0.0-beta.5",
"@clack/prompts": "1.7.0",
@@ -22,7 +19,6 @@
"opentype.js": "1.3.4",
"sharp": "0.35.3",
"turbo": "2.9.14",
- "yaml": "^2.8.1",
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "16.2.11",
@@ -40,7 +36,6 @@
"@sim/db": "workspace:*",
"@sim/emcn": "workspace:*",
"@sim/workflow-renderer": "workspace:*",
- "@vercel/og": "^0.6.5",
"ai": "5.0.203",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -53,7 +48,6 @@
"lucide-react": "^0.511.0",
"next": "16.2.11",
"next-themes": "^0.4.6",
- "postgres": "^3.4.5",
"react": "19.2.4",
"react-dom": "19.2.4",
"reactflow": "^11.11.4",
@@ -67,7 +61,7 @@
"@sim/tsconfig": "workspace:*",
"@tailwindcss/postcss": "^4.0.12",
"@types/mdx": "^2.0.13",
- "@types/node": "^22.14.1",
+ "@types/node": "24.2.1",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.0.4",
"@typescript/native-preview": "7.0.0-dev.20260707.2",
@@ -118,6 +112,7 @@
"@1password/sdk": "0.3.1",
"@a2a-js/sdk": "1.0.0-alpha.0",
"@anthropic-ai/sdk": "0.114.0",
+ "@aws-sdk/client-appconfig": "3.1032.0",
"@aws-sdk/client-appconfigdata": "3.1032.0",
"@aws-sdk/client-athena": "3.1032.0",
"@aws-sdk/client-bedrock-runtime": "3.1032.0",
@@ -159,14 +154,15 @@
"@modelcontextprotocol/sdk": "1.29.0",
"@monaco-editor/react": "4.7.0",
"@opentelemetry/api": "^1.9.0",
- "@opentelemetry/exporter-logs-otlp-http": "^0.217.0",
- "@opentelemetry/exporter-metrics-otlp-http": "^0.217.0",
- "@opentelemetry/exporter-trace-otlp-http": "^0.217.0",
- "@opentelemetry/resources": "^2.7.0",
- "@opentelemetry/sdk-metrics": "^2.7.0",
- "@opentelemetry/sdk-node": "^0.217.0",
- "@opentelemetry/sdk-trace-base": "^2.7.0",
- "@opentelemetry/sdk-trace-node": "^2.7.0",
+ "@opentelemetry/core": "2.7.1",
+ "@opentelemetry/exporter-logs-otlp-http": "0.217.0",
+ "@opentelemetry/exporter-metrics-otlp-http": "0.217.0",
+ "@opentelemetry/exporter-trace-otlp-http": "0.217.0",
+ "@opentelemetry/resources": "2.7.1",
+ "@opentelemetry/sdk-metrics": "2.7.1",
+ "@opentelemetry/sdk-node": "0.217.0",
+ "@opentelemetry/sdk-trace-base": "2.7.1",
+ "@opentelemetry/sdk-trace-node": "2.7.1",
"@opentelemetry/semantic-conventions": "^1.32.0",
"@radix-ui/react-avatar": "1.1.10",
"@radix-ui/react-checkbox": "^1.1.3",
@@ -211,6 +207,7 @@
"@trigger.dev/sdk": "4.4.3",
"@typescript/typescript6": "^6.0.2",
"ajv": "8.18.0",
+ "archiver": "8.0.0",
"better-auth": "1.6.23",
"binary-extensions": "3.1.0",
"browser-image-compression": "^2.0.2",
@@ -229,7 +226,6 @@
"drizzle-orm": "^0.45.2",
"echarts": "6.1.0",
"es-toolkit": "1.45.1",
- "ffmpeg-static": "5.3.0",
"fluent-ffmpeg": "2.1.3",
"framer-motion": "^12.5.0",
"free-email-domains": "1.2.25",
@@ -240,7 +236,7 @@
"http-proxy-agent": "7.0.2",
"https-proxy-agent": "7.0.6",
"idb-keyval": "6.2.2",
- "image-size": "1.2.1",
+ "image-size": "2.0.2",
"imapflow": "1.2.4",
"input-otp": "^1.4.2",
"ioredis": "^5.6.0",
@@ -249,10 +245,9 @@
"jose": "6.0.11",
"js-tiktoken": "1.0.21",
"js-yaml": "4.3.0",
- "json5": "2.2.3",
"jszip": "3.10.1",
"lru-cache": "11.3.6",
- "lucide-react": "^0.479.0",
+ "lucide-react": "^0.511.0",
"mammoth": "^1.9.0",
"mermaid": "11.15.0",
"micromatch": "4.0.8",
@@ -275,6 +270,7 @@
"posthog-node": "5.28.9",
"pptxgenjs": "4.0.1",
"prismjs": "^1.30.0",
+ "re2js": "2.8.6",
"react": "19.2.4",
"react-dom": "19.2.4",
"react-hook-form": "^7.54.2",
@@ -287,7 +283,6 @@
"remark-gfm": "4.0.1",
"resend": "^4.1.2",
"rss-parser": "3.13.0",
- "safe-regex2": "5.1.0",
"sharp": "0.35.3",
"socket.io-client": "4.8.1",
"ssh2": "^1.17.0",
@@ -312,6 +307,7 @@
"@tailwindcss/typography": "0.5.19",
"@testing-library/jest-dom": "^6.6.3",
"@trigger.dev/build": "4.4.3",
+ "@types/archiver": "8.0.0",
"@types/busboy": "1.5.4",
"@types/fluent-ffmpeg": "2.1.28",
"@types/html-to-text": "9.0.4",
@@ -329,7 +325,6 @@
"@vitejs/plugin-react": "^4.3.4",
"@vitest/coverage-v8": "^4.1.0",
"autoprefixer": "10.4.21",
- "critters": "0.0.25",
"jsdom": "^26.0.0",
"postcss": "^8",
"react-email": "4.3.2",
@@ -376,16 +371,12 @@
"simstudio": "dist/index.js",
},
"dependencies": {
- "chalk": "^4.1.2",
+ "chalk": "5.6.2",
"commander": "^11.1.0",
- "dotenv": "^16.3.1",
- "inquirer": "^8.2.6",
- "listr2": "^6.6.1",
},
"devDependencies": {
"@sim/tsconfig": "workspace:*",
- "@types/inquirer": "^8.2.6",
- "@types/node": "^20.5.1",
+ "@types/node": "24.2.1",
"typescript": "^7.0.2",
},
},
@@ -401,7 +392,7 @@
},
"devDependencies": {
"@sim/tsconfig": "workspace:*",
- "@types/node": "^22.10.5",
+ "@types/node": "24.2.1",
"drizzle-kit": "^0.31.4",
"typescript": "^7.0.2",
},
@@ -410,18 +401,32 @@
"name": "@sim/emcn",
"version": "0.1.0",
"dependencies": {
+ "@sim/utils": "workspace:*",
"clsx": "^2.1.1",
"tailwind-merge": "^2.6.0",
},
"devDependencies": {
+ "@radix-ui/react-avatar": "1.1.10",
+ "@radix-ui/react-checkbox": "^1.1.3",
+ "@radix-ui/react-collapsible": "^1.1.3",
+ "@radix-ui/react-dialog": "^1.1.5",
+ "@radix-ui/react-dismissable-layer": "1.1.13",
+ "@radix-ui/react-dropdown-menu": "^2.1.17",
+ "@radix-ui/react-label": "^2.1.2",
+ "@radix-ui/react-popover": "^1.1.5",
+ "@radix-ui/react-slider": "^1.2.2",
+ "@radix-ui/react-slot": "1.2.2",
+ "@radix-ui/react-switch": "^1.1.2",
+ "@radix-ui/react-tabs": "^1.1.2",
"@sim/tsconfig": "workspace:*",
+ "@tanstack/react-virtual": "3.13.24",
"@types/prismjs": "^1.26.5",
"@types/react": "^19",
"@types/react-dom": "^19",
"class-variance-authority": "^0.7.1",
"framer-motion": "^12.5.0",
"input-otp": "^1.4.2",
- "lucide-react": "^0.479.0",
+ "lucide-react": "^0.511.0",
"next": "16.2.11",
"prismjs": "^1.30.0",
"react": "19.2.4",
@@ -430,6 +435,19 @@
"vitest": "^4.1.0",
},
"peerDependencies": {
+ "@radix-ui/react-avatar": "^1.1.10",
+ "@radix-ui/react-checkbox": "^1.1.3",
+ "@radix-ui/react-collapsible": "^1.1.3",
+ "@radix-ui/react-dialog": "^1.1.5",
+ "@radix-ui/react-dismissable-layer": "1.1.13",
+ "@radix-ui/react-dropdown-menu": "^2.1.17",
+ "@radix-ui/react-label": "^2.1.2",
+ "@radix-ui/react-popover": "^1.1.5",
+ "@radix-ui/react-slider": "^1.2.2",
+ "@radix-ui/react-slot": "^1.2.2",
+ "@radix-ui/react-switch": "^1.1.2",
+ "@radix-ui/react-tabs": "^1.1.2",
+ "@tanstack/react-virtual": "^3.13.24",
"class-variance-authority": "^0.7.1",
"framer-motion": "^12.5.0",
"input-otp": "^1.4.2",
@@ -521,12 +539,9 @@
"packages/ts-sdk": {
"name": "simstudio-ts-sdk",
"version": "0.1.2",
- "dependencies": {
- "node-fetch": "^3.3.2",
- },
"devDependencies": {
"@sim/tsconfig": "workspace:*",
- "@types/node": "^20.5.1",
+ "@types/node": "24.2.1",
"@vitest/coverage-v8": "^4.1.0",
"typescript": "^7.0.2",
"vitest": "^4.1.0",
@@ -571,7 +586,7 @@
"@sim/tsconfig": "workspace:*",
"@sim/utils": "workspace:*",
"@types/react": "^19",
- "lucide-react": "^0.479.0",
+ "lucide-react": "^0.511.0",
"react": "19.2.4",
"reactflow": "^11.11.4",
"remark-breaks": "^4.0.0",
@@ -975,8 +990,6 @@
"@daytonaio/sdk": ["@daytonaio/sdk@0.197.0", "", { "dependencies": { "@aws-sdk/client-s3": "^3.787.0", "@aws-sdk/lib-storage": "^3.798.0", "@daytona/analytics-api-client": "0.197.0", "@daytona/api-client": "0.197.0", "@daytona/toolbox-api-client": "0.197.0", "@iarna/toml": "^2.2.5", "@opentelemetry/api": "^1.9.0", "@opentelemetry/exporter-trace-otlp-http": "^0.219.0", "@opentelemetry/instrumentation-http": "^0.219.0", "@opentelemetry/otlp-exporter-base": "0.219.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-node": "^0.219.0", "@opentelemetry/sdk-trace-base": "^2.8.0", "@opentelemetry/semantic-conventions": "^1.40.0", "axios": "^1.15.2", "busboy": "^1.0.0", "dotenv": "^17.0.1", "expand-tilde": "^2.0.2", "fast-glob": "^3.3.0", "form-data": "^4.0.4", "isomorphic-ws": "^5.0.0", "pathe": "^2.0.3", "shell-quote": "^1.8.2", "tar": "^7.5.11" } }, "sha512-RFrK/TLZy8S0VyQcXOzOv70VKNUoUyJ45u/HlCJjmXu5vyK3TH0pQJv9yJn8uT49W+4ypMSODcz0YJIRjH16VA=="],
- "@derhuerst/http-basic": ["@derhuerst/http-basic@8.2.4", "", { "dependencies": { "caseless": "^0.12.0", "concat-stream": "^2.0.0", "http-response-object": "^3.0.1", "parse-cache-control": "^1.0.1" } }, "sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw=="],
-
"@dimforge/rapier3d-compat": ["@dimforge/rapier3d-compat@0.12.0", "", {}, "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow=="],
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
@@ -1151,8 +1164,6 @@
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.3", "", { "os": "win32", "cpu": "x64" }, "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA=="],
- "@inquirer/external-editor": ["@inquirer/external-editor@1.0.3", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA=="],
-
"@ioredis/commands": ["@ioredis/commands@1.10.0", "", {}, "sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q=="],
"@isaacs/cliui": ["@isaacs/cliui@9.0.0", "", {}, "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg=="],
@@ -1339,17 +1350,17 @@
"@opentelemetry/propagator-jaeger": ["@opentelemetry/propagator-jaeger@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-KMjVBHzP4N60bOzxja76M1F1hZZ43lGPga5ix+mkv9+kk1nx9SbkxSvJsMbuVUxdPQmsPTqGShmhN8ulrMOg6Q=="],
- "@opentelemetry/resources": ["@opentelemetry/resources@2.8.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg=="],
+ "@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="],
"@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.217.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.217.0", "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-BB+PcHItcZDL63dPMW+mJvwN9rk37wuIDjRxbVlg6pPDvDR/7GL7UJHbGsllgoggOoTimsKgENaWPoGch/oE1A=="],
- "@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.8.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-UDBGaj6W0Rgy5rTTaoxs8gVGF/aGkAKyjurJv7se6wjRxJu7FoquTLT/vt54DZfo4crbprYfhX/SOK9+BPw1qg=="],
+ "@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ=="],
"@opentelemetry/sdk-node": ["@opentelemetry/sdk-node@0.217.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.217.0", "@opentelemetry/configuration": "0.217.0", "@opentelemetry/context-async-hooks": "2.7.1", "@opentelemetry/core": "2.7.1", "@opentelemetry/exporter-logs-otlp-grpc": "0.217.0", "@opentelemetry/exporter-logs-otlp-http": "0.217.0", "@opentelemetry/exporter-logs-otlp-proto": "0.217.0", "@opentelemetry/exporter-metrics-otlp-grpc": "0.217.0", "@opentelemetry/exporter-metrics-otlp-http": "0.217.0", "@opentelemetry/exporter-metrics-otlp-proto": "0.217.0", "@opentelemetry/exporter-prometheus": "0.217.0", "@opentelemetry/exporter-trace-otlp-grpc": "0.217.0", "@opentelemetry/exporter-trace-otlp-http": "0.217.0", "@opentelemetry/exporter-trace-otlp-proto": "0.217.0", "@opentelemetry/exporter-zipkin": "2.7.1", "@opentelemetry/instrumentation": "0.217.0", "@opentelemetry/otlp-exporter-base": "0.217.0", "@opentelemetry/propagator-b3": "2.7.1", "@opentelemetry/propagator-jaeger": "2.7.1", "@opentelemetry/resources": "2.7.1", "@opentelemetry/sdk-logs": "0.217.0", "@opentelemetry/sdk-metrics": "2.7.1", "@opentelemetry/sdk-trace-base": "2.7.1", "@opentelemetry/sdk-trace-node": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-K/60pSv42+NQiZKy1pAH18nYDkxltsDV4O3SJ233J0E9raU1ksyL9gsKuS8p30bYBb4AMPCfDuutHQaHYpcv0Q=="],
- "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.8.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ=="],
+ "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw=="],
- "@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.8.0", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.8.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-nZt9OGufioAc3AfoLTqA9bsAeaMJAictYDdI2VcNQ+PmT+3rfKjAZDZvgPfd8VPX0O5Bw1hdQF6kDK8VSpZiWg=="],
+ "@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.7.1", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.7.1", "@opentelemetry/core": "2.7.1", "@opentelemetry/sdk-trace-base": "2.7.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-pCpQxU68lV+I9s9svqMyVu5iHdDDUnqUpSxqwyCU8A9ejEsSnMPCbearwsUO4yk08ZJzAIUCFuReMdVQvHrdvg=="],
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.41.1", "", {}, "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA=="],
@@ -1871,6 +1882,8 @@
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
+ "@types/archiver": ["@types/archiver@8.0.0", "", { "dependencies": { "@types/node": "*", "@types/readdir-glob": "*" } }, "sha512-YpXPbEuv9+eUIPPQWUPahj3cvs9isWRuF+J4z+KbdYVDO3rWorWQFxUVHnwPu2AgKwvgpki5F2VMX0Xx+mX45A=="],
+
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
@@ -1969,8 +1982,6 @@
"@types/html-to-text": ["@types/html-to-text@9.0.4", "", {}, "sha512-pUY3cKH/Nm2yYrEmDlPR1mR7yszjGx4DrwPjQ702C4/D5CwHuZTgZdIdwPkRbcuhs7BAh2L5rg3CL5cbRiGTCQ=="],
- "@types/inquirer": ["@types/inquirer@8.2.13", "", { "dependencies": { "@types/through": "*", "rxjs": "^7.2.0" } }, "sha512-shSvl3mn4Z8AK627kA1vx8PYkyH6CdIjV5NYYj7a0xIxzmG3ZgzEpzCi3CWfktjAlq+0Z0wHJGtWNiACaYpeOg=="],
-
"@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="],
"@types/jsdom": ["@types/jsdom@21.1.7", "", { "dependencies": { "@types/node": "*", "@types/tough-cookie": "*", "parse5": "^7.0.0" } }, "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA=="],
@@ -2001,6 +2012,8 @@
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
+ "@types/readdir-glob": ["@types/readdir-glob@1.1.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg=="],
+
"@types/request": ["@types/request@2.48.13", "", { "dependencies": { "@types/caseless": "*", "@types/node": "*", "@types/tough-cookie": "*", "form-data": "^2.5.5" } }, "sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg=="],
"@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="],
@@ -2011,8 +2024,6 @@
"@types/three": ["@types/three@0.177.0", "", { "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", "@types/stats.js": "*", "@types/webxr": "*", "@webgpu/types": "*", "fflate": "~0.8.2", "meshoptimizer": "~0.18.1" } }, "sha512-/ZAkn4OLUijKQySNci47lFO+4JLE1TihEjsGWPUT+4jWqxtwOPPEwJV1C3k5MEx0mcBPCdkFjzRzDOnHEI1R+A=="],
- "@types/through": ["@types/through@0.0.33", "", { "dependencies": { "@types/node": "*" } }, "sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ=="],
-
"@types/tough-cookie": ["@types/tough-cookie@4.0.5", "", {}, "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA=="],
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
@@ -2145,11 +2156,11 @@
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
- "ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="],
+ "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="],
- "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+ "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
- "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
+ "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="],
@@ -2157,6 +2168,8 @@
"anynum": ["anynum@1.0.0", "", {}, "sha512-xjR9/zBVnUOP6ztMIIgShjsxui80nQUQH+5xJnvrYLs+90bF25/KJqaAi8mk+B4RDtX1Nspi6fmp4YTEts8SfA=="],
+ "archiver": ["archiver@8.0.0", "", { "dependencies": { "async": "^3.2.4", "buffer-crc32": "^1.0.0", "is-stream": "^4.0.0", "lazystream": "^1.0.0", "normalize-path": "^3.0.0", "readable-stream": "^4.0.0", "readdir-glob": "^3.0.0", "tar-stream": "^3.0.0", "zip-stream": "^7.0.2" } }, "sha512-fV1orZfsnPn9BaSByR/qE67rJCLJEy2Ox5bq7nJh+jquWaNh6Sfec75kJ2T6PtdGUbPQlrVoSVCEOa5SdiTQ1g=="],
+
"arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
@@ -2179,7 +2192,7 @@
"astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="],
- "async": ["async@0.2.10", "", {}, "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ=="],
+ "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
"async-retry": ["async-retry@1.3.3", "", { "dependencies": { "retry": "0.13.1" } }, "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw=="],
@@ -2195,10 +2208,22 @@
"axios": ["axios@1.18.0", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw=="],
+ "b4a": ["b4a@1.8.1", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw=="],
+
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
+ "bare-events": ["bare-events@2.9.1", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg=="],
+
+ "bare-fs": ["bare-fs@4.7.4", "", { "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", "bare-stream": "^2.6.4", "bare-url": "^2.2.2", "fast-fifo": "^1.3.2" }, "peerDependencies": { "bare-buffer": "*" }, "optionalPeers": ["bare-buffer"] }, "sha512-y1kC+ffIx/tPLdTE693uNjHfzTfr+ravR5tvWlMXe25nELbkqV400S71qHDwbkAQ1FVEZobB1NFRzFbCCcyBCQ=="],
+
+ "bare-path": ["bare-path@3.1.1", "", {}, "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ=="],
+
+ "bare-stream": ["bare-stream@2.13.3", "", { "dependencies": { "b4a": "^1.8.1", "streamx": "^2.25.0", "teex": "^1.0.1" }, "peerDependencies": { "bare-abort-controller": "*", "bare-buffer": "*", "bare-events": "*" }, "optionalPeers": ["bare-abort-controller", "bare-buffer", "bare-events"] }, "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ=="],
+
+ "bare-url": ["bare-url@2.4.5", "", { "dependencies": { "bare-path": "^3.0.0" } }, "sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ=="],
+
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"base64id": ["base64id@2.0.0", "", {}, "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="],
@@ -2241,6 +2266,8 @@
"buffer": ["buffer@5.6.0", "", { "dependencies": { "base64-js": "^1.0.2", "ieee754": "^1.1.4" } }, "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw=="],
+ "buffer-crc32": ["buffer-crc32@1.0.0", "", {}, "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w=="],
+
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
@@ -2265,8 +2292,6 @@
"caniuse-lite": ["caniuse-lite@1.0.30001799", "", {}, "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw=="],
- "caseless": ["caseless@0.12.0", "", {}, "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw=="],
-
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
"chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
@@ -2281,8 +2306,6 @@
"character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],
- "chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="],
-
"cheerio": ["cheerio@1.1.2", "", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "encoding-sniffer": "^0.2.1", "htmlparser2": "^10.0.0", "parse5": "^7.3.0", "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", "undici": "^7.12.0", "whatwg-mimetype": "^4.0.0" } }, "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg=="],
"cheerio-select": ["cheerio-select@2.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-select": "^5.1.0", "css-what": "^6.1.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1" } }, "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g=="],
@@ -2301,20 +2324,16 @@
"classcat": ["classcat@5.0.5", "", {}, "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w=="],
- "cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="],
+ "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="],
"cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="],
- "cli-truncate": ["cli-truncate@3.1.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^5.0.0" } }, "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA=="],
-
- "cli-width": ["cli-width@3.0.0", "", {}, "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw=="],
+ "cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="],
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
- "clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="],
-
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"cluster-key-slot": ["cluster-key-slot@1.1.1", "", {}, "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw=="],
@@ -2337,6 +2356,8 @@
"compare-versions": ["compare-versions@6.1.1", "", {}, "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg=="],
+ "compress-commons": ["compress-commons@7.0.1", "", { "dependencies": { "crc-32": "^1.2.0", "crc32-stream": "^7.0.1", "is-stream": "^4.0.0", "normalize-path": "^3.0.0", "readable-stream": "^4.0.0" } }, "sha512-g0S8KAD8qf4+V//pr3BfB1aBnARLXNz2Gx+jmHU0LEriUuoQUOPOulVquHKTJ8+EAIIO7fhseNDr9wK5Q9FKBQ=="],
+
"compute-scroll-into-view": ["compute-scroll-into-view@3.1.1", "", {}, "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw=="],
"concat-stream": ["concat-stream@2.0.0", "", { "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.0.2", "typedarray": "^0.0.6" } }, "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A=="],
@@ -2365,7 +2386,9 @@
"cpu-features": ["cpu-features@0.0.10", "", { "dependencies": { "buildcheck": "~0.0.6", "nan": "^2.19.0" } }, "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA=="],
- "critters": ["critters@0.0.25", "", { "dependencies": { "chalk": "^4.1.0", "css-select": "^5.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.2", "htmlparser2": "^8.0.2", "postcss": "^8.4.23", "postcss-media-query-parser": "^0.2.3" } }, "sha512-ROF/tjJyyRdM8/6W0VqoN5Ql05xAGnkf5b7f3sTEl1bI5jTQQf8O918RD/V9tEb9pRY/TKcvJekDbJtniHyPtQ=="],
+ "crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="],
+
+ "crc32-stream": ["crc32-stream@7.0.1", "", { "dependencies": { "crc-32": "^1.2.0", "readable-stream": "^4.0.0" } }, "sha512-IBWsY8xznyQrcHn8h4bC8/4ErNke5elzgG8GcqF4RFPw6aHkWWRc7Tgw6upjaTX/CT/yQgqYENkxYsTYN+hW2g=="],
"croner": ["croner@9.1.0", "", {}, "sha512-p9nwwR4qyT5W996vBZhdvBCnMhicY5ytZkR4D1Xj0wuTDEiMnjwR57Q3RXYY/s0EpX6Ay3vgIcfaR+ewGHsi+g=="],
@@ -2495,8 +2518,6 @@
"deepmerge-ts": ["deepmerge-ts@7.1.5", "", {}, "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw=="],
- "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="],
-
"defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="],
"delaunator": ["delaunator@5.1.0", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ=="],
@@ -2547,7 +2568,7 @@
"domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
- "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
+ "dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
"drizzle-kit": ["drizzle-kit@0.31.10", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "tsx": "^4.21.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw=="],
@@ -2595,8 +2616,6 @@
"entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="],
- "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
-
"environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
@@ -2651,6 +2670,8 @@
"events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
+ "events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="],
+
"eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
"eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="],
@@ -2685,6 +2706,8 @@
"fast-equals": ["fast-equals@5.4.0", "", {}, "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw=="],
+ "fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="],
+
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
"fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="],
@@ -2713,10 +2736,6 @@
"fflate": ["fflate@0.8.3", "", {}, "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA=="],
- "ffmpeg-static": ["ffmpeg-static@5.3.0", "", { "dependencies": { "@derhuerst/http-basic": "^8.2.0", "env-paths": "^2.2.0", "https-proxy-agent": "^5.0.0", "progress": "^2.0.3" } }, "sha512-H+K6sW6TiIX6VGend0KQwthe+kaceeH/luE8dIZyOP35ik7ahYojDuqlTV1bOrtEwl01sy2HFNGQfi5IDJvotg=="],
-
- "figures": ["figures@3.2.0", "", { "dependencies": { "escape-string-regexp": "^1.0.5" } }, "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg=="],
-
"file-type": ["file-type@16.5.4", "", { "dependencies": { "readable-web-to-node-stream": "^3.0.0", "strtok3": "^6.2.4", "token-types": "^4.1.1" } }, "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
@@ -2875,14 +2894,12 @@
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
- "htmlparser2": ["htmlparser2@8.0.2", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1", "entities": "^4.4.0" } }, "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA=="],
+ "htmlparser2": ["htmlparser2@10.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="],
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
- "http-response-object": ["http-response-object@3.0.2", "", { "dependencies": { "@types/node": "^10.0.3" } }, "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA=="],
-
"https": ["https@1.0.0", "", {}, "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg=="],
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
@@ -2903,7 +2920,7 @@
"ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
- "image-size": ["image-size@1.2.1", "", { "dependencies": { "queue": "6.0.2" }, "bin": { "image-size": "bin/image-size.js" } }, "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw=="],
+ "image-size": ["image-size@2.0.2", "", { "bin": { "image-size": "bin/image-size.js" } }, "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w=="],
"imapflow": ["imapflow@1.2.4", "", { "dependencies": { "@zone-eu/mailsplit": "5.4.8", "encoding-japanese": "2.2.0", "iconv-lite": "0.7.1", "libbase64": "1.3.0", "libmime": "5.3.7", "libqp": "2.1.1", "nodemailer": "7.0.12", "pino": "10.1.0", "socks": "2.8.7" } }, "sha512-X/eRQeje33uZycfopjwoQKKbya+bBIaqpviOFxhPOD24DXU2hMfXwYe9e8j1+ADwFVgTvKq4G2/ljjZK3Y8mvg=="],
@@ -2923,8 +2940,6 @@
"input-otp": ["input-otp@1.4.2", "", { "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA=="],
- "inquirer": ["inquirer@8.2.7", "", { "dependencies": { "@inquirer/external-editor": "^1.0.0", "ansi-escapes": "^4.2.1", "chalk": "^4.1.1", "cli-cursor": "^3.1.0", "cli-width": "^3.0.0", "figures": "^3.0.0", "lodash": "^4.17.21", "mute-stream": "0.0.8", "ora": "^5.4.1", "run-async": "^2.4.0", "rxjs": "^7.5.5", "string-width": "^4.1.0", "strip-ansi": "^6.0.0", "through": "^2.3.6", "wrap-ansi": "^6.0.1" } }, "sha512-UjOaSel/iddGZJ5xP/Eixh6dY1XghiBw4XK13rCCIJcJfyhhoul/7KhLLUGtebEj6GDYM6Vnx/mVsjx2L/mFIA=="],
-
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
"ioredis": ["ioredis@5.11.1", "", { "dependencies": { "@ioredis/commands": "1.10.0", "cluster-key-slot": "1.1.1", "debug": "4.4.3", "denque": "2.1.0", "redis-errors": "1.2.0", "redis-parser": "3.0.0", "standard-as-callback": "2.1.0" } }, "sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A=="],
@@ -2949,7 +2964,7 @@
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
- "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
+ "is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="],
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
@@ -2967,7 +2982,7 @@
"is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="],
- "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
+ "is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="],
"is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
@@ -3045,6 +3060,8 @@
"layout-base": ["layout-base@1.0.2", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="],
+ "lazystream": ["lazystream@1.0.1", "", { "dependencies": { "readable-stream": "^2.0.5" } }, "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw=="],
+
"leac": ["leac@0.6.0", "", {}, "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg=="],
"libbase64": ["libbase64@1.3.0", "", {}, "sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg=="],
@@ -3091,7 +3108,7 @@
"lint-staged": ["lint-staged@16.0.0", "", { "dependencies": { "chalk": "^5.4.1", "commander": "^13.1.0", "debug": "^4.4.0", "lilconfig": "^3.1.3", "listr2": "^8.3.3", "micromatch": "^4.0.8", "nano-spawn": "^1.0.0", "pidtree": "^0.6.0", "string-argv": "^0.3.2", "yaml": "^2.7.1" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-sUCprePs6/rbx4vKC60Hez6X10HPkpDJaGcy3D1NdwR7g1RcNkWL8q9mJMreOqmHBTs+1sNFp+wOiX9fr+hoOQ=="],
- "listr2": ["listr2@6.6.1", "", { "dependencies": { "cli-truncate": "^3.1.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^5.0.1", "rfdc": "^1.3.0", "wrap-ansi": "^8.1.0" }, "peerDependencies": { "enquirer": ">= 2.3.0 < 3" }, "optionalPeers": ["enquirer"] }, "sha512-+rAXGHh0fkEWdXBmX+L6mmfmXmXvDGEKzkjxO+8mP3+nI/r/CWznVBvsibXdxda9Zz0OW2e2ikphN3OwCT/jSg=="],
+ "listr2": ["listr2@8.3.3", "", { "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ=="],
"lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="],
@@ -3115,7 +3132,7 @@
"log-symbols": ["log-symbols@7.0.1", "", { "dependencies": { "is-unicode-supported": "^2.0.0", "yoctocolors": "^2.1.1" } }, "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg=="],
- "log-update": ["log-update@5.0.1", "", { "dependencies": { "ansi-escapes": "^5.0.0", "cli-cursor": "^4.0.0", "slice-ansi": "^5.0.0", "strip-ansi": "^7.0.1", "wrap-ansi": "^8.0.1" } }, "sha512-5UtUDQ/6edw4ofyljDNcOVJQ4c7OjDro4h3y8e1GQL5iYElYclVHJ3zeWchylvMaKnDbDilC8irOVyexnA/Slw=="],
+ "log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="],
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
@@ -3129,7 +3146,7 @@
"lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="],
- "lucide-react": ["lucide-react@0.479.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-aBhNnveRhorBOK7uA4gDjgaf+YlHMdMhQ/3cupk6exM10hWlEU+2QtWYOfhXhjAsmdb6LeKR+NZnow4UxRRiTQ=="],
+ "lucide-react": ["lucide-react@0.511.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-VK5a2ydJ7xm8GvBeKLS9mu1pVK6ucef9780JVUjw6bAjJL/QXnd4Y0p7SPeOUMC27YhzNCZvm5d/QX0Tp3rc0w=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
@@ -3323,8 +3340,6 @@
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
- "mute-stream": ["mute-stream@0.0.8", "", {}, "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA=="],
-
"mysql2": ["mysql2@3.14.3", "", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.6.3", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-fD6MLV8XJ1KiNFIF0bS7Msl8eZyhlTDCDl75ajU5SJtpdx9ZPEACulJcqJWr1Y8OYyxsFc4j3+nflpmhxCU5aQ=="],
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
@@ -3363,7 +3378,7 @@
"node-ensure": ["node-ensure@0.0.0", "", {}, "sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw=="],
- "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
+ "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
@@ -3443,8 +3458,6 @@
"pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
- "parse-cache-control": ["parse-cache-control@1.0.1", "", {}, "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg=="],
-
"parse-css-color": ["parse-css-color@0.2.1", "", { "dependencies": { "color-name": "^1.1.4", "hex-rgb": "^4.1.0" } }, "sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg=="],
"parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
@@ -3527,8 +3540,6 @@
"postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="],
- "postcss-media-query-parser": ["postcss-media-query-parser@0.2.3", "", {}, "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig=="],
-
"postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="],
"postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="],
@@ -3557,8 +3568,6 @@
"process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="],
- "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],
-
"prom-client": ["prom-client@15.1.3", "", { "dependencies": { "@opentelemetry/api": "^1.4.0", "tdigest": "^0.1.1" } }, "sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g=="],
"prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],
@@ -3623,6 +3632,8 @@
"rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="],
+ "re2js": ["re2js@2.8.6", "", {}, "sha512-xLgQil4kIUCrAzVk9fRSkxkFNwmygLFjVxXrLc65aE1F0+Zsb8rxumFBy4XKyvgMCTL6kilDq3EZ0piE2dP/Dg=="],
+
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
"react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
@@ -3649,10 +3660,12 @@
"read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="],
- "readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
+ "readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="],
"readable-web-to-node-stream": ["readable-web-to-node-stream@3.0.4", "", { "dependencies": { "readable-stream": "^4.7.0" } }, "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw=="],
+ "readdir-glob": ["readdir-glob@3.0.0", "", { "dependencies": { "minimatch": "^10.2.2" } }, "sha512-AhNB2KgKeVJr16nK9LLZbJNWnYoT23ZrumNKFDebHBdkC8KHSqWo871JAUhoWC/RtjEVdqNMFpM6qrwRbaUqpw=="],
+
"readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
"real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="],
@@ -3719,9 +3732,7 @@
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
- "restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="],
-
- "ret": ["ret@0.5.0", "", {}, "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw=="],
+ "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
"retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
@@ -3749,8 +3760,6 @@
"rss-parser": ["rss-parser@3.13.0", "", { "dependencies": { "entities": "^2.0.3", "xml2js": "^0.5.0" } }, "sha512-7jWUBV5yGN3rqMMj7CZufl/291QAhvrrGpDNE4k/02ZchL0npisiYYqULF71jCEKoIiHvK/Q2e6IkDwPziT7+w=="],
- "run-async": ["run-async@2.4.1", "", {}, "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ=="],
-
"run-exclusive": ["run-exclusive@2.2.19", "", { "dependencies": { "minimal-polyfills": "^2.2.3" } }, "sha512-K3mdoAi7tjJ/qT7Flj90L7QyPozwUaAG+CVhkdDje4HLKXUYC3N/Jzkau3flHVDLQVhiHBtcimVodMjN9egYbA=="],
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
@@ -3761,8 +3770,6 @@
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
- "safe-regex2": ["safe-regex2@5.1.0", "", { "dependencies": { "ret": "~0.5.0" }, "bin": { "safe-regex2": "bin/safe-regex2.js" } }, "sha512-pNHAuBW7TrcleFHsxBr5QMi/Iyp0ENjUKz7GCcX1UO7cMh+NmVK6HxQckNL1tJp1XAJVjG6B8OKIPqodqj9rtw=="],
-
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
@@ -3895,19 +3902,21 @@
"streamsearch": ["streamsearch@1.1.0", "", {}, "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="],
+ "streamx": ["streamx@2.28.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw=="],
+
"string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="],
- "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
+ "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
"string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"string.prototype.codepointat": ["string.prototype.codepointat@0.2.1", "", {}, "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg=="],
- "string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
+ "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
- "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+ "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
"strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
@@ -3963,12 +3972,16 @@
"tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="],
- "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
+ "tar-stream": ["tar-stream@3.2.0", "", { "dependencies": { "b4a": "^1.6.4", "bare-fs": "^4.5.5", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg=="],
"tdigest": ["tdigest@0.1.2", "", { "dependencies": { "bintrees": "1.0.2" } }, "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA=="],
"teeny-request": ["teeny-request@9.0.0", "", { "dependencies": { "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", "node-fetch": "^2.6.9", "stream-events": "^1.0.5", "uuid": "^9.0.0" } }, "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g=="],
+ "teex": ["teex@1.0.1", "", { "dependencies": { "streamx": "^2.12.5" } }, "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg=="],
+
+ "text-decoder": ["text-decoder@1.2.7", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ=="],
+
"thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="],
"thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
@@ -3979,8 +3992,6 @@
"throttleit": ["throttleit@2.1.0", "", {}, "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw=="],
- "through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="],
-
"tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
@@ -4035,8 +4046,6 @@
"twilio": ["twilio@5.9.0", "", { "dependencies": { "axios": "^1.11.0", "dayjs": "^1.11.9", "https-proxy-agent": "^5.0.0", "jsonwebtoken": "^9.0.2", "qs": "^6.9.4", "scmp": "^2.1.0", "xmlbuilder": "^13.0.2" } }, "sha512-Ij+xT9MZZSjP64lsy+x6vYsCCb5m2Db9KffkMXBrN3zWbG3rbkXxl+MZVVzrvpwEdSbQD0vMuin+TTlQ6kR6Xg=="],
- "type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],
-
"type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="],
"typebox": ["typebox@1.1.38", "", {}, "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA=="],
@@ -4127,8 +4136,6 @@
"warning": ["warning@4.0.3", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w=="],
- "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="],
-
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
"web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="],
@@ -4147,7 +4154,7 @@
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
- "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
+ "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
@@ -4197,6 +4204,8 @@
"yoga-wasm-web": ["yoga-wasm-web@0.3.3", "", {}, "sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA=="],
+ "zip-stream": ["zip-stream@7.0.5", "", { "dependencies": { "compress-commons": "^7.0.0", "normalize-path": "^3.0.0", "readable-stream": "^4.0.0" } }, "sha512-dSvYKdvLsAHCDqPOhIwk/q5CvuWtTB3Dgpoe0uVEFjTzIOAmsQpprX25InCvrvJsirEbu1OHyy67n/kAj1Sw/w=="],
+
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
"zod-error": ["zod-error@1.5.0", "", { "dependencies": { "zod": "^3.20.2" } }, "sha512-zzopKZ/skI9iXpqCEPj+iLCKl9b88E43ehcU+sbRoHuwGd9F1IDVGQ70TyO6kmfiRL1g4IXkjsXK+g1gLYl4WQ=="],
@@ -4253,19 +4262,17 @@
"@browserbasehq/sdk/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
- "@browserbasehq/sdk/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
-
"@browserbasehq/stagehand/@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.39.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-eMyDIPRZbt1CCLErRCi3exlAvNkBtRe+kW5vvJyef93PmNr/clstYgHhtvmkxN82nlKgzyGPCyGxrm0JQ1ZIdg=="],
"@cerebras/cerebras_cloud_sdk/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
- "@cerebras/cerebras_cloud_sdk/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
-
"@daytonaio/sdk/@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.219.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/otlp-exporter-base": "0.219.0", "@opentelemetry/otlp-transformer": "0.219.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-9t6SvBXXBEjOBcIzgozvBbd3jWrv3Gt3ngGhl1fhdZ/zRc7oZDVOFEqbi2zlBpW9BXhgDMKv422J0DL/3iQWfw=="],
+ "@daytonaio/sdk/@opentelemetry/resources": ["@opentelemetry/resources@2.8.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg=="],
+
"@daytonaio/sdk/@opentelemetry/sdk-node": ["@opentelemetry/sdk-node@0.219.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/configuration": "0.219.0", "@opentelemetry/context-async-hooks": "2.8.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/exporter-logs-otlp-grpc": "0.219.0", "@opentelemetry/exporter-logs-otlp-http": "0.219.0", "@opentelemetry/exporter-logs-otlp-proto": "0.219.0", "@opentelemetry/exporter-metrics-otlp-grpc": "0.219.0", "@opentelemetry/exporter-metrics-otlp-http": "0.219.0", "@opentelemetry/exporter-metrics-otlp-proto": "0.219.0", "@opentelemetry/exporter-prometheus": "0.219.0", "@opentelemetry/exporter-trace-otlp-grpc": "0.219.0", "@opentelemetry/exporter-trace-otlp-http": "0.219.0", "@opentelemetry/exporter-trace-otlp-proto": "0.219.0", "@opentelemetry/exporter-zipkin": "2.8.0", "@opentelemetry/instrumentation": "0.219.0", "@opentelemetry/otlp-exporter-base": "0.219.0", "@opentelemetry/otlp-grpc-exporter-base": "0.219.0", "@opentelemetry/propagator-b3": "2.8.0", "@opentelemetry/propagator-jaeger": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-logs": "0.219.0", "@opentelemetry/sdk-metrics": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0", "@opentelemetry/sdk-trace-node": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-NWLpWLEb8gV3+JBHYoIrktbM385wyHpRJoh3J/4Q52d4PR+AlPMNGJT3DzBUrDSUEVbKAXoHR+EDAPxtiNcj8g=="],
- "@daytonaio/sdk/dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
+ "@daytonaio/sdk/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.8.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ=="],
"@earendil-works/pi-ai/@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.91.1", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw=="],
@@ -4309,58 +4316,22 @@
"@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.217.0", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/otlp-transformer": "0.217.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-eYfqnB3UhKu/5frhd1R6+FprKygbhkomuaceMXDyzxbfXB9tKgZOVmjaJ02CkLA6Tdzumxl+e2H+vo2a8jiMPQ=="],
- "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="],
-
- "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw=="],
-
"@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.217.0", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/otlp-transformer": "0.217.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-eYfqnB3UhKu/5frhd1R6+FprKygbhkomuaceMXDyzxbfXB9tKgZOVmjaJ02CkLA6Tdzumxl+e2H+vo2a8jiMPQ=="],
"@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.217.0", "", { "dependencies": { "@grpc/grpc-js": "^1.14.3", "@opentelemetry/core": "2.7.1", "@opentelemetry/otlp-exporter-base": "0.217.0", "@opentelemetry/otlp-transformer": "0.217.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-7RTAdZuOsCDnsyqTCG4+bDzrfnsWdzkRs7z0AVi/V3tEQx0oKeyc+OuRWYxnRsmaJXgxcmB8vb/lfxn58Dj6Ag=="],
- "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="],
-
- "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ=="],
-
"@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.217.0", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/otlp-transformer": "0.217.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-eYfqnB3UhKu/5frhd1R6+FprKygbhkomuaceMXDyzxbfXB9tKgZOVmjaJ02CkLA6Tdzumxl+e2H+vo2a8jiMPQ=="],
- "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="],
-
- "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ=="],
-
"@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.217.0", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/otlp-transformer": "0.217.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-eYfqnB3UhKu/5frhd1R6+FprKygbhkomuaceMXDyzxbfXB9tKgZOVmjaJ02CkLA6Tdzumxl+e2H+vo2a8jiMPQ=="],
- "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="],
-
- "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ=="],
-
- "@opentelemetry/exporter-prometheus/@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="],
-
- "@opentelemetry/exporter-prometheus/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ=="],
-
"@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.217.0", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/otlp-transformer": "0.217.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-eYfqnB3UhKu/5frhd1R6+FprKygbhkomuaceMXDyzxbfXB9tKgZOVmjaJ02CkLA6Tdzumxl+e2H+vo2a8jiMPQ=="],
"@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.217.0", "", { "dependencies": { "@grpc/grpc-js": "^1.14.3", "@opentelemetry/core": "2.7.1", "@opentelemetry/otlp-exporter-base": "0.217.0", "@opentelemetry/otlp-transformer": "0.217.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-7RTAdZuOsCDnsyqTCG4+bDzrfnsWdzkRs7z0AVi/V3tEQx0oKeyc+OuRWYxnRsmaJXgxcmB8vb/lfxn58Dj6Ag=="],
- "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="],
-
- "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw=="],
-
"@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.217.0", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/otlp-transformer": "0.217.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-eYfqnB3UhKu/5frhd1R6+FprKygbhkomuaceMXDyzxbfXB9tKgZOVmjaJ02CkLA6Tdzumxl+e2H+vo2a8jiMPQ=="],
- "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="],
-
- "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw=="],
-
"@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.217.0", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/otlp-transformer": "0.217.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-eYfqnB3UhKu/5frhd1R6+FprKygbhkomuaceMXDyzxbfXB9tKgZOVmjaJ02CkLA6Tdzumxl+e2H+vo2a8jiMPQ=="],
- "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="],
-
- "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw=="],
-
- "@opentelemetry/exporter-zipkin/@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="],
-
- "@opentelemetry/exporter-zipkin/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw=="],
-
"@opentelemetry/instrumentation-http/@opentelemetry/core": ["@opentelemetry/core@2.8.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww=="],
"@opentelemetry/instrumentation-http/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.219.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.219.0", "import-in-the-middle": "^3.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-X5t7I8GyIO9rmGHwoedZLREpQqrF1WW2nxzNNym6HOKpFiE+rvqV3ngC0xcZVO2YwIGf3KKmRdWrYwdwz3H9RQ=="],
@@ -4373,36 +4344,10 @@
"@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.219.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-logs": "0.219.0", "@opentelemetry/sdk-metrics": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-aaYKAyXhw9VchKZVGOopD3Gw/kPsyrX2c6IQ0AW32mTjqmZOh5Y6Gf5OYqTNqVktAeBjmFinhyFaCwW6GYK9YQ=="],
- "@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="],
-
- "@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ=="],
-
- "@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw=="],
-
"@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@8.0.1", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-NWWCCscLjs+cOKF/s/XVNFRW7Yih0fdH+9brffR5NZCy8k42yRdl5KlWKMVXuI1vfCoy4o1z80XR/W/QUb3V3w=="],
- "@opentelemetry/resources/@opentelemetry/core": ["@opentelemetry/core@2.8.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww=="],
-
- "@opentelemetry/sdk-logs/@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="],
-
- "@opentelemetry/sdk-metrics/@opentelemetry/core": ["@opentelemetry/core@2.8.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww=="],
-
"@opentelemetry/sdk-node/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.217.0", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/otlp-transformer": "0.217.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-eYfqnB3UhKu/5frhd1R6+FprKygbhkomuaceMXDyzxbfXB9tKgZOVmjaJ02CkLA6Tdzumxl+e2H+vo2a8jiMPQ=="],
- "@opentelemetry/sdk-node/@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="],
-
- "@opentelemetry/sdk-node/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ=="],
-
- "@opentelemetry/sdk-node/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw=="],
-
- "@opentelemetry/sdk-node/@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.7.1", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.7.1", "@opentelemetry/core": "2.7.1", "@opentelemetry/sdk-trace-base": "2.7.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-pCpQxU68lV+I9s9svqMyVu5iHdDDUnqUpSxqwyCU8A9ejEsSnMPCbearwsUO4yk08ZJzAIUCFuReMdVQvHrdvg=="],
-
- "@opentelemetry/sdk-trace-base/@opentelemetry/core": ["@opentelemetry/core@2.8.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww=="],
-
- "@opentelemetry/sdk-trace-node/@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.8.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-/3FIraneMcng67SUJCxvyInk/oxzwsxyadufk0wwfOBLf5wqtAGX4MoQASwSbndBPeARzBryUM9Azr5kHIdWLw=="],
-
- "@opentelemetry/sdk-trace-node/@opentelemetry/core": ["@opentelemetry/core@2.8.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww=="],
-
"@radix-ui/react-accordion/@radix-ui/react-context": ["@radix-ui/react-context@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg=="],
"@radix-ui/react-accordion/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.6", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g=="],
@@ -4561,8 +4506,6 @@
"@shuding/opentype.js/fflate": ["fflate@0.7.4", "", {}, "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw=="],
- "@sim/db/@types/node": ["@types/node@22.19.21", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-VMeFBSCKQKmm2swI2kW51SFusDqekC6q9trBCvJ/JliDchFSuoYYKN7yVNjPthP1HKZcx3U1gI/wTcEBjEFKTA=="],
-
"@smithy/middleware-compression/fflate": ["fflate@0.8.1", "", {}, "sha512-/exOvEuc+/iaUm105QIiOt4LpBdMTWsXxqR0HDF35vx3fmaKzw7354gTilCh5rkzEt8WYyG//ku3h3nRmd7CHQ=="],
"@socket.io/redis-adapter/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
@@ -4627,20 +4570,22 @@
"@trigger.dev/sdk/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
+ "@types/archiver/@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="],
+
"@types/cors/@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="],
"@types/node-fetch/@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="],
"@types/nodemailer/@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="],
+ "@types/readdir-glob/@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="],
+
"@types/request/@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="],
"@types/request/form-data": ["form-data@2.5.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35", "safe-buffer": "^5.2.1" } }, "sha512-Ogz/E85h9tlfJzpI6TuFpGcHZFhLrb9Gw8wq9v40CxSCPnv7ahKr6Xgtkn0KYCDQJ8DNn5VoMO8EXr9V5PadyA=="],
"@types/ssh2/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
- "@types/through/@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="],
-
"@types/ws/@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="],
"accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
@@ -4669,13 +4614,15 @@
"c12/confbox": ["confbox@0.2.4", "", {}, "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="],
- "c12/pkg-types": ["pkg-types@2.3.1", "", { "dependencies": { "confbox": "^0.2.4", "exsolve": "^1.0.8", "pathe": "^2.0.3" } }, "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg=="],
+ "c12/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
- "cheerio/htmlparser2": ["htmlparser2@10.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="],
+ "c12/pkg-types": ["pkg-types@2.3.1", "", { "dependencies": { "confbox": "^0.2.4", "exsolve": "^1.0.8", "pathe": "^2.0.3" } }, "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg=="],
"chrome-launcher/@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="],
- "cli-truncate/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
+ "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
+
+ "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
@@ -4685,8 +4632,6 @@
"concat-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
- "critters/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
-
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"cytoscape-fcose/cose-base": ["cose-base@2.2.0", "", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="],
@@ -4699,10 +4644,6 @@
"d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="],
- "docs/@types/node": ["@types/node@22.19.21", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-VMeFBSCKQKmm2swI2kW51SFusDqekC6q9trBCvJ/JliDchFSuoYYKN7yVNjPthP1HKZcx3U1gI/wTcEBjEFKTA=="],
-
- "docs/lucide-react": ["lucide-react@0.511.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-VK5a2ydJ7xm8GvBeKLS9mu1pVK6ucef9780JVUjw6bAjJL/QXnd4Y0p7SPeOUMC27YhzNCZvm5d/QX0Tp3rc0w=="],
-
"docs/tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="],
"docx/@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="],
@@ -4739,9 +4680,7 @@
"fetch-cookie/tough-cookie": ["tough-cookie@6.0.1", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw=="],
- "ffmpeg-static/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="],
-
- "figures/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
+ "fluent-ffmpeg/async": ["async@0.2.10", "", {}, "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ=="],
"foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
@@ -4769,7 +4708,7 @@
"fumadocs-ui/tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="],
- "gaxios/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
+ "gaxios/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
"gaxios/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
@@ -4783,49 +4722,35 @@
"groq-sdk/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
- "groq-sdk/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
-
"gtoken/gaxios": ["gaxios@7.1.5", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-5FZy72Rh8LhtjmvDrKkI+lVhrsQrVKVsItxMoDm5mNQE+xR0WVIIs+jzPSJgBvKVsLi24fZhXJIsNI0bihDzFg=="],
- "htmlparser2/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
+ "html-to-text/htmlparser2": ["htmlparser2@8.0.2", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1", "entities": "^4.4.0" } }, "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA=="],
- "http-response-object/@types/node": ["@types/node@10.17.60", "", {}, "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw=="],
+ "htmlparser2/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
"imapflow/nodemailer": ["nodemailer@7.0.12", "", {}, "sha512-H+rnK5bX2Pi/6ms3sN4/jRQvYSMltV6vqup/0SFOrxYYY/qoNvhXPlYq3e+Pm9RFJRwrMGbMIwi81M4dxpomhA=="],
"imapflow/pino": ["pino@10.1.0", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-0zZC2ygfdqvqK8zJIr1e+wT1T/L+LF6qvqvbzEQ6tiMAoTqEVK9a1K3YRu8HEUvGEvNqZyPJTtb2sNIoTkB83w=="],
- "inquirer/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
-
- "inquirer/ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="],
-
"is-binary-path/binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
- "isomorphic-unfetch/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
-
"json-schema-to-typescript/js-yaml": ["js-yaml@4.2.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw=="],
"jsonwebtoken/semver": ["semver@7.8.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA=="],
+ "jszip/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
+
"katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
+ "lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
+
"libmime/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"linebreak/base64-js": ["base64-js@0.0.8", "", {}, "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw=="],
"lint-staged/commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="],
- "lint-staged/listr2": ["listr2@8.3.3", "", { "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ=="],
-
- "listr2/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
-
- "log-update/ansi-escapes": ["ansi-escapes@5.0.0", "", { "dependencies": { "type-fest": "^1.0.2" } }, "sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA=="],
-
- "log-update/cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="],
-
- "log-update/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
-
- "log-update/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
+ "log-update/slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="],
"loose-envify/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
@@ -4847,14 +4772,14 @@
"neo4j-driver-bolt-connection/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
- "neo4j-driver-bolt-connection/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
-
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
"next/sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
"node-abi/semver": ["semver@7.8.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA=="],
+ "node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
+
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
"nuqs/@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
@@ -4865,16 +4790,8 @@
"openai/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
- "openai/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
-
- "ora/cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="],
-
"ora/log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="],
- "ora/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
-
- "ora/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
-
"p-retry/retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="],
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
@@ -4891,12 +4808,16 @@
"posthog-js/@opentelemetry/exporter-logs-otlp-http": ["@opentelemetry/exporter-logs-otlp-http@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.208.0", "@opentelemetry/otlp-transformer": "0.208.0", "@opentelemetry/sdk-logs": "0.208.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg=="],
+ "posthog-js/@opentelemetry/resources": ["@opentelemetry/resources@2.8.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg=="],
+
"posthog-js/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA=="],
"posthog-js/fflate": ["fflate@0.4.8", "", {}, "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA=="],
"pptxgenjs/@types/node": ["@types/node@22.19.21", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-VMeFBSCKQKmm2swI2kW51SFusDqekC6q9trBCvJ/JliDchFSuoYYKN7yVNjPthP1HKZcx3U1gI/wTcEBjEFKTA=="],
+ "pptxgenjs/image-size": ["image-size@1.2.1", "", { "dependencies": { "queue": "6.0.2" }, "bin": { "image-size": "bin/image-size.js" } }, "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw=="],
+
"protobufjs/@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="],
"proxy-addr/ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
@@ -4911,13 +4832,13 @@
"react-promise-suspense/fast-deep-equal": ["fast-deep-equal@2.0.1", "", {}, "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w=="],
- "readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
-
- "readable-web-to-node-stream/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="],
+ "readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
"resend/@react-email/render": ["@react-email/render@1.1.2", "", { "dependencies": { "html-to-text": "^9.0.5", "prettier": "^3.5.3", "react-promise-suspense": "^0.3.4" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-RnRehYN3v9gVlNMehHPHhyp2RQo7+pSkHDtXPvg3s0GbzM9SQMW4Qrf8GRNvtpLC4gsI+Wt0VatNRUFqjvevbw=="],
- "restore-cursor/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
+ "restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
+
+ "restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"rimraf/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
@@ -4925,16 +4846,6 @@
"sim/tailwindcss": ["tailwindcss@3.4.19", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.7", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ=="],
- "simstudio/@types/node": ["@types/node@20.19.43", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA=="],
-
- "simstudio/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
-
- "simstudio-ts-sdk/@types/node": ["@types/node@20.19.43", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA=="],
-
- "slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
-
- "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="],
-
"socket.io-adapter/ws": ["ws@8.20.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w=="],
"socket.io-client/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
@@ -4945,11 +4856,13 @@
"streamdown/tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="],
- "string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
-
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
- "string_decoder/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
+ "string-width-cjs/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
+
+ "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
+ "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
@@ -4957,14 +4870,12 @@
"tar-fs/chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
- "tar-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
+ "tar-fs/tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
"teeny-request/http-proxy-agent": ["http-proxy-agent@5.0.0", "", { "dependencies": { "@tootallnate/once": "2", "agent-base": "6", "debug": "4" } }, "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w=="],
"teeny-request/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="],
- "teeny-request/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
-
"teeny-request/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
"tough-cookie/tldts": ["tldts@6.1.86", "", { "dependencies": { "tldts-core": "^6.1.86" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ=="],
@@ -4983,10 +4894,18 @@
"whatwg-encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
+ "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
+
+ "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
+
+ "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
"xml-crypto/xpath": ["xpath@0.0.33", "", {}, "sha512-NNXnzrkDrAzalLhIUc01jO2mOzXGXh1JwPgkihcLLzw98c0WgYDmmjSh1Kl3wzaxSVWMuA+fe0WTWOBDWCBmNA=="],
"xml2js/xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="],
+ "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
+
"zrender/tslib": ["tslib@2.3.0", "", {}, "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="],
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
@@ -4995,20 +4914,16 @@
"@browserbasehq/sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
- "@browserbasehq/sdk/node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
-
"@browserbasehq/stagehand/@anthropic-ai/sdk/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
- "@browserbasehq/stagehand/@anthropic-ai/sdk/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
-
"@cerebras/cerebras_cloud_sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
- "@cerebras/cerebras_cloud_sdk/node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
-
"@daytonaio/sdk/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/core": ["@opentelemetry/core@2.8.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww=="],
"@daytonaio/sdk/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.219.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-logs": "0.219.0", "@opentelemetry/sdk-metrics": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-aaYKAyXhw9VchKZVGOopD3Gw/kPsyrX2c6IQ0AW32mTjqmZOh5Y6Gf5OYqTNqVktAeBjmFinhyFaCwW6GYK9YQ=="],
+ "@daytonaio/sdk/@opentelemetry/resources/@opentelemetry/core": ["@opentelemetry/core@2.8.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww=="],
+
"@daytonaio/sdk/@opentelemetry/sdk-node/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.219.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg=="],
"@daytonaio/sdk/@opentelemetry/sdk-node/@opentelemetry/configuration": ["@opentelemetry/configuration@0.219.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "yaml": "^2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0" } }, "sha512-wXZUYv4ngu43nA4WEhuXNacm46LW+17LRM8nKyIhBzroRA24PBYjMnakwzR/w777nFUB5xlgsYTTeuXxumZM1Q=="],
@@ -5045,6 +4960,12 @@
"@daytonaio/sdk/@opentelemetry/sdk-node/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.219.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-s6lTKRakaPClvKoWHRChxnXjDMkM/TQ30ff78jN6EBGf7MI7VzANE5PU3f4z9qDUudWjvZjOLHG0rBnBKYvoXA=="],
+ "@daytonaio/sdk/@opentelemetry/sdk-node/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.8.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-UDBGaj6W0Rgy5rTTaoxs8gVGF/aGkAKyjurJv7se6wjRxJu7FoquTLT/vt54DZfo4crbprYfhX/SOK9+BPw1qg=="],
+
+ "@daytonaio/sdk/@opentelemetry/sdk-node/@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.8.0", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.8.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-nZt9OGufioAc3AfoLTqA9bsAeaMJAictYDdI2VcNQ+PmT+3rfKjAZDZvgPfd8VPX0O5Bw1hdQF6kDK8VSpZiWg=="],
+
+ "@daytonaio/sdk/@opentelemetry/sdk-trace-base/@opentelemetry/core": ["@opentelemetry/core@2.8.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww=="],
+
"@earendil-works/pi-ai/@aws-sdk/client-bedrock-runtime/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1048.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.11", "@aws-sdk/nested-clients": "^3.997.9", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA=="],
"@earendil-works/pi-ai/@aws-sdk/client-bedrock-runtime/@smithy/node-http-handler": ["@smithy/node-http-handler@4.8.0", "", { "dependencies": { "@smithy/core": "^3.25.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-Mq7TNt/VhlEWiYRLQGpzUWeUxh899UGpjKh7Ru0WVIDIjnE+cTRAn0NYlFQ6bWfsQnKnpCbWJj86HzmcG0qEdg=="],
@@ -5105,12 +5026,24 @@
"@opentelemetry/otlp-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.219.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg=="],
+ "@opentelemetry/otlp-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.8.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg=="],
+
"@opentelemetry/otlp-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.219.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-s6lTKRakaPClvKoWHRChxnXjDMkM/TQ30ff78jN6EBGf7MI7VzANE5PU3f4z9qDUudWjvZjOLHG0rBnBKYvoXA=="],
+ "@opentelemetry/otlp-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.8.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-UDBGaj6W0Rgy5rTTaoxs8gVGF/aGkAKyjurJv7se6wjRxJu7FoquTLT/vt54DZfo4crbprYfhX/SOK9+BPw1qg=="],
+
+ "@opentelemetry/otlp-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.8.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ=="],
+
"@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.219.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg=="],
+ "@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.8.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg=="],
+
"@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.219.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-s6lTKRakaPClvKoWHRChxnXjDMkM/TQ30ff78jN6EBGf7MI7VzANE5PU3f4z9qDUudWjvZjOLHG0rBnBKYvoXA=="],
+ "@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.8.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-UDBGaj6W0Rgy5rTTaoxs8gVGF/aGkAKyjurJv7se6wjRxJu7FoquTLT/vt54DZfo4crbprYfhX/SOK9+BPw1qg=="],
+
+ "@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.8.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ=="],
+
"@opentelemetry/otlp-transformer/protobufjs/@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="],
"@radix-ui/react-accordion/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="],
@@ -5151,8 +5084,6 @@
"@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="],
- "@sim/db/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
-
"@trigger.dev/core/@opentelemetry/api-logs/@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="],
"@trigger.dev/core/@opentelemetry/core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.41.1", "", {}, "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA=="],
@@ -5187,41 +5118,41 @@
"@trigger.dev/core/socket.io-client/engine.io-client": ["engine.io-client@6.5.4", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", "ws": "~8.17.1", "xmlhttprequest-ssl": "~2.0.0" } }, "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ=="],
+ "@types/archiver/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
+
"@types/cors/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
"@types/node-fetch/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
"@types/nodemailer/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
+ "@types/readdir-glob/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
+
"@types/request/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
"@types/request/form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"@types/ssh2/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
- "@types/through/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
-
"@types/ws/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
"accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"axios/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
- "bl/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
-
"c12/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
- "cheerio/htmlparser2/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
-
"chrome-launcher/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
- "cli-truncate/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
+ "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
- "cli-truncate/string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
+ "cliui/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
- "cmdk/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
+ "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
- "concat-stream/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
+ "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
+
+ "cmdk/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
"cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="],
@@ -5229,18 +5160,12 @@
"d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="],
- "docs/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
-
"docx/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
- "duplexify/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
-
"engine.io/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
"express/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
- "ffmpeg-static/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
-
"form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"fumadocs-mdx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
@@ -5295,43 +5220,27 @@
"fumadocs-mdx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="],
- "gaxios/node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
+ "gcp-metadata/gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
+
+ "google-auth-library/gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
"gray-matter/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
"groq-sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
- "groq-sdk/node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
-
- "inquirer/ora/is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="],
-
- "inquirer/ora/is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="],
+ "gtoken/gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
- "inquirer/ora/log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="],
+ "html-to-text/htmlparser2/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
- "isomorphic-unfetch/node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
+ "jszip/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
- "lint-staged/listr2/cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="],
+ "jszip/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
- "lint-staged/listr2/log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="],
+ "lazystream/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
- "lint-staged/listr2/wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
+ "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
- "listr2/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
-
- "listr2/wrap-ansi/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
-
- "listr2/wrap-ansi/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
-
- "log-update/ansi-escapes/type-fest": ["type-fest@1.4.0", "", {}, "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA=="],
-
- "log-update/cli-cursor/restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="],
-
- "log-update/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
-
- "log-update/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
-
- "log-update/wrap-ansi/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
+ "log-update/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="],
"next/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
@@ -5383,24 +5292,24 @@
"next/sharp/semver": ["semver@7.8.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA=="],
- "nypm/pkg-types/confbox": ["confbox@0.2.4", "", {}, "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="],
+ "node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
- "openai/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
+ "node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
- "openai/node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
+ "nypm/pkg-types/confbox": ["confbox@0.2.4", "", {}, "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="],
- "ora/cli-cursor/restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
+ "openai/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"ora/log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="],
- "ora/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
-
"posthog-js/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="],
"posthog-js/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.208.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.208.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA=="],
"posthog-js/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.208.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ=="],
+ "posthog-js/@opentelemetry/resources/@opentelemetry/core": ["@opentelemetry/core@2.8.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww=="],
+
"posthog-js/@opentelemetry/sdk-logs/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="],
"posthog-js/@opentelemetry/sdk-logs/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="],
@@ -5411,12 +5320,6 @@
"react-email/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
- "readable-web-to-node-stream/readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
-
- "readable-web-to-node-stream/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
-
- "restore-cursor/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
-
"rimraf/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
"rimraf/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
@@ -5427,20 +5330,14 @@
"sim/tailwindcss/postcss-selector-parser": ["postcss-selector-parser@6.1.4", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ=="],
- "simstudio-ts-sdk/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
+ "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
- "simstudio/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
-
- "stream-browserify/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
-
- "tar-stream/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
+ "tar-fs/tar-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
"teeny-request/http-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
"teeny-request/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
- "teeny-request/node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
-
"tough-cookie/tldts/tldts-core": ["tldts-core@6.1.86", "", {}, "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA=="],
"tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
@@ -5497,22 +5394,26 @@
"twilio/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
- "@browserbasehq/sdk/node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
+ "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
- "@browserbasehq/sdk/node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
+ "wrap-ansi-cjs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
- "@browserbasehq/stagehand/@anthropic-ai/sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
+ "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
- "@browserbasehq/stagehand/@anthropic-ai/sdk/node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
+ "yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
- "@cerebras/cerebras_cloud_sdk/node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
+ "yargs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
- "@cerebras/cerebras_cloud_sdk/node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
+ "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
+ "@browserbasehq/stagehand/@anthropic-ai/sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"@daytonaio/sdk/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.219.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg=="],
"@daytonaio/sdk/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.219.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-s6lTKRakaPClvKoWHRChxnXjDMkM/TQ30ff78jN6EBGf7MI7VzANE5PU3f4z9qDUudWjvZjOLHG0rBnBKYvoXA=="],
+ "@daytonaio/sdk/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.8.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-UDBGaj6W0Rgy5rTTaoxs8gVGF/aGkAKyjurJv7se6wjRxJu7FoquTLT/vt54DZfo4crbprYfhX/SOK9+BPw1qg=="],
+
"@daytonaio/sdk/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.219.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-logs": "0.219.0", "@opentelemetry/sdk-metrics": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-aaYKAyXhw9VchKZVGOopD3Gw/kPsyrX2c6IQ0AW32mTjqmZOh5Y6Gf5OYqTNqVktAeBjmFinhyFaCwW6GYK9YQ=="],
"@daytonaio/sdk/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.219.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-logs": "0.219.0", "@opentelemetry/sdk-metrics": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-aaYKAyXhw9VchKZVGOopD3Gw/kPsyrX2c6IQ0AW32mTjqmZOh5Y6Gf5OYqTNqVktAeBjmFinhyFaCwW6GYK9YQ=="],
@@ -5547,52 +5448,6 @@
"@types/request/form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
- "cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
-
- "gaxios/node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
-
- "gaxios/node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
-
- "groq-sdk/node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
-
- "groq-sdk/node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
-
- "isomorphic-unfetch/node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
-
- "isomorphic-unfetch/node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
-
- "lint-staged/listr2/cli-truncate/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
-
- "lint-staged/listr2/log-update/ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="],
-
- "lint-staged/listr2/log-update/cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="],
-
- "lint-staged/listr2/log-update/slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="],
-
- "lint-staged/listr2/log-update/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
-
- "lint-staged/listr2/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
-
- "lint-staged/listr2/wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
-
- "lint-staged/listr2/wrap-ansi/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
-
- "listr2/wrap-ansi/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
-
- "listr2/wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
-
- "log-update/cli-cursor/restore-cursor/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
-
- "log-update/wrap-ansi/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
-
- "openai/node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
-
- "openai/node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
-
- "ora/cli-cursor/restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
-
- "ora/cli-cursor/restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
-
"posthog-js/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="],
"posthog-js/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="],
@@ -5607,48 +5462,16 @@
"sim/tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
- "teeny-request/node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
-
- "teeny-request/node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
-
- "@browserbasehq/stagehand/@anthropic-ai/sdk/node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
-
- "@browserbasehq/stagehand/@anthropic-ai/sdk/node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
+ "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"@trigger.dev/core/socket.io/engine.io/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
- "lint-staged/listr2/cli-truncate/string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
-
- "lint-staged/listr2/log-update/cli-cursor/restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
-
- "lint-staged/listr2/log-update/slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
-
- "lint-staged/listr2/log-update/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="],
-
- "lint-staged/listr2/log-update/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
-
- "lint-staged/listr2/wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
-
- "log-update/cli-cursor/restore-cursor/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
-
"rimraf/glob/jackspeak/@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
- "rimraf/glob/jackspeak/@isaacs/cliui/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
-
"rimraf/glob/jackspeak/@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
"sim/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
- "lint-staged/listr2/cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
-
- "lint-staged/listr2/log-update/cli-cursor/restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
-
- "lint-staged/listr2/log-update/cli-cursor/restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
-
"rimraf/glob/jackspeak/@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
-
- "rimraf/glob/jackspeak/@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
-
- "rimraf/glob/jackspeak/@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
}
}
diff --git a/package.json b/package.json
index 5c02c20ab52..508dba29b7b 100644
--- a/package.json
+++ b/package.json
@@ -101,8 +101,7 @@
"lint-staged": "16.0.0",
"opentype.js": "1.3.4",
"sharp": "0.35.3",
- "turbo": "2.9.14",
- "yaml": "^2.8.1"
+ "turbo": "2.9.14"
},
"lint-staged": {
"*.{js,jsx,ts,tsx,json,css,scss}": [
@@ -112,8 +111,5 @@
"trustedDependencies": [
"isolated-vm",
"sharp"
- ],
- "dependencies": {
- "@aws-sdk/client-appconfig": "3.1032.0"
- }
+ ]
}
diff --git a/packages/cli/package.json b/packages/cli/package.json
index 9b0f6bb9d6c..ab1760fa265 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -36,19 +36,15 @@
"author": "Sim",
"license": "Apache-2.0",
"engines": {
- "node": ">=16"
+ "node": ">=18"
},
"dependencies": {
- "chalk": "^4.1.2",
- "commander": "^11.1.0",
- "dotenv": "^16.3.1",
- "inquirer": "^8.2.6",
- "listr2": "^6.6.1"
+ "chalk": "5.6.2",
+ "commander": "^11.1.0"
},
"devDependencies": {
"@sim/tsconfig": "workspace:*",
- "@types/inquirer": "^8.2.6",
- "@types/node": "^20.5.1",
+ "@types/node": "24.2.1",
"typescript": "^7.0.2"
}
}
diff --git a/packages/db/package.json b/packages/db/package.json
index 62e398b5b19..842211bd27a 100644
--- a/packages/db/package.json
+++ b/packages/db/package.json
@@ -40,7 +40,7 @@
"devDependencies": {
"@sim/tsconfig": "workspace:*",
"drizzle-kit": "^0.31.4",
- "@types/node": "^22.10.5",
+ "@types/node": "24.2.1",
"typescript": "^7.0.2"
}
}
diff --git a/packages/emcn/package.json b/packages/emcn/package.json
index c0a48f0da71..198d42b48a8 100644
--- a/packages/emcn/package.json
+++ b/packages/emcn/package.json
@@ -35,10 +35,24 @@
"format:check": "biome format ."
},
"dependencies": {
+ "@sim/utils": "workspace:*",
"clsx": "^2.1.1",
"tailwind-merge": "^2.6.0"
},
"peerDependencies": {
+ "@radix-ui/react-avatar": "^1.1.10",
+ "@radix-ui/react-checkbox": "^1.1.3",
+ "@radix-ui/react-collapsible": "^1.1.3",
+ "@radix-ui/react-dialog": "^1.1.5",
+ "@radix-ui/react-dismissable-layer": "1.1.13",
+ "@radix-ui/react-dropdown-menu": "^2.1.17",
+ "@radix-ui/react-label": "^2.1.2",
+ "@radix-ui/react-popover": "^1.1.5",
+ "@radix-ui/react-slider": "^1.2.2",
+ "@radix-ui/react-slot": "^1.2.2",
+ "@radix-ui/react-switch": "^1.1.2",
+ "@radix-ui/react-tabs": "^1.1.2",
+ "@tanstack/react-virtual": "^3.13.24",
"class-variance-authority": "^0.7.1",
"framer-motion": "^12.5.0",
"input-otp": "^1.4.2",
@@ -49,14 +63,27 @@
"react-dom": "^19"
},
"devDependencies": {
+ "@radix-ui/react-avatar": "1.1.10",
+ "@radix-ui/react-checkbox": "^1.1.3",
+ "@radix-ui/react-collapsible": "^1.1.3",
+ "@radix-ui/react-dialog": "^1.1.5",
+ "@radix-ui/react-dismissable-layer": "1.1.13",
+ "@radix-ui/react-dropdown-menu": "^2.1.17",
+ "@radix-ui/react-label": "^2.1.2",
+ "@radix-ui/react-popover": "^1.1.5",
+ "@radix-ui/react-slider": "^1.2.2",
+ "@radix-ui/react-slot": "1.2.2",
+ "@radix-ui/react-switch": "^1.1.2",
+ "@radix-ui/react-tabs": "^1.1.2",
"@sim/tsconfig": "workspace:*",
+ "@tanstack/react-virtual": "3.13.24",
"@types/prismjs": "^1.26.5",
"@types/react": "^19",
"@types/react-dom": "^19",
"class-variance-authority": "^0.7.1",
"framer-motion": "^12.5.0",
"input-otp": "^1.4.2",
- "lucide-react": "^0.479.0",
+ "lucide-react": "^0.511.0",
"next": "16.2.11",
"prismjs": "^1.30.0",
"react": "19.2.4",
diff --git a/packages/emcn/src/components/toast/toast.tsx b/packages/emcn/src/components/toast/toast.tsx
index 3dee9af1541..f5778e2e687 100644
--- a/packages/emcn/src/components/toast/toast.tsx
+++ b/packages/emcn/src/components/toast/toast.tsx
@@ -396,6 +396,29 @@ export function ToastProvider({ children }: { children?: ReactNode }) {
const [mounted, setMounted] = useState(false)
const timersRef = useRef(new Map>())
+ /**
+ * Clear the previous route's toasts when the route changes. Toasts flagged
+ * `persistAcrossRoutes` — global, ongoing-state notifications like the
+ * connection status — survive; page-scoped ones do not.
+ *
+ * Done during render rather than in an effect. Effects run child-first, so an
+ * effect here would also sweep toasts the newly rendered route just raised: a
+ * toast added from a child's mount effect — which is what happens whenever
+ * that child's data is already cached — would be appended and filtered out in
+ * the same commit, before it ever painted. Adjusting state during render runs
+ * before children render, so only toasts predating the navigation are cleared.
+ *
+ * Stays a pure state update: orphaned timers and measured heights are already
+ * reconciled by the effects below, which key off `toasts`.
+ */
+ const [sweptPathname, setSweptPathname] = useState(pathname)
+ if (pathname !== sweptPathname) {
+ setSweptPathname(pathname)
+ setToasts((prev) =>
+ prev.some((t) => !t.persistAcrossRoutes) ? prev.filter((t) => t.persistAcrossRoutes) : prev
+ )
+ }
+
useEffect(() => {
setMounted(true)
}, [])
@@ -459,27 +482,6 @@ export function ToastProvider({ children }: { children?: ReactNode }) {
setHeights({})
}, [])
- /**
- * Clear only route-scoped toasts. Toasts flagged `persistAcrossRoutes` —
- * global, ongoing-state notifications like the connection status — survive,
- * everything else (page-scoped notifications) is cleared on navigation.
- */
- const dismissRouteScopedToasts = useCallback(() => {
- setToasts((prev) => {
- const kept = prev.filter((t) => t.persistAcrossRoutes)
- if (kept.length === prev.length) return prev
- for (const t of prev) {
- if (t.persistAcrossRoutes) continue
- const timer = timersRef.current.get(t.id)
- if (timer) {
- clearTimeout(timer)
- timersRef.current.delete(t.id)
- }
- }
- return kept
- })
- }, [])
-
const measureToast = useCallback((id: string, height: number) => {
setHeights((prev) => (prev[id] === height ? prev : { ...prev, [id]: height }))
}, [])
@@ -536,11 +538,6 @@ export function ToastProvider({ children }: { children?: ReactNode }) {
}
}, [])
- /** On navigation, clear route-scoped toasts so they don't trail the user; `persistAcrossRoutes` toasts survive. */
- useEffect(() => {
- dismissRouteScopedToasts()
- }, [pathname, dismissRouteScopedToasts])
-
/** Held in a ref (seeded once from the stable `addToast`) so the module-level `toast` binds to the live provider. */
const toastFn = useRef(createToastFn(addToast))
diff --git a/packages/ts-sdk/package.json b/packages/ts-sdk/package.json
index b85e704e9ea..1974aeba580 100644
--- a/packages/ts-sdk/package.json
+++ b/packages/ts-sdk/package.json
@@ -35,18 +35,15 @@
],
"author": "Sim",
"license": "Apache-2.0",
- "dependencies": {
- "node-fetch": "^3.3.2"
- },
"devDependencies": {
"@sim/tsconfig": "workspace:*",
- "@types/node": "^20.5.1",
+ "@types/node": "24.2.1",
"@vitest/coverage-v8": "^4.1.0",
"typescript": "^7.0.2",
"vitest": "^4.1.0"
},
"engines": {
- "node": ">=16"
+ "node": ">=18"
},
"publishConfig": {
"access": "public"
diff --git a/packages/ts-sdk/src/index.test.ts b/packages/ts-sdk/src/index.test.ts
index e84457aceb1..c5066442f99 100644
--- a/packages/ts-sdk/src/index.test.ts
+++ b/packages/ts-sdk/src/index.test.ts
@@ -1,9 +1,8 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { SimStudioClient, SimStudioError } from './index'
-vi.mock('node-fetch', () => ({
- default: vi.fn(),
-}))
+const mockFetch = vi.fn()
+vi.stubGlobal('fetch', mockFetch)
describe('SimStudioClient', () => {
let client: SimStudioClient
@@ -58,15 +57,13 @@ describe('SimStudioClient', () => {
describe('validateWorkflow', () => {
it('should return false when workflow status request fails', async () => {
- const fetch = await import('node-fetch')
- vi.mocked(fetch.default).mockRejectedValue(new Error('Network error'))
+ vi.mocked(mockFetch).mockRejectedValue(new Error('Network error'))
const result = await client.validateWorkflow('test-workflow-id')
expect(result).toBe(false)
})
it('should return true when workflow is deployed', async () => {
- const fetch = await import('node-fetch')
const mockResponse = {
ok: true,
json: vi.fn().mockResolvedValue({
@@ -75,14 +72,13 @@ describe('SimStudioClient', () => {
needsRedeployment: false,
}),
}
- vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
+ vi.mocked(mockFetch).mockResolvedValue(mockResponse as any)
const result = await client.validateWorkflow('test-workflow-id')
expect(result).toBe(true)
})
it('should return false when workflow is not deployed', async () => {
- const fetch = await import('node-fetch')
const mockResponse = {
ok: true,
json: vi.fn().mockResolvedValue({
@@ -91,7 +87,7 @@ describe('SimStudioClient', () => {
needsRedeployment: true,
}),
}
- vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
+ vi.mocked(mockFetch).mockResolvedValue(mockResponse as any)
const result = await client.validateWorkflow('test-workflow-id')
expect(result).toBe(false)
@@ -100,7 +96,6 @@ describe('SimStudioClient', () => {
describe('executeWorkflow - async execution', () => {
it('should return AsyncExecutionResult when async is true', async () => {
- const fetch = await import('node-fetch')
const mockResponse = {
ok: true,
status: 202,
@@ -115,7 +110,7 @@ describe('SimStudioClient', () => {
get: vi.fn().mockReturnValue(null),
},
}
- vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
+ vi.mocked(mockFetch).mockResolvedValue(mockResponse as any)
const result = await client.executeWorkflow(
'workflow-id',
@@ -128,14 +123,13 @@ describe('SimStudioClient', () => {
expect(result).toHaveProperty('async', true)
// Verify headers were set correctly
- const calls = vi.mocked(fetch.default).mock.calls
+ const calls = vi.mocked(mockFetch).mock.calls
expect(calls[0][1]?.headers).toMatchObject({
'X-Execution-Mode': 'async',
})
})
it('should return WorkflowExecutionResult when async is false', async () => {
- const fetch = await import('node-fetch')
const mockResponse = {
ok: true,
status: 200,
@@ -148,7 +142,7 @@ describe('SimStudioClient', () => {
get: vi.fn().mockReturnValue(null),
},
}
- vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
+ vi.mocked(mockFetch).mockResolvedValue(mockResponse as any)
const result = await client.executeWorkflow(
'workflow-id',
@@ -162,7 +156,6 @@ describe('SimStudioClient', () => {
})
it('should not set X-Execution-Mode header when async is undefined', async () => {
- const fetch = await import('node-fetch')
const mockResponse = {
ok: true,
status: 200,
@@ -174,18 +167,17 @@ describe('SimStudioClient', () => {
get: vi.fn().mockReturnValue(null),
},
}
- vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
+ vi.mocked(mockFetch).mockResolvedValue(mockResponse as any)
await client.executeWorkflow('workflow-id', { message: 'Hello' })
- const calls = vi.mocked(fetch.default).mock.calls
+ const calls = vi.mocked(mockFetch).mock.calls
expect(calls[0][1]?.headers).not.toHaveProperty('X-Execution-Mode')
})
})
describe('getJobStatus', () => {
it('should fetch job status with correct endpoint', async () => {
- const fetch = await import('node-fetch')
const mockResponse = {
ok: true,
json: vi.fn().mockResolvedValue({
@@ -203,7 +195,7 @@ describe('SimStudioClient', () => {
get: vi.fn().mockReturnValue(null),
},
}
- vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
+ vi.mocked(mockFetch).mockResolvedValue(mockResponse as any)
const result = await client.getJobStatus('task-123')
@@ -212,12 +204,11 @@ describe('SimStudioClient', () => {
expect(result).toHaveProperty('output')
// Verify correct endpoint was called
- const calls = vi.mocked(fetch.default).mock.calls
+ const calls = vi.mocked(mockFetch).mock.calls
expect(calls[0][0]).toBe('https://test.sim.ai/api/jobs/task-123')
})
it('should handle job not found error', async () => {
- const fetch = await import('node-fetch')
const mockResponse = {
ok: false,
status: 404,
@@ -230,7 +221,7 @@ describe('SimStudioClient', () => {
get: vi.fn().mockReturnValue(null),
},
}
- vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
+ vi.mocked(mockFetch).mockResolvedValue(mockResponse as any)
await expect(client.getJobStatus('invalid-task')).rejects.toThrow(SimStudioError)
await expect(client.getJobStatus('invalid-task')).rejects.toThrow('Job not found')
@@ -239,7 +230,6 @@ describe('SimStudioClient', () => {
describe('executeWithRetry', () => {
it('should succeed on first attempt when no rate limit', async () => {
- const fetch = await import('node-fetch')
const mockResponse = {
ok: true,
status: 200,
@@ -251,17 +241,15 @@ describe('SimStudioClient', () => {
get: vi.fn().mockReturnValue(null),
},
}
- vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
+ vi.mocked(mockFetch).mockResolvedValue(mockResponse as any)
const result = await client.executeWithRetry('workflow-id', { message: 'test' })
expect(result).toHaveProperty('success', true)
- expect(vi.mocked(fetch.default)).toHaveBeenCalledTimes(1)
+ expect(vi.mocked(mockFetch)).toHaveBeenCalledTimes(1)
})
it('should retry on rate limit error', async () => {
- const fetch = await import('node-fetch')
-
// First call returns 429, second call succeeds
const rateLimitResponse = {
ok: false,
@@ -294,7 +282,7 @@ describe('SimStudioClient', () => {
},
}
- vi.mocked(fetch.default)
+ vi.mocked(mockFetch)
.mockResolvedValueOnce(rateLimitResponse as any)
.mockResolvedValueOnce(successResponse as any)
@@ -306,11 +294,10 @@ describe('SimStudioClient', () => {
)
expect(result).toHaveProperty('success', true)
- expect(vi.mocked(fetch.default)).toHaveBeenCalledTimes(2)
+ expect(vi.mocked(mockFetch)).toHaveBeenCalledTimes(2)
})
it('should throw after max retries exceeded', async () => {
- const fetch = await import('node-fetch')
const mockResponse = {
ok: false,
status: 429,
@@ -327,7 +314,7 @@ describe('SimStudioClient', () => {
},
}
- vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
+ vi.mocked(mockFetch).mockResolvedValue(mockResponse as any)
await expect(
client.executeWithRetry(
@@ -338,11 +325,10 @@ describe('SimStudioClient', () => {
)
).rejects.toThrow('Rate limit exceeded')
- expect(vi.mocked(fetch.default)).toHaveBeenCalledTimes(3) // Initial + 2 retries
+ expect(vi.mocked(mockFetch)).toHaveBeenCalledTimes(3) // Initial + 2 retries
})
it('should not retry on non-rate-limit errors', async () => {
- const fetch = await import('node-fetch')
const mockResponse = {
ok: false,
status: 500,
@@ -356,13 +342,13 @@ describe('SimStudioClient', () => {
},
}
- vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
+ vi.mocked(mockFetch).mockResolvedValue(mockResponse as any)
await expect(client.executeWithRetry('workflow-id', { message: 'test' })).rejects.toThrow(
'Server error'
)
- expect(vi.mocked(fetch.default)).toHaveBeenCalledTimes(1) // No retries
+ expect(vi.mocked(mockFetch)).toHaveBeenCalledTimes(1) // No retries
})
})
@@ -373,7 +359,6 @@ describe('SimStudioClient', () => {
})
it('should return rate limit info after API call', async () => {
- const fetch = await import('node-fetch')
const mockResponse = {
ok: true,
status: 200,
@@ -388,7 +373,7 @@ describe('SimStudioClient', () => {
},
}
- vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
+ vi.mocked(mockFetch).mockResolvedValue(mockResponse as any)
await client.executeWorkflow('workflow-id', {})
@@ -402,7 +387,6 @@ describe('SimStudioClient', () => {
describe('getUsageLimits', () => {
it('should fetch usage limits with correct structure', async () => {
- const fetch = await import('node-fetch')
const mockResponse = {
ok: true,
json: vi.fn().mockResolvedValue({
@@ -440,7 +424,7 @@ describe('SimStudioClient', () => {
},
}
- vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
+ vi.mocked(mockFetch).mockResolvedValue(mockResponse as any)
const result = await client.getUsageLimits()
@@ -454,12 +438,11 @@ describe('SimStudioClient', () => {
expect(result.storage.percentUsed).toBe(10)
// Verify correct endpoint was called
- const calls = vi.mocked(fetch.default).mock.calls
+ const calls = vi.mocked(mockFetch).mock.calls
expect(calls[0][0]).toBe('https://test.sim.ai/api/users/me/usage-limits')
})
it('should handle unauthorized error', async () => {
- const fetch = await import('node-fetch')
const mockResponse = {
ok: false,
status: 401,
@@ -473,7 +456,7 @@ describe('SimStudioClient', () => {
},
}
- vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
+ vi.mocked(mockFetch).mockResolvedValue(mockResponse as any)
await expect(client.getUsageLimits()).rejects.toThrow(SimStudioError)
await expect(client.getUsageLimits()).rejects.toThrow('Invalid API key')
@@ -482,7 +465,6 @@ describe('SimStudioClient', () => {
describe('executeWorkflow - streaming with selectedOutputs', () => {
it('should include stream and selectedOutputs in request body', async () => {
- const fetch = await import('node-fetch')
const mockResponse = {
ok: true,
status: 200,
@@ -495,7 +477,7 @@ describe('SimStudioClient', () => {
},
}
- vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
+ vi.mocked(mockFetch).mockResolvedValue(mockResponse as any)
await client.executeWorkflow(
'workflow-id',
@@ -503,7 +485,7 @@ describe('SimStudioClient', () => {
{ stream: true, selectedOutputs: ['agent1.content', 'agent2.content'] }
)
- const calls = vi.mocked(fetch.default).mock.calls
+ const calls = vi.mocked(mockFetch).mock.calls
const requestBody = JSON.parse(calls[0][1]?.body as string)
expect(requestBody).toHaveProperty('message', 'test')
@@ -515,7 +497,6 @@ describe('SimStudioClient', () => {
describe('executeWorkflow - primitive and array inputs', () => {
it('should wrap primitive string input in input field', async () => {
- const fetch = await import('node-fetch')
const mockResponse = {
ok: true,
status: 200,
@@ -528,11 +509,11 @@ describe('SimStudioClient', () => {
},
}
- vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
+ vi.mocked(mockFetch).mockResolvedValue(mockResponse as any)
await client.executeWorkflow('workflow-id', 'NVDA')
- const calls = vi.mocked(fetch.default).mock.calls
+ const calls = vi.mocked(mockFetch).mock.calls
const requestBody = JSON.parse(calls[0][1]?.body as string)
expect(requestBody).toHaveProperty('input', 'NVDA')
@@ -540,7 +521,6 @@ describe('SimStudioClient', () => {
})
it('should wrap primitive number input in input field', async () => {
- const fetch = await import('node-fetch')
const mockResponse = {
ok: true,
status: 200,
@@ -553,18 +533,17 @@ describe('SimStudioClient', () => {
},
}
- vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
+ vi.mocked(mockFetch).mockResolvedValue(mockResponse as any)
await client.executeWorkflow('workflow-id', 42)
- const calls = vi.mocked(fetch.default).mock.calls
+ const calls = vi.mocked(mockFetch).mock.calls
const requestBody = JSON.parse(calls[0][1]?.body as string)
expect(requestBody).toHaveProperty('input', 42)
})
it('should wrap array input in input field', async () => {
- const fetch = await import('node-fetch')
const mockResponse = {
ok: true,
status: 200,
@@ -577,11 +556,11 @@ describe('SimStudioClient', () => {
},
}
- vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
+ vi.mocked(mockFetch).mockResolvedValue(mockResponse as any)
await client.executeWorkflow('workflow-id', ['NVDA', 'AAPL', 'GOOG'])
- const calls = vi.mocked(fetch.default).mock.calls
+ const calls = vi.mocked(mockFetch).mock.calls
const requestBody = JSON.parse(calls[0][1]?.body as string)
expect(requestBody).toHaveProperty('input')
@@ -590,7 +569,6 @@ describe('SimStudioClient', () => {
})
it('should spread object input at root level', async () => {
- const fetch = await import('node-fetch')
const mockResponse = {
ok: true,
status: 200,
@@ -603,11 +581,11 @@ describe('SimStudioClient', () => {
},
}
- vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
+ vi.mocked(mockFetch).mockResolvedValue(mockResponse as any)
await client.executeWorkflow('workflow-id', { ticker: 'NVDA', quantity: 100 })
- const calls = vi.mocked(fetch.default).mock.calls
+ const calls = vi.mocked(mockFetch).mock.calls
const requestBody = JSON.parse(calls[0][1]?.body as string)
expect(requestBody).toHaveProperty('ticker', 'NVDA')
@@ -616,7 +594,6 @@ describe('SimStudioClient', () => {
})
it('should handle null input as no input (empty body)', async () => {
- const fetch = await import('node-fetch')
const mockResponse = {
ok: true,
status: 200,
@@ -629,11 +606,11 @@ describe('SimStudioClient', () => {
},
}
- vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
+ vi.mocked(mockFetch).mockResolvedValue(mockResponse as any)
await client.executeWorkflow('workflow-id', null)
- const calls = vi.mocked(fetch.default).mock.calls
+ const calls = vi.mocked(mockFetch).mock.calls
const requestBody = JSON.parse(calls[0][1]?.body as string)
// null treated as "no input" - sends empty body (consistent with Python SDK)
diff --git a/packages/ts-sdk/src/index.ts b/packages/ts-sdk/src/index.ts
index bde2321b68f..d1538ff5e84 100644
--- a/packages/ts-sdk/src/index.ts
+++ b/packages/ts-sdk/src/index.ts
@@ -1,5 +1,3 @@
-import fetch from 'node-fetch'
-
export interface SimStudioConfig {
apiKey: string
baseUrl?: string
@@ -107,6 +105,20 @@ export interface UsageLimits {
}
}
+/**
+ * Native fetch reports network failures as a bare `TypeError: fetch failed` and puts the
+ * underlying reason (ECONNREFUSED, DNS, TLS) on `cause`. Fold it into the message so callers
+ * keep the diagnostic detail, and return the plain message unchanged when there is no cause.
+ */
+function describeError(error: any): string | undefined {
+ const message: string | undefined = error?.message
+ const cause: string | undefined = error?.cause?.message
+ if (message && cause && !message.includes(cause)) {
+ return `${message}: ${cause}`
+ }
+ return message
+}
+
export class SimStudioError extends Error {
public code?: string
public status?: number
@@ -276,7 +288,10 @@ export class SimStudioClient {
throw new SimStudioError(`Workflow execution timed out after ${timeout}ms`, 'TIMEOUT')
}
- throw new SimStudioError(error?.message || 'Failed to execute workflow', 'EXECUTION_ERROR')
+ throw new SimStudioError(
+ describeError(error) || 'Failed to execute workflow',
+ 'EXECUTION_ERROR'
+ )
}
}
@@ -310,7 +325,10 @@ export class SimStudioClient {
throw error
}
- throw new SimStudioError(error?.message || 'Failed to get workflow status', 'STATUS_ERROR')
+ throw new SimStudioError(
+ describeError(error) || 'Failed to get workflow status',
+ 'STATUS_ERROR'
+ )
}
}
@@ -388,7 +406,7 @@ export class SimStudioClient {
throw error
}
- throw new SimStudioError(error?.message || 'Failed to get job status', 'STATUS_ERROR')
+ throw new SimStudioError(describeError(error) || 'Failed to get job status', 'STATUS_ERROR')
}
}
@@ -511,7 +529,7 @@ export class SimStudioClient {
throw error
}
- throw new SimStudioError(error?.message || 'Failed to get usage limits', 'USAGE_ERROR')
+ throw new SimStudioError(describeError(error) || 'Failed to get usage limits', 'USAGE_ERROR')
}
}
}
diff --git a/packages/workflow-renderer/package.json b/packages/workflow-renderer/package.json
index d31bb33c895..169217aba7c 100644
--- a/packages/workflow-renderer/package.json
+++ b/packages/workflow-renderer/package.json
@@ -39,7 +39,7 @@
"@sim/tsconfig": "workspace:*",
"@sim/utils": "workspace:*",
"@types/react": "^19",
- "lucide-react": "^0.479.0",
+ "lucide-react": "^0.511.0",
"react": "19.2.4",
"reactflow": "^11.11.4",
"remark-breaks": "^4.0.0",
diff --git a/scripts/check-api-validation-contracts.ts b/scripts/check-api-validation-contracts.ts
index 1712cdaa9f5..0751de73108 100644
--- a/scripts/check-api-validation-contracts.ts
+++ b/scripts/check-api-validation-contracts.ts
@@ -9,8 +9,8 @@ const QUERY_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/queries')
const SELECTOR_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/selectors')
const BASELINE = {
- totalRoutes: 977,
- zodRoutes: 977,
+ totalRoutes: 979,
+ zodRoutes: 979,
nonZodRoutes: 0,
} as const