Commit Graph
5258 Commits
Author SHA1 Message Date
Waleed d64ad856f1 improvement(microsoft-ad): add pagination support and fill BlockMeta 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.
2026-07-06 15:48:43 -07:00
Waleed 6d5ac583bc fix(cloudflare): align integration with live API, fix type-coercion bug (#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)
2026-07-06 15:41:14 -07:00
Waleed 0916b193d9 improvement(instantly): validate integration against live API, add lead/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.
2026-07-06 15:34:00 -07:00
Waleed 0363451539 improvement(new-relic): validate integration against NerdGraph docs, 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.
2026-07-06 15:33:18 -07:00
Waleed a342424ffe fix(mcp): scope tools loading state to each server's own query (#5441)
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.
2026-07-06 14:35:56 -07:00
Waleed 018283b29e fix(rich-md-editor): raw-HTML-block fragmentation + styling follow-up (#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.
2026-07-06 14:10:32 -07:00
Theodore LiandClaude Fable 5 dee814b6fa fix(db): bound knowledge connector and document fan-out concurrency (#5432)
Claude-Session: https://claude.ai/code/session_011Lmib23o5aQtBr7ZPhgpgf

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 16:35:11 -04:00
Theodore Li 34b2abadcb perf(pii): mask offloaded refs + parallelize + raise redaction concurrency (#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.
2026-07-06 16:33:44 -04:00
Waleed 9538beb1b8 improvement(settings): align design-system text-scale tokens across settings 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.
2026-07-06 13:04:46 -07:00
846e0f5562 fix(attachments): cross tenant hardening (#5310)
* 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>
2026-07-06 12:48:58 -07:00
Waleed 5acdaeea02 feat(rich-md-editor): table row/col toolbar, raw HTML/footnote support, 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.
2026-07-06 12:20:58 -07:00
Waleed 89e5a142e2 fix(analytics): GTM/GA4 missing on /demo and other landing pages (#5437)
* 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.
2026-07-06 11:49:06 -07:00
Waleed 8bc0d9717c fix(mailer): correct AgentMail webhook message schema field requirements (#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.
2026-07-06 10:29:44 -07:00
Waleed 4fa1e045bd fix(sidebar): prefetch folders and workspace permissions on cold load (#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
2026-07-05 11:05:09 -07:00
Waleed 65435f89aa improvement(sidebar): memoize workflow/folder rows for faster tab navigation (#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).
2026-07-05 10:59:53 -07:00
Waleed 487166f414 refactor(react-query): hoist every staleTime into a named exported constant (#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.
2026-07-05 10:59:38 -07:00
Waleed 20e86543fb refactor(permissions): make hasWorkspaceAdminAccess and getUserEntityPermissions 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).
2026-07-05 10:59:26 -07:00
Waleed 732e4ec975 fix(custom-blocks): dedupe redundant workspace lookup in POST admin check (#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.
2026-07-05 10:59:16 -07:00
Waleed 85f80fa213 fix(compare): render two already-populated fact rows that were missing 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.
2026-07-04 19:18:39 -07:00
Waleed 82eb6dce42 fix(404): use ChipLink for return-home CTA on root not-found page (#5424)
Matches the emcn ChipLink pattern already used by sibling landing not-found pages instead of a hand-rolled Link with raw Tailwind classes.
2026-07-04 19:07:17 -07:00
Waleed ee1824caef improvement(compare): accuracy and consistency pass on comparison pages (#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
2026-07-04 16:50:00 -07:00
Waleed 3edaae31fe fix(mcp): correct fetchFn fallback order in mcpAuthGuarded (#5423)
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.
2026-07-04 16:44:46 -07:00
Theodore Li f456ed9a0b improvement(chat): smooth streaming — eased stick-to-bottom glide and 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
2026-07-04 19:02:50 -04:00
Waleed 4b133984c6 fix(mcp): make SSRF-guarded fetch structurally mandatory in OAuth auth() 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.
2026-07-04 15:40:38 -07:00
Waleed 0d6994d3cc refactor(uploads): hoist execution permission check out of upload loop, dedupe permission-gate tests (#5420)
/simplify pass on PR #5404: resolve the execution-context workspace permission
check once per request instead of once per uploaded file, and collapse
repeated request-construction boilerplate in the new permission-gate tests
into a shared helper.
2026-07-04 15:40:30 -07:00
Waleed 41fb86355d fix(webhooks): validate and pin EmailBison apiBaseUrl before outbound 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.
2026-07-04 15:37:09 -07:00
Waleed a1fbb5743d fix(stt): bound audio download response size (#5412)
* 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
2026-07-04 15:36:55 -07:00
Waleed 1a371e51e9 fix(uploads): bound multipart body read in workspace-file and knowledge-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
2026-07-04 15:32:00 -07:00
Waleed b98c7c4136 fix(tools): bound download response size for drive/slack/onedrive (#5414)
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.
2026-07-04 15:31:21 -07:00
Waleed 04432b64a8 fix(copilot): validate credential-link URL scheme before rendering (#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.
2026-07-04 15:19:08 -07:00
Theodore Li acf4afb1b5 fix(mothership): block subflow re-parenting in embedded workflow view (#5418) 2026-07-04 18:18:44 -04:00
Theodore Li 82eff5435d feat(custom-block): deploy a workflow as a reusable org-scoped block (#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
2026-07-04 15:57:24 -04:00
Theodore Li 818fa00133 fix(mothership): stop chat perf decay from permanently-animated streamed 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
2026-07-04 15:52:28 -04:00
Waleed 0249516cab fix(compare): swap LangChain logo, fix comparison-table horizontal scroll (#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.
2026-07-03 20:58:13 -07:00
Waleed 759dddbf4c feat(billing): dedicated Credit usage page with date-range filter and CSV export (#5405)
* fix(billing): apportion per-row credit costs so they sum to the page total

Cursor Bugbot (medium): each row rounded its own dollar cost to
credits independently while the header total rounded the summed
dollars once — over enough rows those two roundings can visibly
disagree, the exact "line items don't add up to the total" class of
bug apportionCredits was already built to prevent (used by the trace
view / cost breakdown). Route now apportions each page's row credits
against that page's dollar sum instead of rounding rows independently.

Added a test with three sub-cent rows that would each independently
round to 0 credits (but sum to 1) to prove the reconciliation holds.

* fix(billing): dim stale credit usage rows while a new period loads

Cursor Bugbot (medium): keepPreviousData kept the prior period's rows
and total on screen while a newly selected period fetched, but the
dropdown label updated immediately — so during the transition the
displayed numbers were labeled under a period they didn't belong to.
Now reads isPlaceholderData (the standard TanStack Query signal for
"this data is a stale placeholder, not a fresh fetch for the current
key") and dims the list while it's true, matching the same flag
already used for this exact purpose in integration-skills-section.tsx.

* fix(billing): show "<1 credit" for rows apportioned to 0

Cursor Bugbot (low): with apportioned per-row credits, a row with a
real but sub-credit dollarCost can legitimately apportion to 0 credits
once a sibling row absorbs the shared rounding remainder — rendering a
flat "0 credits" reads as if nothing was charged, inconsistent with
formatCreditCost's "<1 credit" wording used elsewhere in billing.

Added dollarCost to the wire response (needed to distinguish a
genuinely free row from a rounded-to-zero one) and a small
formatRowCredits helper that only changes the label, not the
underlying creditCost number, so the page-total reconciliation from
the prior fix is unaffected.

* fix(audit-logs): fix broken Custom range picker, trim time-range presets

Custom range silently did nothing: the time-range trigger was a
ChipSelect (Radix DropdownMenu, modal by default), and selecting
"Custom range" opened the Calendar popover in the same tick the modal
menu began its close/focus-lock cleanup, trapping the popover
non-interactive. Swapped to ChipCombobox (Radix Popover, non-modal),
mirroring the already-working pattern in the main Logs page exactly.

Also trimmed the preset list from 11 to 8 entries (dropped Past 30
minutes/12 hours/14 days) so the menu fits without scrolling.

* feat(billing): dedicated Credit usage page with date-range filter and CSV export

Follow-up to #5391 per team feedback in Slack: move the credit usage
list out of the inline Billing section into its own page, redesign
rows to show source ("Chat", "Workflow: <name>") instead of a raw
model description + badge, and add real date-range filtering and
export.

- Billing settings now shows a compact glance (30-day total + a "View
  usage logs" link) instead of the full inline list.
- New /settings/billing/credit-usage page (sibling of [section],
  mirrors the secrets/[credentialId] detail-route pattern) with day
  presets (Today/7d/30d/All time) plus a working Custom range picker
  — the same ChipCombobox+Popover+Calendar wiring the audit-logs fix
  in this branch uses, not the broken ChipSelect pattern.
- Rows show the humanized source label, or "Workflow: <name>" for
  workflow-sourced events (new server-side workflow-name lookup,
  batched per page). Dropped the redundant badge and raw model
  description.
- CSV export of the currently-filtered logs via a new GET
  .../usage-logs/export route (mode: 'text' contract, synchronous
  single-response CSV — the dataset is a bounded per-user ledger, not
  a workspace-wide export, so no async job queue needed). Query-filter
  logic (date-range resolution, workflow-name lookup) is shared with
  the list route via shared.ts rather than duplicated.
- period/startDate/endDate live in the URL via a co-located
  search-params.ts; the list query keeps keepPreviousData +
  isPlaceholderData dimming during filter transitions, matching the
  behavior already shipped in #5391.

Verified live end-to-end: back link navigation, custom range picker
opens and applies, day presets, CSV export downloads and matches the
on-screen rows exactly (credits reconcile with the total), compact
Billing summary + link.

* refactor(billing): move workflow-name enrichment into getUserUsageLogs, dedup helpers

/simplify pass over the credit-usage-page branch (4 parallel review
angles: reuse, simplification, efficiency, altitude):

- getUserUsageLogs now LEFT JOINs workflow and returns workflowName
  directly (matching lib/logs/list-logs.ts's established pattern),
  eliminating the route-layer resolveWorkflowNames query that both the
  list and export routes previously ran independently.
- Added includeSummary (default true) to getUserUsageLogs so the
  export route's cursor loop can skip the cursor-independent
  SUM/GROUP BY aggregate it never reads — that aggregate was being
  recomputed on every page of a paginated export for no reason.
- Fixed an off-by-one in the export's pagination loop: `<=
  MAX_EXPORT_ROWS` let it fetch one more full page past the cap only
  to discard it; `< MAX_EXPORT_ROWS` with a shrinking per-page limit
  never overshoots.
- Deduplicated the SOURCE_LABELS map (was defined identically in both
  the page and the export route) into a shared, DB-free
  source-labels.ts both can import.
- Export route now builds CSV rows via lib/table/export-format.ts's
  toCsvRow/formatCsvValue instead of a hand-rolled escaper.
- Added formatApportionedCreditCost to conversion.ts so the page's row
  rendering shares its zero/sub-credit wording with formatCreditCost
  instead of re-deriving the same three-way branch.
- Replaced the generic requireStartDateForCustomPeriod<Schema> contract
  helper (nontrivial generic bound for a single four-line refine used
  at two call sites) with a plain shared error-options object.
- Removed the credit-usage page's dateRangeAppliedRef guard — a
  controlled Radix Popover never re-invokes onOpenChange in response
  to the parent's own setState call, so the guard was defending
  against a re-entrant close that can't happen.
- Added a modal prop to ChipSelect (forwarded to the underlying
  DropdownMenu, which already supported it) so a future call site that
  hits the same "modal select traps a same-tick Popover" bug the
  audit-logs Custom range fix worked around has a real fix available
  instead of having to swap components again.

Re-verified live end-to-end after the refactor: workflow-name
resolution, credit reconciliation, and CSV export all still correct.

* fix(billing): drop Dollar cost from the CSV export, strip inline comments

We only surface credits to the user, not the underlying dollar figure
— "Dollar cost" was the one place the export literally displayed a
dollar amount (the rest of the codebase uses dollarCost purely as an
internal signal to distinguish a sub-credit charge from a genuinely
free event, never rendered as a "$" value).

* fix(billing): export honors partial custom date range, surfaces truncation

Greptile (P1) and Cursor Bugbot independently caught the same bug:
handleExport only forwarded startDate/endDate when BOTH were truthy,
but the list query and both API contracts treat endDate as optional
for a custom period (defaults to now). A user landing on a bookmarked
?period=custom&startDate=... URL would see populated rows and an
enabled Export button, then get a 400 on click since the export
omitted the required startDate too. Fixed by forwarding each date
independently, matching the list query's existing behavior.

Also addressed Greptile's other two findings:
- The export route now sets X-Export-Truncated so a 5,000-row-capped
  download is visible to the user (a toast), not just a server log.
  Reading that header meant switching the trigger from a plain anchor
  navigation to fetch+blob — an anchor can't inspect the response
  before the browser commits to the download.
- resolveDateRange now throws explicitly when a custom period is
  missing startDate instead of silencing the null check with `as
  string`, which would have produced a silent Invalid Date if ever
  called without prior contract validation.

* fix(billing): remove the export's arbitrary row cap, fix a cursor pagination bug it exposed

A personal credit ledger doesn't have the same unbounded-growth problem
a workspace table does — capping the export at 5,000 rows just meant
long-tenured or high-usage accounts (exactly the ones most likely to
need a full export to reconcile a billing question) got silently
truncated. Replaced the cap with a 50,000-row circuit breaker that
should never fire in normal use (logged as an error, not a warning,
if it ever does) and bumped the page size from 500 to 1,000 to cut
round trips.

Removing the cap surfaced a real, pre-existing bug in
getUserUsageLogs's cursor pagination: a raw `sql` template embedded a
JS Date object directly as a bound parameter, which the postgres
driver can't serialize (unlike drizzle's typed gte/lte operators,
which already handle Date correctly elsewhere in the same function).
It only ever manifested past the first page, which nothing before
this export route's tight multi-page loop reliably exercised.
Replaced the raw sql template with drizzle's typed lt/eq/or/and
operators, matching the pattern already proven correct in this file.

Verified live: seeded 6,000 rows (past the old cap) and confirmed the
export downloads all of them in one request with credits reconciling
exactly against the total.

* perf(billing): skip the redundant cursor lookup when the caller already has it

The export loop holds the previous page's rows in memory, so its next
cursor's createdAt is already known — getUserUsageLogs was still
re-resolving it via an extra DB round trip every page regardless.
Added an optional cursorCreatedAt to skip that lookup when provided;
the list route's existing callers are unaffected since they don't
pass it. Verified live: zero cursor-lookup queries fired across a
3,500-row / 4-page export that previously issued one per page.

* fix(billing): apportion credits over the whole filtered set, not per page/call

Cursor Bugbot caught this: the list route apportioned each page's
rows against only that page's own dollar total, while the export
apportioned every exported row against the complete set's total.
Since apportionment depends on the full set, the same log could show
a different creditCost between the list and the export, or even
between two pages of the same "Load more" list — and the sum of every
loaded row could visibly drift from the "Total" header shown above
them once more than one page had loaded.

Extracted getUsageCreditsByLogId — a single, shared, whole-filter
apportionment lookup both routes now call instead of each computing
their own subset locally. The list route calls it once per page
request (same cost profile as the summary aggregate it already pays
for every page); the export calls it once before its pagination loop,
not per page, keeping the round-trip count this session's earlier fix
already reduced. Also extracted the condition-building shared by the
main query, the summary aggregate, and this new lookup into one
buildUsageLogConditions helper, removing a third copy of that logic.

Verified live: summed every row across 4 "Load more" pages and
confirmed it now matches the reported total exactly (previously could
drift), and confirmed the list and the export produce byte-identical
credit sequences for the same rows.

* fix(billing): make custom-range startDate/endDate nullable, not '' defaulted

startDate/endDate had no sensible static default (they're only ever
meaningful mid-custom-range), so defaulting them to '' via
.withDefault('') meant switching back to a preset left the URL
carrying startDate=&endDate= instead of dropping the params entirely.
Made them nullable (no .withDefault) instead, matching the identical
fields in the main Logs page's own search-params.ts. Verified live —
switching from a custom range back to a preset now clears both params
from the URL completely.

* feat(audit-logs): add CSV export, matching the Credit usage page pattern

Adds an Export chip to the top-right of the Audit Logs page (via
SettingsPanel's actions slot — the same header mechanism the Credit
usage page uses), downloading every audit log matching the current
search/type/date filters as CSV.

- New GET /api/audit-logs/export route: same session + enterprise
  admin/owner gating as the existing list route, reuses the shared
  buildFilterConditions/buildOrgScopeCondition/queryAuditLogs helpers
  (already using drizzle's typed operators for cursor pagination, not
  the raw-sql-with-embedded-Date pattern fixed elsewhere this
  session), and the same fetch+blob+X-Export-Truncated pattern the
  Credit usage export already established.
- Capped at 10,000 rows (not the 50,000 used for a personal credit
  ledger) — an org's audit trail can genuinely grow much larger than
  one user's usage history, so this is sized for "a reasonable audit
  review window," with truncation surfaced via a toast rather than
  silently dropped.
- Bumped the API-validation-contract audit's route-count baseline for
  the new route.

Verified live against a real enterprise org: switched to "All time,"
exported ~750 real audit log rows, confirmed formatting (quoted
descriptions, actor email fallback) and correct filter scoping.

* fix(billing): skip wasted credit apportionment on the summary fetch, block export during stale data

Cursor Bugbot caught two real issues:

1. The compact Billing summary glance (limit=1) only ever reads
   summary.totalCredits, but the list route unconditionally ran
   getUsageCreditsByLogId's whole-filter scan on every call including
   this one — pure wasted work for a caller that discards the result.
   Added an includeCredits query flag (default true, using the shared
   booleanQueryFlagSchema) so useUsageSummary can opt out; the main
   paginated view keeps it on since it genuinely needs per-row values.

2. Export stayed enabled while useUsageLogs held stale rows via
   keepPreviousData mid-filter-transition — a user could change the
   period/range and click Export before the new data loaded, exporting
   against the new filter while the table still showed the old one.
   Export is now also disabled while isPlaceholderData is true.

* fix(billing): deterministic apportionment order, block audit export during stale data

Cursor Bugbot caught two more real issues on the latest push:

1. Same stale-export bug as the earlier Credit usage fix, this time in
   Audit Logs: Export stayed enabled while useAuditLogs held prior
   rows via keepPreviousData, so it could export against a
   just-changed filter while the table still showed the old one. Now
   also disabled while isPlaceholderData is true.

2. getUsageCreditsByLogId had no ORDER BY before apportionCredits's
   largest-remainder tie-break, so which row absorbed a tied
   remainder credit depended on undefined Postgres row order — the
   same event's displayed credit could flip between calls (list vs.
   export, or even two successive requests). Added the same
   `orderBy(desc(createdAt), desc(id))` the main list query already
   uses, making the tie-break reproducible.

Verified live: 3 identically-costed rows produced the same tie-break
winner across 3 repeated requests (previously order-dependent).

* fix(billing): distinguish a failed summary fetch from zero usage

The compact Billing glance only branched on isPending, so once
useUsageSummary settled into an error state, totalCredits stayed
undefined and formatCreditsLabel(0) rendered "0 credits" — visually
identical to genuinely having no usage this period. Now shows the
same neutral "—" placeholder for isError as it already does for
isPending.

* fix(billing): gate the credit-usage page server-side for enterprise accounts

Greptile (P1) caught this: hiding the "View usage logs" link on the
Billing page for enterprise accounts doesn't stop direct navigation —
anyone with the URL (bookmark, shared link, browser history) could
still reach the full page and its CSV export, which enterprise
accounts were never supposed to see at all (billing is managed
out-of-band for them).

Added a server-side check in page.tsx before anything renders:
resolve the session, look up the highest-priority subscription, and
redirect to /settings/billing if it's enterprise — matching how
getHighestPrioritySubscription is already used elsewhere for
server-side plan checks, rather than relying on a client-side-only
conditional the way the Billing page's inline section does.

Also fixes loading.tsx: it was a Server Component (no directive)
passing a raw icon function reference into the client Chip component,
which fails RSC serialization. Added 'use client'.

Verified live in a real browser against both an enterprise account
(redirects to Billing before any credit-usage content renders) and a
non-enterprise account (reaches the page normally).
2026-07-03 19:37:17 -07:00
Theodore Li d5082013a2 fix(ui): surface silent validation and rejection errors across editors and modals (#5394)
* fix(ui): surface silent validation and rejection errors across editors and modals

* improvement(knowledge): toast chunk validation errors instead of inline footer text

* fix(tables): reject invalid date/number in expanded cell editor, dedupe invalid-input toasts

* fix(knowledge): toast every failed chunk validation attempt, replacing the previous toast

* fix(knowledge): dismiss stale validation toast once content validates

* revert(ui): drop chat identifier error rendering and profile-name toast
2026-07-03 19:13:09 -04:00
Waleed 88330b43bf fix(uploads): gate execution-context uploads behind write/admin permission (#5404)
Fallback multipart upload route (/api/files/upload) had no workspace
permission check for execution-context uploads, unlike the primary
presigned-upload route which requires write/admin. Mirror that gate
so both paths enforce the same access control.
2026-07-03 16:12:03 -07:00
Waleed afe3d32032 fix(mcp): pass SSRF-guarded fetch into OAuth callback token exchange (#5399)
Mirrors the same wiring already used by probe.ts and revoke.ts, so the
callback's token-exchange request goes through the same guarded fetch as
the rest of the OAuth flow.
2026-07-03 15:33:59 -07:00
Waleed 1574c20806 fix(mcp): pass SSRF-guarded fetch into OAuth start flow, matching probe/revoke (#5398)
Discovery and registration during the MCP OAuth start flow were using the
default global fetch. probe.ts and revoke.ts already route these calls
through createSsrfGuardedMcpFetch(); this brings the start route in line
with the same pattern.
2026-07-03 15:31:25 -07:00
Waleed ff8844c7aa fix(guardrails): authorize vertexCredential before use in hallucination validation (#5400)
* fix(guardrails): authorize vertexCredential before use in hallucination validation

The guardrails validate route now checks credential access via
authorizeCredentialUse before passing a caller-supplied vertexCredential
into hallucination validation, matching the existing pattern already used
in the providers route for the same field.

* fix(guardrails): gate vertexCredential authorization on the resolved provider

Only run the credential check when the model actually resolves to vertex,
matching the providers route's gating exactly, and drop a redundant
fallback now that workflowId is already guaranteed present at that point.
2026-07-03 15:20:44 -07:00
Waleed 6bc70cb149 fix(tables): verify workflow belongs to table's workspace before binding (#5397)
* fix(tables): verify workflow belongs to table's workspace before binding

Add a check that a table workflow group's workflowId resolves to an
active workflow in the table's own workspace, in both the create and
update handlers, before it is persisted.

* fix(tables): reorder JSDoc for mapWorkflowGroupError

Greptile flagged the JSDoc for mapWorkflowGroupError as orphaned after
validateWorkflowInWorkspace was inserted between the comment and the
function it documented. Move the comment back above its function.
2026-07-03 14:38:36 -07:00
Waleed fd98218e58 fix(gmail): strip CR/LF from header values before MIME assembly (#5395)
* fix(gmail): strip CR/LF from header values before MIME assembly

Adds sanitizeHeaderValue and applies it to to/cc/bcc/subject/
inReplyTo/references and the attachment filename in
buildSimpleEmailMessage and buildMimeMessage before they're placed
into MIME header lines.

* fix(gmail): sanitize attachment mimeType in Content-Type header

attachment.mimeType was written verbatim into the Content-Type header,
unlike the other header fields this PR sanitizes. Route it through the
same sanitizeHeaderValue helper for consistency.
2026-07-03 14:25:12 -07:00
Waleed 2e3574407b fix(chat): fail-safe to noindex if the deployment lookup errors (#5396)
generateMetadata queried the DB with no error handling, unlike the
identical query in api/chat/[identifier]/route.ts (which already wraps
it in try/catch). There's no error.tsx under app/(interfaces)/chat/ —
metadata resolution errors aren't caught by route-segment error
boundaries at all, only the root global-error.tsx — so a DB hiccup
during this lookup would take the whole page down to a generic error
page instead of just failing to determine indexability. Catches the
error, logs it, and defaults to noindex: if we can't confirm the
deployment is safely public, that's the correct SEO default anyway,
not a reason to crash the request.

Flagged by Cursor Bugbot on #5388 (already merged); this ships the fix
as a standalone follow-up since the underlying code is already live.
2026-07-03 14:23:46 -07:00
Waleed ca2a52ccf7 feat(billing): expose credit usage log in Billing settings (#5391)
* feat(billing): expose credit usage log in Billing settings

Add a "Credit usage" section under Billing, below Invoices, showing a
paginated, period-filterable list of individual credit-consuming
events (model, tool, and fixed charges) for every plan except
Enterprise. Wires the existing (previously unused) usage-logs backend
to a proper contract, React Query hook, and emcn-styled UI:

- getUsageLogsContract in contracts/user.ts, broadened the source enum
  to match the real usage_log schema
- Rewrote the route to use parseRequest per the API boundary rules
- useUsageLogs infinite-query hook, keyset-paginated
- CreditUsageSection: period dropdown, total-credits summary, row
  list with source badges, "Load more"
- Extracted formatCreditsLabel in conversion.ts as the shared
  formatter for already-converted integer credits

* fix(billing): move usage-log key factory out of the 'use client' boundary

Greptile P2: usageLogKeys lived in the 'use client' usage-logs.ts hook
file — importing it from a server component (e.g. a future prefetch)
would resolve to a client-reference stub and crash at build/SSR, the
same class of bug that hit tables' key factory before. Extracted to
hooks/queries/utils/usage-log-keys.ts, matching table-keys.ts and
folder-keys.ts.

Also renamed a shadowed `source` map param in the route to `sourceKey`
for clarity.

* chore(billing): dedupe the usage-log period literal type

The '1d' | '7d' | '30d' | 'all' union was hand-typed in three places
across usage-logs.ts and credit-usage-section.tsx. Extracted
usageLogPeriodSchema in the contract and derived UsageLogPeriod from
it, so the hook, the component, and the key factory all share one
definition instead of risking drift.

* improvement(billing): move credit usage period filter into the URL

/cleanup pass (nuqs rule, react-query-best-practices, emcn review):
- period was a plain useState, but it's exactly the kind of shareable
  list filter sim-url-state.md calls out for nuqs — migrated to
  billingParsers/useQueryStates so the selection deep-links, survives
  reload, and matches every sibling settings section (recently-deleted,
  inbox, teammates). Derives its literal values from
  usageLogPeriodSchema instead of a fourth copy of the same union.
- Removed an unjustified showSelectedCheck={false} on the period
  ChipDropdown — the one other usage in the codebase is a one-shot
  action menu with no persistent selection; this is a real filter and
  should show the check like the default intends.
- Added placeholderData: keepPreviousData to useUsageLogs — period is
  a variable query key, so without it switching periods flashed the
  loading empty-state instead of smoothly transitioning.

Verified live: deep-link with ?period=7d pre-selects and loads
correctly, switching periods updates the URL, and the selection
survives a hard reload.
2026-07-03 13:20:59 -07:00
Waleed 4fe372b583 improvement(seo): extract shared withFilteredNoindex helper (#5392)
The `{ ...base, ...(isFiltered && { robots: { index: false, follow: true } }) }`
faceted-navigation noindex pattern was duplicated verbatim across five
catalog pages (integrations, models, blog, careers, pricing). Extracted to
lib/landing/seo.ts alongside buildLandingMetadata, giving the pattern a
name and a single point of change. Pure refactor — verified byte-identical
rendered output (robots meta + canonical) on baseline and filtered variants
of all five pages before and after.
2026-07-03 13:18:08 -07:00
Waleed 8937005076 improvement(compare): tighten comparison-page copy, cut self-referential hedging (#5390)
* improvement(compare): tighten comparison-page copy, cut self-referential hedging

Across sim.ts and all 20 competitor profiles, remove phrasing that describes
a claim's own provenance/reliability (e.g. 'marketing claim', 'marketing
copy states', 'as of this check') in favor of stating the fact directly.
Also trims overlong run-on sentences into shorter, more direct ones.

No numbers, dates, comparisons, confidence levels, or sources changed.

* fix(compare): avoid accidental boolean-icon misparse from copy tightening

Three facts whose tightened wording started with a bare 'No ' now matched
the Yes/No boolean-icon convention (parseFactValue), collapsing a nuanced
'Partial' or plain descriptive sentence into a bare X icon. Rephrase with
'Not'/'not' so these render as full text again, matching pre-edit behavior.
2026-07-03 12:54:28 -07:00
Waleed 03e0e075e1 improvement(styling): consistent branded text-selection color app-wide (#5389)
* improvement(styling): use a consistent branded text-selection color app-wide

Previously no ::selection rule was defined anywhere, so text selection fell
back to the browser/OS default, which dims when a window loses focus and
made pages look inconsistent side by side. Adds one global rule using the
existing --selection token.

* fix(styling): resolve markdown-selection conflict, use --white token

Remove the local link/strikethrough ::selection color overrides in
rich-markdown-editor.css now that the global ::selection rule already
forces uniform white text on every selection. Also swap the literal
#ffffff for the existing --white design token.
2026-07-03 12:50:40 -07:00
Waleed 7ff4f17eb2 fix(seo): fix GSC indexing issues, remove unused academy/partners pages (#5388)
* fix(seo): fix GSC indexing issues, remove unused academy/partners pages

- robots.ts: unblock /chat/ (page-level noindex now gates gated/inactive
  deployments instead), drop the now-vestigial blog-tag/link-preview carve-out
- next.config.ts: add missing redirects for renamed integration slugs
  (sap-s-4hana, calcom), removed /partners, and removed /academy
- fix missing canonical/noindex on filtered catalog pages (integrations,
  models, blog, careers, pricing) causing GSC "duplicate, Google chose
  different canonical"
- standardize page titles to "Page | Sim, the AI Workspace" across the board
- remove the academy marketing pages and partner program page (content
  consolidated into docs.sim.ai/academy); drop the unused academy_certificate
  table via migration and strip the sandbox-mode plumbing from the workflow
  editor that only academy ever used

* fix(migrations): defer academy_certificate table drop to a follow-up PR

CI's expand/contract migration safety check correctly flagged this: the
academy_certificate DROP TABLE was bundled in the same PR as removing the
code that reads/writes it (the certificates API route had no feature-flag
guard of its own, so it was reachable independent of the marketing pages
being disabled). Dropping the table in the same deploy risks breaking any
pod still running the old code during a rolling deploy.

Restores the table/enum in schema.ts and the test mock, and removes the
0254 migration. The table drop should ship in its own PR once this one's
code removal is confirmed live.

* fix(seo): drop dead revalidate exports on searchParams-driven pages

Any Server Component in the route tree reading searchParams forces the
whole route to fully dynamic per-request rendering, which overrides ISR —
revalidate is a silent no-op once that happens. True on pricing/careers
because generateMetadata now parses searchParams directly, and was already
true on blog before this PR (its page body already read searchParams).
Flagged by Greptile on pricing; same root cause applies to all three.
2026-07-03 12:15:54 -07:00
Waleed e1b8200660 fix(sso): support skipping the OIDC UserInfo endpoint at registration (#5386)
* fix(sso): support skipping the OIDC UserInfo endpoint at registration

* fix(sso): cap OIDC discovery fetch at 10s to avoid stalling registration

* test(sso): default-mock discovery fetch so intent is explicit

* fix(sso): prefer client_secret_post and surface discovery failure reasons

* fix(sso): always resolve token auth method and skip SSRF-checking a discarded userInfoEndpoint
2026-07-03 12:09:22 -07:00
Waleed 331875b7fe fix(billing): stop showing "View all" when there are no more invoices (#5387)
* fix(billing): stop showing "View all" when there are no more invoices

Show fewer invoices (10) and derive hasMore from the finalized-invoice
count instead of Stripe's raw has_more, which counted drafts we filter
out client-side and could report more invoices than actually exist.

* chore(billing): move invoice pagination explanation into TSDoc

Extract collectFinalizedInvoices with a TSDoc comment explaining the
Stripe has_more/draft-filtering rationale, instead of an inline
comment block.

* fix(billing): don't hide View all when the page-cap is hit inconclusively

Greptile P1: if MAX_STRIPE_PAGES is exhausted while the finalized count
sits exactly at MAX_INVOICES and Stripe still has_more, hasMore was
silently returned as false. collectFinalizedInvoices now also returns
whether Stripe's cursor was still open at exit, and the route ORs that
into hasMore so the safety cap can never suppress "View all".

Also asserts starting_after cursor propagation across pages and adds a
test for the safety-cap-hit case.
2026-07-03 11:49:14 -07:00