v0.7.24: frontend perf improvements, md editor fixes, mailer fix, tools audit#5458
v0.7.24: frontend perf improvements, md editor fixes, mailer fix, tools audit#5458waleedlatif1 wants to merge 32 commits into
Conversation
…heck (#5429) hasWorkspaceAdminAccess + a separate getWorkspaceWithOwner call each independently re-fetched the workspace row for the same (userId, workspaceId) pair. Consolidated into a single checkWorkspaceAccess call, matching the pattern the GET handler in this same file already uses. access.canAdmin is logically identical to hasWorkspaceAdminAccess's result (admin is the top PERMISSION_RANK, nothing else satisfies it) — no behavior change, one fewer DB round-trip per publish request.
…Permissions thin wrappers of checkWorkspaceAccess (#5430) hasWorkspaceAdminAccess and getUserEntityPermissions('workspace', ...) each independently re-implemented the same getWorkspaceWithOwner + getEffectiveWorkspacePermission fetch that checkWorkspaceAccess already does — this is exactly the duplication that let the custom-blocks POST handler drift into two separate DB round-trips for one permission resolution (fixed separately). Centralizing all three onto one implementation means a future logic change or bug fix only has to happen once, and any future caller that (accidentally) calls two of these functions together is now at least drawing from one consistent definition of "effective permission" instead of two that could diverge. Adds a `permission: PermissionType | null` field to the WorkspaceAccess return shape so getUserEntityPermissions doesn't need a second independent getEffectiveWorkspacePermission call to get the raw value. No behavior change: verified analytically (canAdmin is exactly permission === 'admin' per PERMISSION_RANK) and confirmed by an independent security review focused on argument-order and privilege-escalation risks in this kind of refactor. 616 existing tests across lib/workspaces, lib/credentials, lib/invitations, lib/copilot/vfs, and the affected API routes pass unmodified (plus 2 test assertions updated for the new additive field).
…nstant (#5427) * refactor(react-query): hoist every staleTime into a named exported constant Adds a repo-wide convention (documented in .claude/rules/sim-queries.md, CLAUDE.md, and the react-query-best-practices skill/command) that every staleTime value must come from a named exported constant instead of an inline numeric literal. This prevents a server-side prefetch and its client hook from independently duplicating the same duration and drifting out of sync, which Greptile caught on PR #5426. Applies the convention across all of hooks/queries/** and their prefetch.ts consumers. * refactor(react-query): use FOLDER_LIST_STALE_TIME in folders.ts hooks useFolders and useFolderMap still used inline 60 * 1000 despite folders.ts already exporting FOLDER_LIST_STALE_TIME — the same prefetch-drift gap this refactor closes everywhere else. Same value, no behavior change.
…igation (#5428) * improvement(sidebar): memoize workflow/folder rows for faster tab navigation Switching between workspace tabs re-rendered every workflow and folder row in the sidebar because the rows (and the shared export hooks they call) subscribed to useParams, which re-renders on every navigation. - Wrap WorkflowItem and FolderItem in React.memo (the only un-memoized leaf rows; every sibling row was already memoized). - Decouple the rows from useParams: thread workspaceId as a stable prop from WorkflowList (matching the FileList convention), and expose the live active workflowId through a stable activeWorkflowIdRef on SidebarListContext, read only in delete callbacks — never during render. - Refactor the three shared export hooks (used only by these two rows) to take workspaceId as a param instead of calling useParams internally. - Stabilize handleWorkflowClick's identity via refs so the shared list context no longer changes identity on navigation. - Lazy-init the drag-drop siblings Map ref. On a tab switch only the two rows whose active state flips now re-render. * fix(sidebar): add workspaceId to render-callback deps renderWorkflowItem/renderFolderSection now pass workspaceId into the rows, so they must list it as a dependency — otherwise a workspace switch that doesn't also change workflowId would leave the callbacks closing over a stale workspaceId (wrong-workspace deletes/exports).
…#5426) * fix(sidebar): prefetch folders and workspace permissions on cold load * refactor(sidebar): dedupe folder query and permission lookups from /simplify pass * docs(workspaces): trim getWorkspacePermissionsForViewer tsdoc to match repo convention * fix(folders): reference FOLDER_LIST_STALE_TIME constant instead of duplicating literal
…nts (#5434) cc, attachments, and html are omitted from AgentMail's webhook payload when empty rather than sent as empty arrays/null, but the zod schema marked them required. Every inbound email without a CC recipient or attachment (the common case) failed schema validation before a task row was ever created, silently breaking Sim Mailer inbox ingestion since the feature shipped. Renames from_ to from to match AgentMail's documented field name.
* fix(analytics): apply runtime CSP to all landing pages, not just / GTM/GA4 script-src and connect-src allowlist entries are gated behind isHosted, which next.config.ts's build-time CSP bakes in from whatever NEXT_PUBLIC_APP_URL was resolved at build/boot time. Only the homepage route was overridden with the request-time CSP in proxy.ts, so every other landing page (including /demo) silently lost the googletagmanager.com/google-analytics.com allowlist and GTM never fired. * style: drop inline comment from proxy CSP fix * fix(analytics): apply runtime CSP to authenticated /invite pages too handleInvitationRedirects returns NextResponse.next() for authenticated users, bypassing the catch-all block entirely, so it never picked up the runtime CSP fix in the same way. Same class of gap flagged by review.
…t, paste fidelity (#5438) * feat(rich-md-editor): table row/col toolbar, raw HTML/footnote support, paste fidelity - Add a table toolbar (add/delete row, add/delete column, toggle header, delete table) wired to stock @tiptap/extension-table commands - Add verbatim snippet nodes for raw HTML blocks, HTML comments, footnotes (def + ref), and inline raw HTML tags — these no longer force the whole document read-only, and the raw source is directly editable in place - Fix an upstream @tiptap/markdown footgun found while building this: a custom tokenizer with no explicit `start` callback corrupts the shared lexer and silently drops unrelated content elsewhere in the document - Prefer the markdown parser over generic HTML→DOM paste mapping when the plaintext clipboard side looks like markdown, even if an HTML sibling is present - Fix a stale select-all selection surviving a stream-settle content replace, which permanently highlighted every divider/image after a file regeneration * fix(rich-md-editor): address review findings from initial PR round - Collapse the stream-settle selection unconditionally, not only when setContent re-runs — the last streaming tick already syncs lastSyncedBodyRef to the final body, so the fix was previously skipped in the common streamed-content case (Cursor Bugbot) - Support GFM footnote definition continuation lines (>=4-space indented, with blank lines between paragraphs) instead of truncating to just the opening line (Greptile P1) - Fix the inline raw-HTML tokenizer to find the balanced closing tag by tracking nesting depth, instead of matching the first same-name closing tag — fixes corruption on nested same-tag elements like <span>outer <span>inner</span></span> (Greptile P1) - Stop caching the table toolbar's anchor rect by selection key — the same cell can move on screen from scrolling alone with no selection change, and the cached rect went stale (Greptile P2) * fix(rich-md-editor): fix CI type errors in raw-markdown-snippet.tsx - parseMarkdown callbacks returned null on no-match, which @tiptap/core's MarkdownParseResult type doesn't permit — switch to returning [] (empty array), matching the same no-match convention MarkdownCodeBlock already uses, with identical runtime behavior - Pin NodeViewContent's generic to 'span' (NodeViewContent<'span'> as='span'), since it defaults to 'div' and rejects other `as` values without an explicit type argument Verified with a full `tsc --noEmit` (NODE_OPTIONS=--max-old-space-size=8192, matching CI) — 0 errors, where it previously failed the Next.js build's type-check step.
* v0.6.29: login improvements, posthog telemetry (#4026) * feat(posthog): Add tracking on mothership abort (#4023) Co-authored-by: Theodore Li <theo@sim.ai> * fix(login): fix captcha headers for manual login (#4025) * fix(signup): fix turnstile key loading * fix(login): fix captcha header passing * Catch user already exists, remove login form captcha * fix(attachments): cross tenant security hardening * address comments * fix * fix * remove dead code --------- Co-authored-by: Waleed <walif6@gmail.com> Co-authored-by: Theodore Li <theodoreqili@gmail.com> Co-authored-by: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com> Co-authored-by: Theodore Li <theo@sim.ai>
…ettings pages (#5439) * improvement(inbox): align Sim Mailer settings page with design-system tokens Replace literal pixel text-size classes (text-[12px]/[13px]/[14px]/[16px]) with the named Tailwind scale tokens across inbox.tsx, inbox-enable-toggle, inbox-settings-tab, and inbox-task-list — pixel-identical, no visual change. Swap the hand-rolled toggle-row span pair for the emcn Label component, matching the toggle-row convention used elsewhere in settings. * improvement(settings): extend design-system token migration across all settings pages Replace literal pixel text-size classes with named Tailwind scale tokens (text-caption/small/sm/base/md/lg) across every other settings page and shared component (member-list, api-keys, mcp, billing, credential-sets, workflow-mcp-servers, mothership, admin, byok, copilot, custom-tools, settings-resource-row, and the ee/ access-control, data-retention, whitelabeling, and shared setting-row components) — pixel-identical, no visual change. Document the row title/subtitle token pairing and toggle-row Label convention in sim-settings-pages.md and the add-settings-page skill's audit checklist so future pages default to it.
…rency (#5436) * perf(pii): mask offloaded refs in block outputs/input + parallelize + raise concurrency - Block-output and input stages now hydrate → mask → re-store large-value refs (function/tool outputs are offloaded to refs before reaching the redactor, which treats refs as opaque) — fixes big block outputs coming back unredacted - Parallelize large-value ref hydrate/mask/re-store: collect refs across the whole payload and process them with bounded concurrency instead of one sequential pass per key (PII_REF_CONCURRENCY, default 4) - Raise mask-batch chunk concurrency default 4 -> 64 to saturate the load-balanced Presidio fleet behind the internal ALB (PII_MASK_CHUNK_CONCURRENCY, env-tunable) * fix(pii): keep resolveReplacements mapper total (respect mapWithConcurrency contract) - Catch per-ref errors inside the mapWithConcurrency mapper and rethrow the first after the pool drains, instead of letting a throwing mapper reject the pool (the util documents fn MUST NOT reject). Preserves fail-fast abort in throw mode. - Add multi-ref throw-mode test: one of several refs failing aborts the redaction * feat(pii): make route->Presidio chunk concurrency env-tunable (PII_SERVICE_CHUNK_CONCURRENCY) Was hardcoded to 4; now env-tunable (default 4) so the inner route->Presidio fan-out can scale with the fleet alongside the outer PII_MASK_CHUNK_CONCURRENCY. * feat(pii): combined /redact + /redact_batch endpoint (one round-trip) - Presidio: add /redact and /redact_batch (analyze + anonymize server-side, feeding analyzer results straight to the anonymizer, no dict round-trip). All existing endpoints kept, so old clients keep working during rollout. - App: maskPIIBatch now uses /redact_batch (halves app<->service round-trips, no shipping text back up for anonymize). Falls back to the legacy analyze_batch + anonymize_batch pair on a 404 (older Presidio image), so the app is safe to deploy before or after the service in either order. Same length-check fail-closed guarantee.
…5432) Claude-Session: https://claude.ai/code/session_011Lmib23o5aQtBr7ZPhgpgf Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…#5440) * fix(rich-md-editor): fix raw-HTML-block fragmentation and styling - RawHtmlBlock previously delegated to marked's built-in block-HTML tokenizer, which (per CommonMark's HTML-block-type-6 rule) stops at the first blank line. A real <details> with a paragraph inside — the common case — fragmented into a raw chip, an ordinary rendered paragraph, and a second raw chip, stranding real content in between. - Fixed with a custom block-level tokenizer that scans to the tag's matching close (reusing the balanced open/close depth-tracking already built for nested inline HTML), spanning blank lines, restricted to CommonMark's own block-tag whitelist (details, div, table, section, etc.) so tags that can legitimately start a paragraph (em, a, span, code, kbd) are left untouched. - Dropped the warning-colored tint on raw HTML/footnote blocks (color-mix with --warning read as an error state) in favor of the same neutral surface as existing code blocks — the hover badge alone now signals "this is raw, unrendered text". * fix(rich-md-editor): fix indented HTML, quoted attributes, and code-mention edge cases - CommonMark allows up to 3 leading spaces before a block-HTML opening line; the new block tokenizer required column 0, so an indented <details>/<div> still fragmented across blank lines. Fixed by splitting off the leading indent, matching against the rest, and stitching the indent back onto raw. - The open-tag and balanced-close regexes stopped at the first `>`, even inside a quoted attribute value (e.g. data-example="a > b"), producing wrong match lengths and miscounting a same-tag mention inside the quoted value as a real nested tag. Replaced with an attribute-aware pattern that treats a full quoted value (including any interior >) as one unit. - The balanced-tag scan couldn't distinguish real markup from a tag name mentioned inside inline code or a fenced code block. Now masks code regions (same-length filler, positions preserved) before scanning, so a properly-escaped mention (backticks or a fenced example) is never miscounted. A genuinely bare, unescaped mention outside code remains a known, inherent limitation of regex-based tag matching (shared by real HTML parsers given the same ambiguous input) — verified this can never lose data or hang, only reflow to a stable fixpoint on save. * fix(rich-md-editor): fix blockquoted-fence masking and void-tag balance scan - maskCodeRegions's fenced-code regex required fence markers at column 0, so a fence quoted inside a markdown blockquote (each line prefixed with `> `) wasn't recognized, leaving tag mentions inside it visible to the balance scanner. Extended the fence pattern to tolerate an optional blockquote prefix on both the opening and closing fence line. - BLOCK_HTML_TAG_NAMES includes several void elements (link, meta, base, hr, ...) that have no closing tag at all. The block tokenizer only treated an explicit self-closing `/>` as complete, so a void tag without one would scan the rest of the document for a `</meta>` that will never appear, risking a false match on a later same-name mention. Now reuses the existing VOID_TAGS set (already used by the inline tokenizer) to treat these as complete right after the open tag. Also restored `hr` to the whitelist, which was accidentally dropped when transcribing marked's tag list. * fix(rich-md-editor): tolerate an indented (non-blockquoted) fence in code masking maskCodeRegions's fence pattern handled a blockquoted fence (`> \`\`\``) but still required the fence marker at column 0 otherwise, missing CommonMark's independent up-to-3-space fence indent tolerance this codebase already relies on elsewhere (FENCE_OPEN/FENCE_CLOSE in markdown-parse.ts). An indented fence's tag-name mention could still end a whitelisted block early. Fixed by combining both allowances in one prefix pattern (zero-or-more blockquote levels, each independently followed by up to 3 more spaces of indent). * fix(rich-md-editor): mask HTML comments in balanced-tag scan A tag-name mention inside an HTML comment (e.g. `<!-- see </div> below -->`) inside a whitelisted raw HTML block could still be matched by the balance scan and end the block early, fragmenting it. maskCodeRegions already masked fenced/inline code the same way; extend it to mask comments too.
Server rows derived "Loading..." from a workspace-wide isLoading/isFetching aggregate instead of that server's own per-server query, so a row could sit stuck on "Loading..." (or flicker back to it) whenever any other server's tool query was fetching, only clearing after a full page reload.
…surface more entity fields (#5443) * improvement(new-relic): validate integration against NerdGraph docs, surface more entity fields - Audited all 4 New Relic tools (nrql_query, search_entities, get_entity, create_deployment_event) plus the block against live NerdGraph API docs; request/response shapes, auth, region endpoints, and GraphQL injection handling all confirmed correct. - Added domain, reporting, alertSeverity, and tags to get_entity and search_entities outputs — stable Entity schema fields we weren't surfacing. Purely additive, no existing fields changed. - Skipped NerdGraph's aiIssues/incidents API; New Relic marks it "unsafe experimental" and requires an opt-in header, not worth the fragility. * fix(new-relic): normalize search_entities output, share entity normalization search_entities passed raw NerdGraph entities straight through, so tags/ domain/reporting/alertSeverity could arrive as null/undefined at runtime despite the non-nullable NewRelicEntity type contract — a .map() on a null tags array would throw. get_entity already normalized these fields correctly. Extracted the normalization into a shared normalizeNewRelicEntity() in utils.ts and reuse it from both tools, so the two can't drift again. * fix(new-relic): gate alertSeverity behind required GraphQL interface fragments alertSeverity isn't a direct field on New Relic's Entity/EntityOutline types — it only exists on the AlertableEntity/AlertableEntityOutline sub-interfaces, and NerdGraph rejects the query outright without the inline fragment. get_entity.ts (entity(guid), returns Entity) now uses `... on AlertableEntity { alertSeverity }`; search_entities.ts (entitySearch, returns EntityOutline) now uses `... on AlertableEntityOutline { alertSeverity }`. Confirmed against New Relic's live NerdGraph entities API docs.
…ad/campaign lifecycle tools (#5448) * improvement(instantly): validate integration against live API, add lead/campaign lifecycle tools - Verify all existing Instantly tools against the live API v2 spec - Add instantly_patch_lead, instantly_pause_campaign, instantly_delete_campaign - Fix activate_campaign response mapping to surface the full campaign object - Restore status/ai_sales_agent_id list_campaigns filters * fix(instantly): guard against empty patch_lead request body Follows the same pattern used in tools/langsmith/update_run.ts.
…ug (#5444) * fix(cloudflare): align integration with live API, fix type-coercion bug - fix update_zone_setting body builder: was blindly JSON.parse-ing every value, silently coercing scalar settings (e.g. min_tls_version "1.2") to the wrong type; now only parses object/array literals - fix list_dns_records name/content/tag filters to use Cloudflare's exact-match dotted param names (name.exact/content.exact/tag.exact) - remove auto_minify (never a real setting ID) and minify (deprecated and removed from the live zone-settings API in Aug 2024) - remove dead jump_start param from create_zone (not part of the current POST /zones schema) - fix block bgColor from #F5F6FA to Cloudflare's actual brand orange #F38020 - add missing secondary zone type option and plan.id sort option - expand BlockMeta skills/templates * fix(cloudflare): reject empty browser_cache_ttl value instead of coercing to 0 * fix(cloudflare): address Greptile review feedback - use trimmed value consistently in update_zone_setting fallback paths - document that tag_match has no effect with a single exact-match tag filter (Cloudflare's API only combines multiple tag conditions) * fix(cloudflare): coerce non-string setting values before trim Wand-generated or block-referenced values can arrive as a number at runtime despite the declared string param type, which crashed the body builder's .trim() call. * fix(cloudflare): harden null/undefined value handling, fix stale tag description - coerce null/undefined value to empty string instead of literal "null"/"undefined" strings before trimming - fix stale block-level 'tag' input description left over from the comma-separated-tags -> exact-match-tag filter fix * fix(cloudflare): pass through structured array/object values as-is A block-referenced array/object value was being blindly stringified before the JSON-shape check, so String([...]) comma-joined arrays and String({...}) produced the literal "[object Object]" instead of the JSON shape Cloudflare expects (e.g. for ciphers). Already-structured values now pass straight through. * revert(cloudflare): keep original block bgColor (#F5F6FA)
…gaps (#5442) * improvement(microsoft-ad): add pagination support and fill BlockMeta gaps Full validate-integration pass against live Microsoft Graph API docs. No critical bugs found (endpoints, methods, params, and OAuth scopes were already correct). Fixed the real gaps: - List Users/Groups/Group Members had no way to page past the default Graph page size (100, max 999 via $top), which silently truncated results for the block's own audit/sweep templates. Added a nextLink input/output following the same convention as microsoft_dataverse. - Create Group's visibility dropdown was missing HiddenMembership, a valid Microsoft 365-only value that can only be set at creation time. - Rounded out BlockMeta skills (3 -> 5) with two more real, tool-grounded use cases: directory search and ad hoc group membership changes. * fix(microsoft-ad): correct $search syntax and create-user response fields Independent 4-agent re-audit against live Graph docs surfaced two real bugs predating this PR: - list_users/list_groups sent $search="<term>" with no property prefix. Graph requires the "property:value" form for directory-object search (e.g. "displayName:term" OR "mail:term") and 400s on a bare string. - create_user's POST had no $select, so Graph's default create response omits department/accountEnabled even when submitted, making transformResponse report them back as null. Added the same $select used by list/get so the response reflects what was actually set. Also tightened create_group's visibility description: only HiddenMembership is create-only: Private/Public can still be changed after creation via Update Group. * fix(microsoft-ad): validate nextLink origin, allow nextLink-only pagination Greptile and Cursor both flagged the same real issue: nextLink was passed straight to fetch() as the request URL with no origin check, while the OAuth bearer token was always attached. A crafted or prompt-injected nextLink pointing outside graph.microsoft.com would exfiltrate the token. Fix: reuse the existing assertGraphNextPageUrl/getGraphNextPageUrl helpers from tools/sharepoint/utils (already the shared pattern for SharePoint, OneDrive, Teams, Planner, Outlook, Excel Graph pagination) instead of a bespoke unvalidated pass-through. Cursor also caught that list_group_members required groupId even when only nextLink was supplied for a later page. Relaxed groupId to optional at the tool level, matching how sharepoint_get_list treats its analogous listId param — the URL builder still throws a clear error if neither groupId nor nextLink is given. * fix(microsoft-ad): allow $search+$filter combo, escape backslashes, fix pagination UX Second independent 4-agent re-audit of the final state (post security fix) surfaced 3 more real issues: - list_users/list_groups threw an error whenever $search and $filter were both supplied, claiming Graph doesn't support combining them. It does (AND semantics, documented) — the check was blocking valid, documented usage for no reason. Removed it. - The $search term escaping only handled embedded double quotes, not backslashes, which Graph's own escaping rule also requires. Fixed the replace order (backslashes first, then quotes). - list_group_members's Group ID field was still hard-required in the block UI for every operation including list_group_members, undermining the nextLink-only pagination path added earlier (the tool itself no longer requires it). Dropped list_group_members from the UI-required list, matching the tool's own conditional requirement — the runtime "Group ID is required" check still catches a genuinely empty call.
…verage (#5446) * feat(pagerduty): validate integration + add 9 tools for deeper API coverage - fix list tools' total field to null-default (matches PagerDuty spec, was wrong 0 default) - add pagination offset across all list tools - add incidentKey, resolution, urgencies, triggered status support - add get_incident, get_service, list_escalation_policies, list_incident_alerts, list_schedules, list_users, merge_incidents, snooze_incident tools (REST API v2) - add send_event tool (Events API v2) for trigger/acknowledge/resolve via routing key - add 2 new BlockMeta skills grounded in documented PagerDuty workflows * fix(pagerduty): address review findings on send_event/snooze/merge validation - match required condition to visibility condition for eventSummary/eventSource (trigger-only) - validate trigger payload has summary/source/severity before sending, throw descriptive error - validate snooze duration is finite and within PagerDuty's 1-604800 range - drop empty segments when splitting merge source incident IDs * fix(pagerduty): reject empty merge sources, enforce dedupKey, require integer snooze duration - merge_incidents: throw if source_incidents ends up empty after filtering blanks - send_event: require dedupKey for acknowledge/resolve to match block's required condition - snooze_incident: require an integer duration, not just a finite number * fix(pagerduty): reject resolution note unless status is resolved PagerDuty only accepts an incident's resolution field when status is being set to resolved in the same request; sending it otherwise gets rejected by the API with an opaque error. * docs(pagerduty): mention triggered as a valid update_incident status
…endpoints (#5447) * feat(ahrefs): validate integration, fix cents/column bugs, add 13 v3 endpoints Audited the existing Ahrefs integration against live API v3 docs and fixed real bugs: broken_backlinks selected the wrong column (http_code_target instead of http_code), and keyword_overview/metrics/metrics_history returned CPC/cost fields in USD cents without converting to USD. Also fixed the top-pages mode dropdown missing the "exact" option. Added 13 new tools covering previously-unsupported Ahrefs v3 endpoints: Rank Tracker (overview, SERP overview, competitors overview, competitors stats), Batch Analysis, Site Audit page explorer, four history/trend endpoints (domain rating, metrics, referring domains, keywords), Related Terms, Anchors, and Paid Pages. Note: the CPC/cost unit fix silently shifts existing organicCost/paidCost/cpc values by 100x for any workflow already consuming them (the old values were wrong, in cents instead of dollars). * fix(ahrefs): convert remaining cents-to-USD fields, drop unneeded fallback key Rank Tracker SERP Overview's value field, Competitors Stats' trafficValue, and Competitors Overview's nested competitor value were all left in USD cents while every other monetary field in the integration converts to USD - fixed for consistency with the rest of the integration. Also drops the unverified `data.results` fallback in Batch Analysis; the Ahrefs v3 docs confirm the response key is always `targets`. * docs(ahrefs): regenerate docs to reflect cents-to-USD conversion fix The generated docs page was stale after the previous commit converted rank_tracker_serp_overview.value, rank_tracker_competitors_stats.trafficValue, and rank_tracker_competitors_overview's nested value from cents to USD - regenerating picks up the corrected field descriptions. * fix(ahrefs): revert incorrect broken_backlinks column, fix missed top_pages conversion, revert unverified competitor value conversions Independent re-verification against live Ahrefs v3 docs surfaced two real regressions from the earlier fix rounds and one missed conversion: - broken_backlinks: the earlier fix changed the selected column from http_code_target to http_code, but the docs say http_code is the *referring page's* status and http_code_target is the *broken target page's* status - the tool needs the latter (matches its own output description). Reverted to http_code_target. - top_pages: value field was never divided by 100 despite the docs stating it's in USD cents and the output already claiming USD - fixed. - rank_tracker_competitors_stats.trafficValue and the nested competitor.value in rank_tracker_competitors_overview were converted from cents to USD last round on a "match every other monetary field" assumption, but the live docs do not document these two fields as cents (unlike every field that was correctly converted). Reverted to passthrough and dropped the "(USD)" claim from their descriptions until Ahrefs documents the unit. Also added the missing "exact" mode option to top_pages' mode param description, matching every sibling tool. * fix(ahrefs): convert rank tracker competitor value/trafficValue to USD Both bots independently flagged these two fields as inconsistent with every other monetary field in the integration, all 7 of which are explicitly documented as USD cents. Neither field has explicit unit documentation (one is undocumented as cents, the other's schema isn't statically retrievable at all), but given the unanimous pattern across every other verified field and no contrary evidence, converting for consistency is the better bet than leaving them as an outlier. * style(ahrefs): drop non-TSDoc inline comments introduced in this PR Repo convention disallows non-TSDoc comments; removed the three explanatory // comments this PR added next to cents-to-USD conversions (keyword_overview, paid_pages, related_terms) - pre-existing comments elsewhere in the file are untouched, out of scope for this PR. * fix(ahrefs): default optional country to us consistently across all tools paid_pages, metrics_history, keywords_history, and batch_analysis were the only 4 of the 11 tools with an optional country param that didn't fall back to "us" when omitted, unlike domain_rating, metrics, keyword_overview, organic_keywords, organic_competitors, top_pages, and related_terms - all of which default client-side. Aligned all four to the same convention so direct tool/agent calls without an explicit country get the same behavior regardless of which operation is used. * fix(ahrefs): split shared date subBlock id for datetime-format operations site_audit_page_explorer and rank_tracker_serp_overview both reused the generic 'date' subBlock id with YYYY-MM-DDThh:mm:ss semantics, while every other operation using that same id expects YYYY-MM-DD. Switching operations without clearing the field could carry a stale wrong-format value into the new operation's request. Split into distinct ids (crawlDate, asOfDate) mapped back to each tool's date param in tools.config.params.
n8n's pricing standout feature implied a per-step pricing contrast that doesn't hold up; reframe around the verified unlimited-seats difference.
* fix(comparison): correct false-contrast claims across competitor pages Audited every Sim-vs-competitor page against Sim's own facts; fixed standout/limitation claims that implicitly overstated a Sim gap that doesn't actually exist, plus a few stale or under-sourced facts. * fix(comparison): address review feedback on wording precision Clarify the pipedream acquisition-status claim, fix circular OIDC phrasing, correct a source/claim mismatch, and remove a stale SOC2 implication. * fix(comparison): fix issues introduced by the previous fix pass Second independent audit pass on the fixed pages caught a few new problems from the edits themselves: a stale/mismatched source, an internal contradiction, an unverified superlative, a still-dominant false-contrast paragraph, a broken repo link, an editorializing note leaking into shipped copy, and a duplicated/grammatically broken entry. * fix(comparison): qualify self-reported Series B figure in value field The confidence downgrade to 'estimated' wasn't reflected in the value field itself, which still stated the Series B/total funding as plain fact.
* feat(attio): add attribute tools + fix API alignment gaps
- Add 4 new tools: attio_list_attributes, get_attribute, create_attribute, update_attribute
- Add missing completed_at field to task tools
- Fix note tags output to match Attio's actual response shape (workspace-member vs record tags)
- Fix attribute outputs missing is_default_value_enabled, default_value, relationship
- Fix create_comment sending both entry and thread_id (Attio requires exactly one)
- Wire attribute pagination + task sort into the block
* fix(attio): address review feedback on attribute tools
- Throw on invalid config JSON instead of silently overwriting with {}
(create_attribute, update_attribute)
- create_attribute: omit config field entirely when not provided instead
of always sending {}
- Add "Leave unchanged" option to Required/Unique dropdowns so
update_attribute no longer clears existing constraints on unrelated
field updates
- Fix stale sort param description on list_tasks (was missing
completed_at:asc/desc variants)
* fix(attio): fix silent update-clobber bugs + complete comment mutual-exclusion
- create_comment: support all 3 of Attio's mutually-exclusive comment
targets (thread_id, record, entry), not just thread_id/entry
- Fix update_task silently clearing is_completed on unrelated field
updates (taskIsCompletedUpdate now defaults to "leave unchanged")
- Fix update_list silently resetting workspace_access to full-access on
unrelated field updates (listWorkspaceAccessUpdate, same pattern)
- create_attribute: restore config:{} as always-required per Attio's
schema (previously omitted it entirely, which is invalid on create)
- create_record/update_record/assert_record/list_records: throw on
invalid values/filter/sorts JSON instead of silently substituting {}
(was a silent data-loss risk on malformed input)
- get_task: drop stray Content-Type header on a GET request
- types.ts: remove two dead unreferenced interfaces, fix
workspaceMemberAccess type (was string, is actually an array)
* fix(attio): gate operation-specific param mapping on current operation
Params like taskIsCompleted/listWorkspaceAccess/attributeIsMultiselect/
attributeIsArchived are only meant for one specific operation, but their
mapping wasn't checking params.operation. A stale value persisted in
block state from a prior operation selection (e.g. switching the
dropdown from create_task to update_task) could leak through and
override the "leave unchanged" default on the other operation.
* fix(attio): explicit comment target selector + attribute archived tri-state
- Add "Leave unchanged" default to attributeIsArchived dropdown, same
pattern as isRequired/isUnique, so a stale archived flag from an
earlier edit can't unarchive an attribute on an unrelated update
- create_comment: replace field-presence inference (which let stale
record fields hijack a list-entry comment or vice versa) with an
explicit commentTarget selector (List Entry / Record / Reply to
Thread). Only the fields for the selected target are ever forwarded
* fix(attio): gate remaining unscoped param mappings + comment-target back-compat
- taskFilterCompleted (list_tasks filter) was mapped to isCompleted for
every operation; gate it to list_tasks so it can't override the
isCompleted value on an unrelated update_task call
- Generic threadId (get_thread) was unconditionally forwarded and
create_comment prioritizes thread_id first, so a leftover threadId
from configuring get_thread could silently hijack a comment meant for
a list entry or record; gate it to get_thread
- Default commentTarget to the pre-existing behavior (entry, or thread
if commentThreadId is set) when absent, so blocks saved before this
field existed keep working instead of throwing
* fix(attio): gate every param mapping by its operation, eliminate stale-value class of bugs
The params() function reuses cleanParams keys (title, content, apiSlug,
filter, sorts, recordId, entryId, list, object, threadId, ...) across
several unrelated operation families. Every mapping was unconditional on
presence alone, so a stale value left in block state from a previously
selected operation could silently leak into an unrelated request and
overwrite the intended value (last-write-wins on a shared key).
Rewrote the whole function to gate every line by params.operation
against the exact operation set its subBlock's condition exposes it
under, closing this entire bug class in one pass instead of patching
individual instances as they were found in review.
Also split attributeIsRequired/attributeIsUnique into create-only
(default false) and update-only (tri-state, default "leave unchanged")
variants — the shared field let an explicit Yes/No choice from
create_attribute carry over and silently change constraints on an
unrelated update_attribute call.
* fix(attio): preserve legacy taskIsCompleted/listWorkspaceAccess on update
taskIsCompleted and listWorkspaceAccess pre-date this PR as fields
shared between create and update operations (confirmed present on
origin/staging). Splitting them into create/update variants earlier
this session meant existing saved blocks with a value stored under the
legacy field name would silently stop applying it on update_task /
update_list once the new -Update field (always undefined for old
blocks) took over. Fall back to the legacy field when the new field is
untouched, so old saved workflows keep behaving exactly as before.
…nd enrichment cells (#5449)
…nment gaps (#5450) * feat(aws): expand SES/STS/Secrets Manager tool coverage, fix API alignment gaps - SES: add suppression list management, email identity CRUD, template update, configuration set creation, custom verification email (10 new tools); fix silent httpsPolicy drop in create_configuration_set and unvalidated suppression reason enum in list_suppressed_destinations - STS: add AssumeRoleWithWebIdentity and AssumeRoleWithSAML (unsigned, no static credentials required); extend assume_role with policyArns/tags/transitiveTagKeys session params - Secrets Manager: add describe_secret, tag_resource, untag_resource, restore_secret, rotate_secret; fix list_secrets dropping rotation/version metadata fields; normalize tool versions to 1.0.0 and alphabetize registry entries All 31 tools verified param-by-param against live AWS API docs across two independent audit passes. * fix(aws): address review findings on SES config/identity and Secrets Manager rotation - ses_create_configuration_set: validate suppressedReasons against BOUNCE/COMPLAINT enum before calling AWS (was silently reaching AWS as a generic 500 for bad values); tags now a proper Zod array schema instead of a string with route-side JSON.parse - ses_create_email_identity: dkimSigningAttributes and tags now proper Zod object/array schemas instead of strings with route-side JSON.parse, matching the pattern used elsewhere (e.g. sts_assume_role tags, secrets_manager_tag_resource) - secrets_manager_rotate_secret: reject automaticallyAfterDays and scheduleExpression when both are supplied — AWS RotationRules accepts only one - sts createUnauthenticatedSTSClient: corrected a misleading comment claiming these calls are fully unsigned; the SDK still falls through its default credential provider chain * fix(ses): correct json input types for tags/dkimSigningAttributes The SES block declared the tags and dkimSigningAttributes block inputs as 'string' instead of 'json', so the generic block executor never parsed the JSON code-editor value before forwarding it — workflow runs sent a raw JSON string where the contract now expects a structured object/array, failing validation. Also corrected the corresponding tool param TypeScript types, which were still typed as string | null. * fix(ses): stop coercing switch string 'false' to true in create_configuration_set Boolean('false') evaluates to true, so turning off the reputationMetricsEnabled or sendingEnabled switch sent the opposite of the user's choice to SES. Match the established === 'true' string comparison pattern used elsewhere in the codebase. * fix(sts): stop double-parsing assume_role session tags input tags was declared as a 'json' block input, so the generic executor JSON.parse'd it before the switch-case handler ran — but that handler already converts the raw table-rows array (or a passthrough string) into the JSON string the sts_assume_role contract expects. Declaring it 'json' broke that conversion for non-string inputs. Reverted to 'string' so the handler's existing string/array disambiguation runs on the untouched raw value. * fix(sts): supply placeholder credentials to the unauthenticated client createUnauthenticatedSTSClient omitted credentials entirely, so the SDK's signing middleware fell through the default credential provider chain and threw CredentialsProviderError before the request was sent in any environment with no ambient AWS identity — even though AssumeRoleWithWebIdentity/AssumeRoleWithSAML never check the signature. Static placeholder credentials skip that resolution without granting or requiring any real IAM identity.
…it (#5454) * fix(microsoft-excel): clean up dead code found during integration audit - Remove unreachable 'update' operation subblocks (Google Sheets copy-paste leftover — never a selectable dropdown option and not handled by the tool selector or any registered tool) - Fix microsoft_excel_table_add to trim spreadsheetId and OData-escape tableName, matching every other tool in this file - Drop phantom/dead type fields: Google-Sheets-only insertDataOption and responseValueRenderOption (never used), never-populated sheetId/sheetName/title/sheets metadata fields, unused rowIndex, and the unconfigurable majorDimension param (hardcoded to 'ROWS' now that it's inlined) * fix(microsoft-excel): fix write output field mapping + OData escaping gaps Found in an independent second-pass audit against the live Graph API docs: - microsoft_excel_write (v1) transformResponse read updatedRange/updatedRows/updatedColumns/updatedCells from the Graph response — none of these fields exist on the workbookRange resource (the real fields are address/rowCount/columnCount), so these four output fields were always undefined. Fixed to read the actual fields, matching what the v2 write tool already does correctly. - read.ts and write.ts (v1 and v2) built worksheets('name') OData URLs with only encodeURIComponent, skipping escapeODataString — a worksheet name containing an apostrophe would produce a malformed request. Every other tool in this integration already escapes correctly; read/write were the outliers. - write.ts (v1) also wasn't trimming spreadsheetId before use, inconsistent with every other tool here.
…tive paste tests, editor docs (#5455) * fix(rich-markdown-editor): show the col-resize cursor on table column borders prosemirror-tables toggles a `resize-cursor` class on the editor while the pointer is over a column boundary, but there was no rule to change the cursor — the blue resize handle showed with no cursor affordance. Add the scoped `col-resize` rule. * test(rich-markdown-editor): exhaustive markdown paste coverage Cover every rich construct (headings, marks, lists, task lists, blockquote, code block, image, thematic break, table), markdown parsed despite an HTML sibling, multi-block order, read-only rejection, and the defer/verbatim cases for non-markdown input. * docs(editor): add rich markdown editor page Document the inline rich markdown editor — formatting, structure, lists, tables, code blocks, images, the slash menu, and markdown fidelity — with a rendered overview screenshot. * test(rich-markdown-editor): scope paste tests to what MarkdownPaste actually gates Inline-only marks (single-asterisk italic, ~~, single-backtick code) are intentionally not detected by looksLikeMarkdown (single `*` would false-positive on e.g. `*args`); they route through the Markdown extension's own paste path, not MarkdownPaste. Move them from the rich-render cases to the defers-to-default cases so the suite tests the handler it names.
|
Too many files changed for review. ( Bypass the limit by tagging |
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryHigh Risk Overview Hardens attachments and PII — file download/parse routes derive storage context from the object key instead of trusting client Performance and data-path refactors — folder listing moves to shared Integration and AWS coverage — new internal routes and SDK helpers for Secrets Manager (describe, tag/untag, restore, rotate) and SES (identities, configuration sets, suppression list, templates, etc.); docs expanded for Ahrefs, Cloudflare, SES, STS, Secrets Manager. New Files → Editor doc page in the nav. Engineering standards (no runtime behavior) — React Query rules now require named exported Reviewed by Cursor Bugbot for commit e9a922d. Bugbot is set up for automated code reviews on this repo. Configure here. |
* feat(pii): env-driven uvicorn worker count (PII_WORKERS)
Launch the Presidio service with --workers from the PII_WORKERS env var so one
image scales per task size (set PII_WORKERS = the task's vCPU count) without a
rebuild. `sh -c exec` expands the var while keeping uvicorn as PID 1 for clean
SIGTERM. Defaults to 1, so local/self-hosted is unchanged. Each worker loads the
spaCy models independently (~3.3GB measured), so task memory must be sized to
PII_WORKERS x ~3.3GB + overhead (set in infra alongside PII_WORKERS).
* harden(pii): quote PII_WORKERS expansion + raise healthcheck start-period for multi-worker
- Quote ${PII_WORKERS} so a malformed value fails uvicorn arg-parsing instead of
being shell-interpreted (verified: '1; echo X' rejected as non-integer, X not run)
- Bump HEALTHCHECK start-period 180s -> 300s: N workers load the spaCy models in
parallel, stretching cold start beyond the single-worker case
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 07a1224. Configure here.
| .split(',') | ||
| .map((r) => r.trim()) | ||
| .filter(Boolean) as SuppressionListReason[]) | ||
| : null |
There was a problem hiding this comment.
SES config invalid suppression reasons
Low Severity
create-configuration-set splits suppressedReasons and casts tokens to SuppressionListReason without checking values. The new list-suppressed-destinations route rejects invalid reasons with 400, but this path can forward bad strings to the AWS API.
Reviewed by Cursor Bugbot for commit 07a1224. Configure here.
… menu (#5459) * fix(tables): resolve column ids to display names in enrichment edit sidebar * feat(tables): scope group-header run menu to the active filter
* fix(images): fix stale images * Fix comment
…nations (#5461) startDate/endDate were passed through new Date(...) with no validity check, so a malformed non-empty string became an Invalid Date and was still forwarded to AWS, surfacing as a generic 500. The contract now rejects unparseable date strings with a clear 400.


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