v0.7.23: comparison table, security hardening, subflow & streaming improvements #5421
Conversation
…roll (#5409) * fix(compare): swap LangChain logo, fix comparison-table horizontal scroll Replace LangChainIcon's path with the official brand mark (light-blue link icon) instead of the prior monochrome recreation. The comparison table's grid cells were missing min-w-0, so a grid item without it sizes to its content's max-content width instead of respecting its column's fr track, pushing the whole table wider than its container and forcing a horizontal scrollbar. Add min-w-0 to every grid cell/header, and size the row-label column to minmax(140px, max-content) instead of a guessed fr ratio, so it's exactly as wide as its longest label ("Vetted first-party integrations") needs and no wider, leaving the Sim/competitor value columns their full share. * fix(compare): stacked mobile layout for the comparison table Below sm (640px) a 3-column table has no room to be legible even with the sticky label column, so switch to the standard responsive-table pattern instead: each fact stacks as label -> Sim's value -> the competitor's value, each value tagged with its product name since the column headers are no longer directly above. Pure CSS (max-sm:/sm: variants), no JS, keeping this a zero-hydration server component. * fix(compare): fix SourceLink inline-anchor overflow, align table breakpoint Root cause of the persistent table-overflow/mobile-scroll issues: SourceLink renders a plain <a> with no explicit display, so it defaults to 'display: inline'. min-width and truncate are no-ops on inline elements, so any fact with a source (nearly all of them) ignored its flex/grid parent's width constraint and could force the whole row (and thus the whole table) wider than intended, regardless of any container-level min-w-0/max-content fix. Give the anchor 'block min-w-0' so width constraints and truncation actually cascade down to the wrapped value. Also aligns the table's mobile-stack breakpoint from an ad hoc sm (640px) to lg (1024px), matching this route group's own tablet-and-below convention (.claude/rules for the (landing) group), and fixes the stacked mobile cells to override the cell's base items-center with items-stretch so the name tag and value get a real full-width box to truncate within instead of shrinking to their own content size with no boundary. * fix(compare): add min-w-0 to ColumnHeader per Greptile review ColumnHeader (the Sim/competitor logo header cells in the two fr columns) still had default min-width: auto, so an unusually long competitor name could size the header to its min-content width and push the grid wider than its column allows, same root cause as the rows already fixed.
…med messages (#5411) * fix(mothership): stop chat perf decay from permanently-animated streamed messages * fix(mothership): reset animation latches when a reused ChatContent gets replaced content * fix(mothership): keep streaming parser on settled messages to kill the drain-swap flash * fix(mothership): unify plain/special render branches to stop whole-message re-fade when options arrive * improvement(mothership): hold render-phase animation latches in useState per updated hook rules * fix(styling): translucent text-selection in form controls so field text stays readable * improvement(styling): one lighter translucent text-selection color everywhere, no forced text color * chore(styling): selection-muted tokens as 8-digit hex to match color token convention
…5407) * feat(custom-block): deploy a workflow as a reusable org-scoped block * fix(custom-block): reseed deploy form, guard duplicate publish, run child deployed * test(custom-block): isolate custom-block rows fetch in execution-core test * fix(custom-block): allow cross-workspace exec, org-scope authority, keep field ids, hide disabled * feat(custom-block): run child under source owner's identity, workspace, and env * fix(custom-block): bind publish authz to the source workflow's workspace * fix(custom-block): gate edit/delete on source-workspace admin, not org admin * chore(custom-block): rebaseline route count to 887 after staging merge * fix(custom-block): sanitize failure output so it can't leak source workflow internals * fix(custom-block): derive inputs and curated outputs from deployed state, not draft * fix(custom-block): hide disabled blocks from the toolbar palette too * fix(custom-block): bill nested + failed-run hosted cost; expose real inputs to the agent * fix(custom-block): enforce enterprise + flag gate at every consumption path
…5416) * fix(copilot): validate credential-link URL scheme before rendering Only render the credential connect link as a clickable anchor when its value resolves to an http(s) URL, reusing the isSafeHttpUrl helper already used for chat file links. * refactor(copilot): move isSafeHttpUrl to shared lib/core/utils/urls Per Greptile's convention feedback: isSafeHttpUrl was consumed by both chat and workspace/home but defined inside a feature-specific 'use client' component. Move it alongside getBrowserOrigin (which it already depends on) in lib/core/utils/urls.ts, matching this repo's shared-utility rule.
) Google Drive, Slack, and OneDrive download routes fetched provider file content without a response size cap, unlike the SharePoint download route. Add maxResponseBytes to each content fetch, reject Google Drive files whose metadata size already exceeds the cap before starting the download, and map the resulting size-limit error to a clean 413 response.
…ge-document upload routes (#5413) * fix(uploads): bound multipart body read in workspace-file and knowledge-document upload routes * fix(uploads): avoid FormData-body stream race in bounded-read tests * fix(uploads): share MAX_MULTIPART_OVERHEAD_BYTES constant across upload routes
* fix(stt): bound audio download response size Cap the audioUrl download in the STT proxy route at the platform's standard 100MB file-size ceiling, matching the pattern already used by sharepoint/download-file and other external download routes. Classify size-limit rejections as a clean 413 instead of an unhandled 500. * fix(stt): avoid double isPayloadSizeLimitError call in error handling
… requests (#5415) * fix(webhooks): validate and pin EmailBison apiBaseUrl before outbound requests Route Email Bison webhook create/delete requests through the shared DNS-validated, IP-pinned fetch used by the Teams and Slack webhook providers instead of a raw fetch to the user-configured instance URL. * fix(webhooks): restore NEXT_PUBLIC_APP_URL in emailbison tests, dedupe strict-delete warning log Test env var mutation was never restored, risking cross-file leakage in single-threaded vitest runs. Strict-mode deleteSubscription failures were logged twice (once at the throw site with context, once generically by the outer catch); the outer catch now skips its own log for errors already logged at the throw site.
…h() calls (#5419) PR #5399 fixed the callback route forgetting to pass fetchFn into the SDK's auth(), but every call site (start + callback routes) still imported the raw SDK auth() directly and had to remember to pass fetchFn: createSsrfGuardedMcpFetch() by hand — the same omission was possible again at any future call site. Add mcpAuthGuarded() in lib/mcp/oauth/auth.ts, a thin wrapper around the SDK's auth() that always defaults fetchFn to the SSRF-guarded fetch (still overridable for tests). Both routes now import mcpAuthGuarded from @/lib/mcp/oauth instead of the raw SDK auth, so omitting the guard is no longer possible by omission.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryHigh Risk Overview Security and abuse limits centralize Landing comparison table gains lg+ sticky row labels, a stacked mobile layout with per-column name tags, and truncation fixes on source links. Mothership chat reduces streaming cost: word-paced fade, a one-way drain that remounts Streamdown to drop per-char spans after reveal, unified plain/special-tag rendering to avoid end-of-message re-fade, plus a fade cutoff on very long segments; global Embedded workflow canvas disables drag-stop re-parenting into loops/parallels while still allowing repositioning. Reviewed by Cursor Bugbot for commit f456ed9. Bugbot is set up for automated code reviews on this repo. Configure here. |
| const { data: customBlocksData } = useCustomBlocks(workspaceId) | ||
| useEffect(() => { | ||
| for (const cb of customBlocksData ?? []) blockConfigCache.current.delete(cb.type) | ||
| }, [customBlocksData]) |
There was a problem hiding this comment.
Stale cache after unpublish
Medium Severity
When a custom block is unpublished, its type disappears from useCustomBlocks, but the canvas cache only clears types still in that list. getBlockConfig can keep serving the removed block’s cached config, so nodes may show outdated names, icons, and client-side schema until a full page refresh.
Reviewed by Cursor Bugbot for commit 4b13398. Configure here.
Greptile SummaryThis release bundles multiple security hardening improvements (SSRF validation on EmailBison webhook calls, bounded response sizes for Drive/OneDrive/Slack/STT downloads, multipart body limits on file uploads, MCP OAuth SSRF guard enforcement) alongside the
Confidence Score: 4/5Safe to merge with one fix recommended before the next release: the spread order in The
|
| Filename | Overview |
|---|---|
| apps/sim/lib/mcp/oauth/auth.ts | New mcpAuthGuarded wrapper defaults all MCP OAuth exchanges to the SSRF-guarded fetch, but the spread order { fetchFn: guard, ...options } allows a caller-supplied fetchFn: undefined to silently overwrite the guard. |
| apps/sim/lib/webhooks/providers/emailbison.ts | Both createSubscription and deleteSubscription now validate apiBaseUrl via DNS and use secureFetchWithPinnedIP; the AlreadyLoggedError sentinel cleanly avoids double-logging. |
| apps/sim/lib/workflows/custom-blocks/operations.ts | Core data-access and authorization layer for deploy-as-block; well-defended with org-scoping and ownership checks, though publishCustomBlock lacks retry logic for the rare shortId collision case. |
| apps/sim/executor/handlers/workflow/workflow-handler.ts | Custom block execution runs under the source workflow owner's identity; correctly skips the same-workspace assertion, strips internal child spans on success/failure, and carries only aggregate cost to the parent. |
| apps/sim/app/api/custom-blocks/route.ts | REST endpoints for listing and publishing custom blocks; correctly gates on workspace admin, feature flag, and enterprise plan, with authorization checks re-verified inside operations layer. |
| apps/sim/app/api/custom-blocks/[id]/route.ts | PATCH/DELETE for managing a custom block; authorization correctly requires admin on the source workflow's workspace (not just any org workspace). |
| apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx | Streaming performance fix: replaces the permanently-live animated Streamdown tree with a drain-then-swap lifecycle, cuts span count via sep: 'word', and caps fade animation at 6000 chars to eliminate long-task bloat. |
| apps/sim/app/api/tools/google_drive/download/route.ts | Adds maxResponseBytes and early assertKnownSizeWithinLimit for size bounding; the parseInt path for metadata.size silently passes NaN to the guard, making it a no-op for malformed size strings. |
| apps/sim/app/_styles/globals.css | Changes ::selection from solid-brand + forced white to a translucent muted tint; functionally justified (fixes invisible text on styled mirrors), but adds global CSS variables where the literal value could be inlined. |
| packages/db/schema.ts | Adds custom_block table with cascade-delete on org and workflow, unique (organizationId, type) index, and JSON outputs column; migration matches the schema definition. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Consumer Workflow Block\n custom_block_xyz] -->|canHandle| B{isCustomBlockType?}
B -->|yes| C[getCustomBlockAuthority\nscoped to consumer org]
C -->|not found / disabled| D[throw: block no longer available]
C -->|found| E[Load authority:\nworkflowId, ownerUserId,\nexposedOutputs]
E --> F[Load source workflow\nas owner, latest deployment]
F --> G[remapCustomBlockInputKeys\nid to name for Start fields]
G --> H[Run child Executor\nunder owner userId+workspace+env]
H -->|success| I[projectCustomBlockOutput\ncurated exposed outputs only\naggregate child cost]
H -->|failure| J[buildCostCarrierSpans\ncost only, no internals]
I --> K[Return to parent executor]
J --> L[throw ChildWorkflowError\ngeneric message to consumer]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[Consumer Workflow Block\n custom_block_xyz] -->|canHandle| B{isCustomBlockType?}
B -->|yes| C[getCustomBlockAuthority\nscoped to consumer org]
C -->|not found / disabled| D[throw: block no longer available]
C -->|found| E[Load authority:\nworkflowId, ownerUserId,\nexposedOutputs]
E --> F[Load source workflow\nas owner, latest deployment]
F --> G[remapCustomBlockInputKeys\nid to name for Start fields]
G --> H[Run child Executor\nunder owner userId+workspace+env]
H -->|success| I[projectCustomBlockOutput\ncurated exposed outputs only\naggregate child cost]
H -->|failure| J[buildCostCarrierSpans\ncost only, no internals]
I --> K[Return to parent executor]
J --> L[throw ChildWorkflowError\ngeneric message to consumer]
Reviews (1): Last reviewed commit: "fix(mcp): make SSRF-guarded fetch struct..." | Re-trigger Greptile
| --brand-accent: #33c482; | ||
| --brand-accent-hover: #2dac72; | ||
| --selection: #1a5cf6; | ||
| --selection-muted: #1a5cf647; |
There was a problem hiding this comment.
This edit modifies
globals.css, which the team style rule reserves for cases where a local component file genuinely cannot carry the change. ::selection is a global pseudo-element that cannot be scoped to a single component, so the change is functionally justified — but the new --selection-muted variable is only consumed by ::selection, so it could be inlined as a literal there instead of adding two new global custom properties.
| --selection-muted: #1a5cf647; | |
| /* --selection-muted: #1a5cf647; — inline in ::selection instead of adding a global variable */ |
Rule Used: Avoid editing the globals.css file unless absolute... (source)
Learned From
simstudioai/sim#367
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| throw new CustomBlockValidationError('You can only publish a workflow from its own workspace') | ||
| } | ||
|
|
||
| const ws = wf.workspaceId ? await getWorkspaceWithOwner(wf.workspaceId) : null | ||
| if (!ws?.organizationId || ws.organizationId !== organizationId) { | ||
| throw new CustomBlockValidationError('Workflow does not belong to this organization') | ||
| } | ||
|
|
There was a problem hiding this comment.
No retry for shortId collision in
publishCustomBlock
generateShortId(10) produces a 10-character identifier, which makes collisions extremely unlikely. However, if the custom_block_organization_type_unique index rejects the insert due to a collision, the error propagates as an unhandled DB constraint violation (HTTP 500), not a user-friendly message. Given that the existing guard already prevents the same workflow from being published twice, the only remaining collision path is a random-ID clash. A simple retry or an explicit catch of unique-constraint errors would surface a clean error rather than leaking internal DB error text.
… time-based reveal pacing (#5417) * improvement(chat): eased stick-to-bottom glide for streaming chat * improvement(chat): time-based word-at-a-time reveal pacing * improvement(chat): harden smooth-chase against reentrant kick/cancel during a step
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit f456ed9. Configure here.
Spread order previously let an explicit fetchFn (including fetchFn: undefined) in options silently disable the SSRF-guarded default. Fallback is now applied after the spread so the guard always wins unless a real override is passed. fix(tools): handle non-numeric Drive file size in early size check Guard the pre-download size check against a malformed metadata.size string so it's skipped explicitly instead of relying on an incidental NaN no-op; the streaming cap on the actual download still enforces the limit either way.
…es (#5422) * improvement(compare): accuracy and consistency pass on comparison pages Cross-checked facts across all comparison pages against live sources, corrected a few internal inconsistencies, and added one new fact category. No schema or rendering changes beyond a single new row. * fix(compare): align a parity check and a stale source citation * improvement(compare): cross-reference Sim's permission-groups system from rbac * improvement(compare): sharper bottom-line reasoning, minor emphasis fixes Reworded a few competitor headline claims to reflect genuine product positioning rather than a narrow feature, and gave a couple of Sim's own already-documented capabilities (model reach, integration count, live collaboration) more prominent placement. * improvement(compare): verify every cited source is live, fix a few dead links * fix(compare): correct mcpSupport boolean-icon misparse in power-automate.ts
…g from the table (#5425) dataTables and richTextEditor are required fact fields, already researched and filled in for Sim and every competitor, but neither was ever added to the row list the table actually renders.


Uh oh!
There was an error while loading. Please reload this page.