Commit Graph
4936 Commits
Author SHA1 Message Date
Theodore Li 67e02fab3a fix(execute): block cross-origin session-authenticated workflow runs (#5062)
* fix(execute): block cross-origin session-authenticated workflow runs

* fix(execute): scope session origin guard to provable cross-origin

Address review on #5062:
- Reject session-cookie execution only when provably cross-origin (Sec-Fetch-Site
  cross-site/same-site/none, or a mismatched Origin) instead of failing closed on
  absent headers. Fixes route tests that 403'd on header-less session requests, and
  reflects that this is CSRF protection, not anti-cookie-replay.
- Drop same-site from the trusted set: only same-origin is our front-end.
- Guard the Origin fallback in try/catch so a getBaseUrl() throw can't escape.
- Add a route-level cross-origin rejection test.
2026-06-15 15:39:43 -04:00
Theodore Li 18edc94b2c fix(billing): deploy modal gates on workspace entitlement, not viewer plan (#5055)
* fix(billing): deploy modal gates on workspace entitlement, not viewer plan

The deploy modal showed the upgrade wall to a free user in a PAID workspace,
because it gated on the viewer's individual plan (useSubscriptionData) while the
server gates on the workspace billed account (rolled-up plan). Add a workspace
api-execution-entitlement endpoint that mirrors isWorkspaceApiExecutionEntitled,
and gate the API/MCP/A2A tabs on it so the UI matches the server exactly.

* fix(billing): key deploy gate on URL workspaceId + refetch entitlement on open

Address review findings:
- key useWorkspaceApiExecutionEntitlement on the URL workspaceId (available on
  mount) instead of workflowWorkspaceId (null until the workflow map resolves),
  so the gate fires immediately instead of leaving the tabs ungated until then
- staleTime 0 so reopening the deploy modal refetches entitlement; a plan upgrade
  happens outside this query's invalidation graph, so the gate self-heals on open

* refactor(billing): workspace owner access state instead of bespoke entitlement endpoint

Replace the single-purpose api-execution-entitlement endpoint with a reusable
workspace-owner billing/access concept — the workspace-scoped counterpart to the
viewer-scoped useSubscriptionData:

- getWorkspaceOwnerSubscriptionAccess(workspaceId): the billed account's rolled-up
  subscription access fields (mirrors getSimplifiedBillingSummary's flag derivation)
- GET /api/workspaces/[id]/owner-billing + useWorkspaceOwnerBilling hook
- deploy modal derives its gate via the existing getSubscriptionAccessState
  (hasUsablePaidAccess) on the owner data, exactly like every other paid feature

Audited the rest of the app: no other UI gates on the viewer's plan where the
server gates on the workspace owner — programmatic execution is the only
workspace-owner-scoped feature; inbox/KB-live-sync/credential-sets all gate
consistently on both sides.

* fix(billing): deploy gate on owner isPaid, not hasUsablePaidAccess

hasUsablePaidAccess rejects past_due and billing-blocked, but the server gate
(isWorkspaceApiExecutionEntitled) allows any paid plan in an entitled status
(active or past_due). Gate on the owner's isPaid so a past_due paid workspace
isn't shown the upgrade wall while the API still works.
2026-06-15 15:36:56 -04:00
Waleed 0673e3c0f7 refactor(sim): consolidate record guards + pure utils into @sim/utils (#5061)
* feat(utils): add record guards and pure helpers to @sim/utils

Add isRecordLike (loose, non-prototype-checked record guard) and
sortObjectKeysDeep; relocate isPlainRecord (strict) and normalizeEmail
into @sim/utils so they are reusable across apps and packages. Unit tests
cover the loose-vs-strict distinction, deep key sorting, and email
normalization.

* refactor(sim): consolidate record guards and normalize helpers onto @sim/utils

Replace ~55 re-implemented loose record guards with the canonical
@sim/utils isRecordLike (and one strict site with isPlainRecord), and
dedupe three normalize clusters: sortObjectKeysDeep (sanitization +
copilot builders), normalizeToken (salesforce + servicenow triggers),
and normalizeEmail. Array-allowing guards and domain-specific normalizers
are intentionally left untouched. Pure refactor — identical predicates
and transforms, no behavior change.
2026-06-15 12:32:19 -07:00
Siddharth Ganesan cefb2dc239 improvement(mship): add enrichment tool, clean up dead tools (#5058)
* feat(mothership): add enrichment_run server tool for one-off lookups

Implement the Sim-side handler for the copilot enrichment_run tool: runs the
enrichment provider cascade for a single entity and returns the result inline,
surfacing the hosted-key cost as _serviceCost for per-round billing (matching
the media tools). Registered in the server-tool router.

Regenerate the copilot tool catalog/schemas to include enrichment_run.

* fix(mothership): remove leftover touch_plan tool references

touch_plan was removed from the copilot tool catalog earlier, but the Sim side
still referenced it. Regenerating the generated tool catalog (for enrichment_run)
synced it to the current contract and dropped the stale TouchPlan export, which
broke the build where router.ts still imported it.

Remove the dead touch_plan server tool and its test, drop its router
registration and WRITE_ACTIONS entry, simplify getServerToolRegistry (no more
beta-gated server tools), and clean up stale "use touch_plan" guidance strings.

* chore(mothership): trigger dev redeploy

* improvement(mothership): log billed cost on enrichment_run lookups

* fix(contracts): regenerate mship contracts

* fix(contracts): fix mship contracts
2026-06-15 10:57:29 -07:00
Siddharth Ganesan 17500432ef Revert "improvement(mship): clean up dead tools, add enrichments (#5056)" (#5057)
This reverts commit 6f4189f015.
2026-06-15 10:43:31 -07:00
Siddharth Ganesan 6f4189f015 improvement(mship): clean up dead tools, add enrichments (#5056)
* feat(mothership): add enrichment_run server tool for one-off lookups

Implement the Sim-side handler for the copilot enrichment_run tool: runs the
enrichment provider cascade for a single entity and returns the result inline,
surfacing the hosted-key cost as _serviceCost for per-round billing (matching
the media tools). Registered in the server-tool router.

Regenerate the copilot tool catalog/schemas to include enrichment_run.

* fix(mothership): remove leftover touch_plan tool references

touch_plan was removed from the copilot tool catalog earlier, but the Sim side
still referenced it. Regenerating the generated tool catalog (for enrichment_run)
synced it to the current contract and dropped the stale TouchPlan export, which
broke the build where router.ts still imported it.

Remove the dead touch_plan server tool and its test, drop its router
registration and WRITE_ACTIONS entry, simplify getServerToolRegistry (no more
beta-gated server tools), and clean up stale "use touch_plan" guidance strings.

* chore(mothership): trigger dev redeploy

* improvement(mothership): log billed cost on enrichment_run lookups
2026-06-15 10:41:33 -07:00
Emir Karabegandwaleed 1c8ac05808 improvement(scheduled-tasks): move recurrence into modal body as a section (#5054)
* improvement(scheduled-tasks): move recurrence into modal body as a section

Replace the footer RecurrenceControl (a row of chip dropdowns) with a
RecurrenceSection rendered between the prompt body and footer: a "Recurring"
Switch toggles one-time vs repeat, and — once on — frequency and end (never,
on a date, after N runs) are labeled ChipModalField rows aligned to the modal
header/footer gutter.

Toggling Recurring off now preserves the recurrence shape (cadence, end, and a
passed-through custom cron) and only sets frequency: 'once', so toggling back
on restores a conversationally-authored custom schedule instead of silently
rewriting it to daily.

Also restore the prompt editor's native scale (text-[15px], -0.015em tracking)
so the editor reads the same in the chat input and the task modal body.

* fix(scheduled-tasks): clear custom cron when switching frequency away from custom

The Recurring toggle restores `frequency: 'custom'` when `recurrence.cron` is
truthy, but switching the frequency dropdown away from custom kept the stale
cron on the object — so editing a custom-cron task to Daily, then toggling
Recurring off and back on, snapped it back to Custom and persisted the old cron.
Clear `cron` in the non-custom frequency branches so it is present only while
the cadence is genuinely custom (matching the type's "custom only" invariant),
making the toggle's restore signal accurate.

Also document the unreachable `once` branch in frequencyOptionFor as a
type-exhaustiveness fallback (keeps the return type without a cast).

* fix(scheduled-tasks): restore the prior cadence when re-enabling recurrence

Toggling Recurring off collapsed frequency to 'once' but toggling back on
forced 'daily' and cleared weekdays, so pausing a weekly/weekdays/monthly task
and re-enabling it silently reset it to daily. Cache the last recurring cadence
in a ref (written during render) and reinstate it on toggle-on, so a paused
"Weekly on Mon" returns as weekly. This also subsumes the custom-cron restore —
the ref remembers 'custom' across the one-time interval — so the toggle no
longer special-cases cron.

* improvement(scheduled-tasks): compose canonical modal separator, tidy imports

Replace the recurrence section's hand-rolled `h-px bg-[var(--border)]`
divider with the canonical ChipModalSeparator (now exported from the
chip-modal barrel) so the modal's hairline has a single source of truth.
Also unify loading.tsx icon imports onto the @/components/emcn barrel.

---------

Co-authored-by: waleed <walif6@gmail.com>
2026-06-15 10:25:40 -07:00
Waleed 940506ad09 feat(square): add Square integration with 34 commerce operations (#5053)
* feat(square): add Square integration with 34 commerce operations

Add a Square integration (API-key auth via personal access token) covering
payments, refunds, customers, locations, orders, invoices, catalog, and
inventory. Catalog image upload routes through an internal API endpoint using
the shared UserFile handling pattern. Adds a dedicated square-errors extractor.

* fix(square): correct catalog image part name and address review feedback

- Fix catalog image upload: Square's multipart part for the binary is `file`,
  not `image_file` (per the live API cURL examples); this would have caused
  upload failures
- Catalog image route: check response.ok before parsing, drop the unreachable
  legacy base64 path, derive MIME from the uploaded file
- Block: split the search query field per operation so placeholders match each
  endpoint's schema; parse each JSON field individually so errors name the field
- Round out coverage: complete_payment version_token; customer nickname/birthday;
  batch inventory states/updated_after/limit

* fix(square): correct canonical file param usage and revert query split

- Read the catalog image file from the canonical `params.file` (the basic/advanced
  inputs are collapsed before the params function runs) instead of the raw
  uploadFile/fileRef ids, which no longer exist at that point — fixes the
  Canonical Param Validation test and a latent upload bug
- Revert the per-operation query split: canonicalParamId is only valid for
  basic/advanced pairs under one condition. Use a single query field with a
  schema-neutral placeholder and a wand prompt that covers each search operation

* chore(square): trigger fresh review

* fix(square): single-location invoice search and guard numeric coercion

- SearchInvoices: Square's invoice filter accepts only one location, so take a
  single locationId (string) instead of an array and wrap it as
  query.filter.location_ids: [locationId]
- Block: fail locally with a clear "<field> must be a valid number" error when
  amount/limit/version/orderVersion are non-numeric instead of forwarding NaN

* fix(square): accept real booleans for autocomplete/includeRelatedObjects

Coerce these from both the dropdown's string values and actual booleans
(which can arrive via connected blocks or templated inputs), so true is not
silently flipped to false.

* fix(square): validate parsed JSON field shapes (array vs object)

parseJsonField now enforces the expected shape so a valid-but-wrong-type value
(e.g. a JSON string where an array is expected for locationIds/objectTypes/
paymentIds/catalogObjectIds/states, or a non-object for order/invoice/etc.)
fails locally with a clear message instead of a confusing Square API error.
2026-06-15 09:55:14 -07:00
Waleed 06191a7fb9 refactor(providers,executor): deepen three shallow modules (#5052)
* refactor(executor): collapse subflow node-ID logic behind a codec

Extract the ~20 scattered subflow node-ID parsing/building helpers and
their regex patterns into a single SubflowNodeIdCodec. All patterns now
live in one place; subflow-utils and execution state delegate to it.
Pure refactor — byte-identical output, ordering, and error modes.

* refactor(providers): extract StreamingExecution assembly into a factory

The near-identical streaming-response assembly (timing segments, cost,
token counts, onComplete wiring, success/logs/metadata envelope) that was
copy-pasted across ~16 providers now goes through createStreamingExecution.
Each provider injects only its stream iterable and delta extractors. Gemini
is intentionally left as-is (its assembly is structurally divergent).
Pure refactor — identical StreamingExecution shape, cost, tokens, timing,
and callback order per provider.

* refactor(providers): dedupe tool-schema wrapping into adapters

Replace the identical inline tool-schema literals repeated across ~16
providers with shared adaptOpenAIChatToolSchema / adaptAnthropicToolSchema
helpers. Content building reverts to calling the attachments builders
directly (the per-provider content seam was pure pass-through indirection).
Pure refactor — byte-identical tool-schema and content output.

* chore(skills): point add-model provider edits at shared response/tool-schema helpers

When editing provider code, reuse createStreamingExecution and the
tool-schema-adapter helpers instead of hand-rolling.

* refactor(providers): drop placeholder stream double-cast in streaming factory

Build the output object before creating the stream so the factory returns
a fully-typed StreamingExecution without 'undefined as unknown as
ReadableStream'. Keeps the strict API-validation boundary ratchet green
(double-cast count back to baseline). No behavior change: the same output
reference is mutated on drain.
2026-06-15 01:03:12 -07:00
Waleed ae075f871f Revert "fix(realtime): re-validate socket role and evict revoked collaborator…" (#5051)
This reverts commit 4ab8760ae0.
2026-06-14 22:20:12 -07:00
Waleed 4ab8760ae0 fix(realtime): re-validate socket role and evict revoked collaborators (#5050)
Socket.io authorized workflow access only at join and cached the workspace
role in presence, so a removed or downgraded collaborator kept live read/
write access until they disconnected.

- Re-validate the cached role against the permissions table on mutating
  events, bounded by a short TTL; refresh or evict on change
- Add /api/permissions-updated so the app reconciles active rooms, evicting
  revoked users (cross-pod) and refreshing downgraded roles
- Notify realtime on workspace member removal and permission changes
2026-06-14 21:19:32 -07:00
Waleed 57b58fd117 feat(context-dev): add Context.dev web + brand data integration (#5048)
* feat(context-dev): add Context.dev web + brand data integration

* feat(context-dev): add brand variants, products, fonts, styleguide, images, prefetch

Expands coverage to all relevant Context.dev endpoints (22 tools): brand by
name/email/ticker, simplified brand, transaction identifier, single + catalog
product extraction, fonts, styleguide, image discovery, and prefetch utilities.
Shared brand output schema and transform helper; verified against the live API.

* fix(context-dev): wire includeFrames, split crawl/extract maxPages, derive screenshot MIME

Addresses review feedback:
- includeFrames is now a block subblock + param for scrape_markdown/scrape_html
- crawl and extract use separate Max Pages fields (crawl 1-500, extract 1-50) so a
  crawl value can no longer be forwarded to extract beyond its limit
- screenshot file MIME type and extension are derived from the returned URL instead
  of being hardcoded to PNG
2026-06-14 20:35:20 -07:00
Waleed 3ada4a34af fix(chat): fail closed when embed gate cannot resolve workspace (#5046) 2026-06-14 18:53:05 -07:00
Waleed 7a33508af1 feat(scheduled-tasks): pause/resume, mutation toasts, submit guards, empty state (#5044)
* feat(scheduled-tasks): pause/resume, mutation toasts, submit guards, empty state

- Toasts on every job mutation (create/update/delete/exclude/pause/resume) so failures are never silent
- TaskModal stays open until the save persists; submit disabled in-flight to block double-submit
- Pause/Resume recurring tasks via the context menu; paused occurrences render dimmed so they stay resumable
- First-run empty state with a create CTA when the workspace has no tasks

* fix(scheduled-tasks): return edit-modal save promise so the modal awaits persistence

The edit TaskModal's onSubmit called updateTask without returning its promise,
so the await resolved immediately — the modal closed before the save finished,
failed edits didn't stay open, and a rejected mutateAsync went unhandled. Return
the promise so the await tracks the real mutation (matching the create path).

* refactor(scheduled-tasks): explicit return in edit-modal onSubmit for clarity

* fix(scheduled-tasks): optimistic create insert + always-reset submit guard

- Seed the created job into the workspace-list cache on success so the new task
  renders instantly and the first-run empty state never flashes between the
  success toast and the list refetch; onSettled still reconciles authoritatively.
- Reset the modal's submitting flag in a finally so the submit button can never
  stick disabled if the modal is kept open.

* revert(scheduled-tasks): drop first-run empty state

Remove the empty-state prompt and its supporting hasTasks flag; with the empty
state gone, also revert the create optimistic-insert (its only purpose was
avoiding the empty-state flash) back to plain toast + list invalidation.

* fix(scheduled-tasks): lock modal dismiss while a save is in flight

Cancel, the header X, Escape, and overlay click all route through one guarded
onOpenChange that no-ops while submitting, and the footer Cancel is disabled
(cancelDisabled) — so an in-progress create/edit can't be abandoned mid-save and
lose its draft. submitting moves up to TaskModal so the guard can read it.

* fix(settings): align general appearance dropdowns to a uniform width

Theme/Snap-to-grid (ChipSelect) hugged their content (~90px) while Timezone
(ChipCombobox) was pinned to 260px, so the three read as a ragged column. Give
all three a shared 240px trigger via fullWidth + a common width wrapper so they
align as one column; menus match their triggers. No behavioral change — timezone
keeps search, options and handlers are untouched.

* docs(scheduled-tasks): document why the dismiss guard doesn't block the success-close

* fix(scheduled-tasks): disable Delete while an edit save is in flight

Delete bypasses the modal's dismiss guard (it closes via closeTask, not
onOpenChange), so a click mid-save could run a delete against the same task as
the in-flight update. Disable it while submitting, matching Cancel.

* refactor(scheduled-tasks): TSDoc over inline comments in task modal

Move the Delete-lock rationale to a TSDoc block on secondaryActions, and
restructure handleSubmit to a boolean persist result so the failure path is
self-evident — removing the inline comments and the empty catch block.

* fix(scheduled-tasks): gate submit on a synchronous ref to fully block double-submit

The submitting state flag only reflects after a re-render, so two same-tick
invocations (Enter racing the click) could both pass a state-based guard and fire
two mutations. A submittingRef flips synchronously, so the second invocation is
rejected before it can submit again; the state still drives the button/cancel UI.
2026-06-14 11:01:28 -07:00
Waleed 6c56a21b94 improvement(settings): right-align timezone picker, order by popularity, drop tooltip (#5043)
* improvement(settings): right-align timezone picker, order by popularity, drop tooltip

* improvement(settings): show human-friendly timezone labels in picker

* improvement(settings): use (GMT±HH:MM) City timezone labels, offset-sorted

* improvement(settings): sort timezones alphabetically by city per UX research
2026-06-14 04:53:43 -07:00
Waleed 4ec26a0404 feat(scheduled-tasks): minute-granular calendar + user timezone preference (#5038)
* feat(scheduled-tasks): position week/day chips at their exact minute

Replace hour-bucketed event rendering with a per-day absolute overlay that
places each task chip at timeToOffset(start), so a 5:38 task sits at 5:38
instead of the top of the 5:00 cell. Hour cells become click/gridline-only;
the overlay is non-interactive so empty-space clicks still create. Removes the
now-obsolete eventsByHour/hourKey/bucketEventsByHour path (month view already
used eventsByDay).

* feat(settings): user timezone preference for scheduled tasks

Add a Timezone preference under Settings → General. Scheduled tasks now run
in the user's chosen IANA zone instead of whatever device created them.

- settings table gains a nullable `timezone` column (migration 0236); null
  means "use the browser-detected zone", so existing users are unchanged
- contract: validated IANA `timezone` on the settings get/update shapes
- useTimezone() resolves the saved zone or the browser fallback; the task
  modal captures it instead of recomputing the device zone
- General settings adds a searchable timezone combobox defaulting to the
  detected zone
- shared timezone util (getBrowserTimezone / getSupportedTimezones)

* fix(scheduled-tasks): interpret launch/end times in the account timezone

Address review: one-time runs and the end-of-day boundary were resolved in the
browser zone, so a task could fire at the wrong instant when the account zone
differed from the device. Resolve wall-clock launch/end through the account
zone (DST-correct), and evaluate the past-launch guard and the default seed in
that zone too — matching how the recurring cron is already evaluated.

- timezone util: zonedWallClockToUtc (DST-correct, no library), wallClockNow;
  getSupportedTimezones falls back to a common set and always includes UTC
- recurrenceToScheduleFields takes the timezone and resolves time/endsAt in it
- settings timezone Label drops its dangling htmlFor
- tests for the zone converter (UTC / +5:30 / DST) and the zoned mappings

* feat(scheduled-tasks): Google-Calendar-style side-by-side overlap layout

Tasks whose pills would collide now split the column into side-by-side lanes
(like Google Calendar) instead of stacking on top of each other; tasks that
don't overlap keep the full width. Adds a pure layoutColumn lane-assignment
helper (interval clustering + greedy lane packing) with tests.

* fix(scheduled-tasks): zone-consistent recurrence/edit + duplicate + loading

Address review (zone consistency):
- recurrenceToCron derives weekday/day-of-month from a UTC-parsed calendar date
  so the cron targets the right day regardless of device zone
- cronToRecurrence + editSeedFor recover the launch date/time and ends-on date
  read back in the schedule's zone (zonedWallClock), so editing shows the right
  values when the account zone differs from the device
- defaultLaunch no longer compares browser-local slot days against account-zone
  "today"

Features:
- right-click Duplicate: opens a pre-filled create modal from any task
  (TaskEditSeed now extends a shared TaskPrefill; modal gains a prefill prop)
- loading.tsx paints only the header chrome (the page is a calendar, not a
  table) so it no longer pops table -> calendar; the empty calendar loads tasks in
- task context menu drops "See details" (finished tasks open on click)

* fix(scheduled-tasks): edit/duplicate use the task's own timezone, not the account one

A task created in one zone but edited after the account zone changed (or
duplicated) seeded its launch in the task's stored zone while validating and
submitting in the current account zone, drifting unchanged run times. TaskPrefill
now carries the task's timezone; the modal seeds AND submits in it for
edit/duplicate, and only blank creates use the account zone.

* fix(scheduled-tasks): duplicating a past one-time task seeds a future launch

Audit follow-up: a duplicate of a one-time task whose launch already passed
opened with Schedule disabled. It now falls through to the next-hour default so
the new task is immediately schedulable. Also clarifies the DST spring-forward
note on zonedWallClockToUtc and drops a stray test comment.

* fix(scheduled-tasks): clear duplicate pre-fill when starting a fresh create

The create modal had two open-sources (isCreateOpen + duplicatePrefill) that
weren't coordinated. Starting a header/slot create now clears any duplicate
pre-fill (and duplicating closes any open create), so a stale duplicate draft
can never bleed into a blank or slot-seeded create.

* fix(scheduled-tasks): make create/duplicate/edit modals mutually exclusive

Opening any of the three modal flows (blank create, duplicate pre-fill, task
edit/record) now closes the other two, so the create modal can never co-exist
with the edit modal and no stale state survives a switch.

* feat(scheduled-tasks): render calendar in the effective timezone

Position each occurrence at its wall-clock time in the task's own
timezone, and draw the now-line / today highlight in the viewer's
effective zone, so the calendar always shows a task at the local time it
was scheduled for — matching the modal. Adds zonedClockDate as the single
zone boundary; the default case (account zone == browser zone) is
unchanged.

* fix(scheduled-tasks): re-sync calendar day frame when timezone resolves

When useTimezone() resolves from the browser fallback to the saved
account zone after mount, re-derive today (and the focused day, while it
is still on today) so the grid frame, now-line, and fetched range stay in
agreement. The focused day is preserved across the change once the user
has navigated away.

* fix(scheduled-tasks): pad view window for timezone slop; re-center scroll on zone change

visibleRange now expands the rendered span by a day on each side so an
occurrence whose own-zone display day is on screen is never filtered out
by the account-zone frame; bucketEventsByDay still places each on its
zoned day, dropping any off-screen. The week/day auto-scroll re-centers
when the effective timezone resolves.

* test(scheduled-tasks): pass timezone to cronToRecurrence ends-on case

The second cronToRecurrence call omitted the required timezone, so the
recovered end date depended on the test runner's system zone instead of
the schedule zone. Pin it to UTC for determinism.

* fix(scheduled-tasks): re-seed blank-create launch when timezone resolves

useTimezone() starts on the browser fallback, so a blank create's
next-top-of-the-hour default (and its past-launch guard) could be seeded
in the wrong zone and submitted in the resolved account zone. Re-seed the
default when the effective zone changes, unless the user has edited the
fields; slot/edit/duplicate seeds are zone-stable and untouched.

* fix(scheduled-tasks): DST fall-back resolve, today month-cell default, late-night pill bounds

Audit follow-ups:
- zonedWallClockToUtc resolves to the self-consistent instant, fixing
  one-time launches on the autumn fall-back day (were an hour early) while
  keeping the spring-forward gap rolling forward; adds DST regression tests.
- defaultLaunch: today's whole-day (month-cell) click defaults to the next
  top of the hour like the header action, not a past 9am that disables Save.
- DayEvents clips to the day bounds so a late-night pill never spills past
  the final hour row; now-line sits above event pills.
2026-06-13 20:49:18 -07:00
Waleed 32b380fd37 improvement(salesforce): align tools + block with Salesforce API and harden CRUD/analytics (#5040)
* improvement(salesforce): align tools + block with Salesforce API and harden CRUD/analytics

- Migrate all 4 Account tools to shared getInstanceUrl + extractErrorMessage helpers (drop ~40 lines of inlined idToken decode per file)
- Use extractErrorMessage consistently across every CRUD tool; add loggers to the opportunity tools
- Trim ID path params on all update/delete/single-get tools; URL-encode single-record field lists
- Fix dashboard tools: read name/metadata from dashboardMetadata.attributes (was always null); refresh now returns status/statusUrl defensively; drop no-op list_dashboards folderName filter
- Expose update_case origin/contact/account and update_task who/what fields end-to-end
- Block: mark create-required (Name, LastName, Company, StageName, Subject) and update/delete IDs conditionally required; add account billing*/revenue/employees and contact mailing*/department subBlocks; convert includeDetails to a Yes/No dropdown; move optional fields to advanced mode
- Trim over-declared dashboard output types; make Task.Status optional
- Regenerate integration docs

* fix(salesforce): correct list/refresh response shapes per live API docs

- list_dashboards: GET /analytics/dashboards returns a bare top-level array, not a {dashboards} wrapper (fixes always-empty result)
- refresh_dashboard: statusUrl is returned at the top level of the PUT response, read it there first
- list_reports: the list resource only returns report name/id/url/describeUrl/instancesUrl, so drop the no-op folderName filter and match searchTerm on name only

Validated tool-by-tool against the live Salesforce REST/Analytics/Object-Reference docs (API v67.0).

* fix(salesforce): address Greptile/Cursor review

- Add shared requireId() guard so whitespace-only IDs fail fast instead of producing malformed /sobjects/Object/ empty-path requests (all update/delete/single-get tools)
- URL-encode the fields query value in get_opportunities and get_tasks single-record GETs (matching the other get_* tools)
- Reflect the Salesforce API success flag consistently across all create tools (success/created use data.success === true)

* fix(salesforce): trim echoed output IDs and relation reference IDs

- update/delete tools now return output.id from the trimmed ID so chained workflows never receive whitespace-padded IDs
- trim relation reference IDs (AccountId, ContactId, WhoId, WhatId) in create/update bodies to avoid Salesforce reference errors from copy-pasted whitespace

* fix(salesforce): trim accountId in update_contact body

Last remaining untrimmed relation reference ID — update_contact now trims AccountId like the other create/update tools.
2026-06-13 20:21:45 -07:00
Vikhyath Mondreti 1fdb43fb44 improvement(perms): followup to org scoping of permission groups 2026-06-13 18:14:10 -07:00
Theodore Li 522ba8e86a feat(billing): gate programmatic workflow execution behind a paid plan (#5036)
* feat(billing): gate programmatic workflow execution behind a paid plan

Block free-plan accounts (hosted only) from running workflows programmatically:
API-key/public execute, MCP server, A2A agent server, generic webhooks, and
cross-origin chat embeds. Returns 402 (403 for chat embeds) with an upgrade
message; provider webhooks, session/browser runs, internal-JWT executor traffic,
and self-hosted are unaffected.

- Add isApiExecutionEntitled / isWorkspaceApiExecutionEntitled gate helpers
- Gate the execute, mcp/serve, a2a/serve, webhooks/trigger, and chat routes
- Deploy modal: show an upgrade prompt on the API/MCP/A2A tabs for free users
- Upgrade page: rename Pro feature to 'Deploy workflows as APIs'; API endpoint
  rate-limit row shows 0 for Free

* test(billing): mock api-execution gate in webhook + execute route tests

These pre-existing tests exercise gated routes with API-key/generic-webhook
paths; on hosted (CI) the gate now returns 402. Mock the gate as entitled by
default and add a generic-webhook 402 coverage case.

* fix(billing): address review findings on the paid-plan gate

- execute route: gate on the workflow's workspace billed account (like MCP/A2A/
  webhooks/chat) instead of the caller/creator's personal plan, so a paid
  workspace is never 402'd because an individual is on free
- webhooks: all-generic-all-free fan-out now returns 402, not a 500 'No webhooks
  processed' fallback
- deploy modal: hold the gate closed until the subscription query resolves
  (isFree(undefined) is true) to avoid flashing the upgrade wall at paid users

* fix(billing): gate on isBillingEnabled, not isHosted

The paywall should follow billing enforcement, not the hostname. Keying off
isHosted would still 402 free users on a hosted deployment with BILLING_ENABLED
unset. Switch the server gate (api-access, chat embed) to isBillingEnabled and
the deploy-modal UI to the client NEXT_PUBLIC_BILLING_ENABLED flag (matching the
Inbox paywall), so a billing-disabled deployment skips the gate entirely.

* feat(billing): add FREE_API_DEPLOYMENT_GATE_ENABLED kill-switch

Gate the programmatic-execution paywall behind a dedicated backend env flag
(combined with BILLING_ENABLED), off by default, so it can ship dark and be
enabled per-deployment after a backend sanity check. Backend only — the deploy
modal UI is unchanged.
2026-06-13 20:57:09 -04:00
Vikhyath Mondreti 3a796f083d improvement(permissions): permission groups scoped to organization level (#5035)
* improvement(permissions): permission groups scoped to organization level

* chore(db): drop 0235 permission-groups migration to regenerate after staging merge

* merge latest staging
2026-06-13 17:56:10 -07:00
Waleed 6282b16c4f feat(hubspot): add notes, emails, properties & associations tools (#5037)
* feat(hubspot): add notes, emails, properties & associations tools

- Add 11 tools: get_properties (read property/enum options), notes
  (create/get/list/search), email engagements (create/get/list/search),
  and v4 associations (list/create)
- Add scopes: crm.objects.notes.read/write, crm.objects.emails.read/write,
  sales-email-read (required for email engagement content)
- Wire new operations into the HubSpot block (subBlocks, conditions,
  tool mapping, outputs, BlockMeta templates/skills)
- Fix pre-existing bugs found in validation: list_marketing_events list
  URL (/marketing/marketing-events/v3), appointment property names
  (hs_appointment_*), get_users output shape (CRM envelope), create_list
  response unwrap, and stringified-associations parsing in create tools
- Regenerate integration docs

* fix(hubspot): drop non-grantable notes/emails scopes; remove inline comments

- crm.objects.notes.*/crm.objects.emails.* are not grantable HubSpot
  scopes (would break the OAuth authorize flow). Notes/emails engagement
  and association endpoints are authorized by crm.objects.contacts.*;
  sales-email-read remains for email-engagement content
- Remove non-TSDoc inline comments from new tool files

* fix(hubspot): address review comments

- Forward the properties param for the Get Users operation (block param
  mapping + Properties-to-Return field now include get_users)
- Use Record<string, unknown> for create_note/create_email request bodies
- Only send Content-Type on create_association when a body is sent
  (default-association PUT has no body)
- Remove stray duplicate JSDoc opener in types.ts
2026-06-13 17:42:44 -07:00
Emir Karabegandwaleed bcedadf1b1 feat(scheduled-tasks): calendar views + persisted, runnable tasks (#4979)
* improvement(resource): simplify table shell, toasts, and loading breadcrumbs

- Resource.Table: remove internal sorting (defaultSort/sortValues) and the
  emptyMessage state — rows render in the order given, chrome always paints
- Resource: root is now the positioning context for overlays; consumers
  (files, tables, knowledge, document) wrap detail views in <Resource>
  instead of hand-rolled divs
- ResourceHeader: root titles no longer truncate during initial layout;
  LocationFocusVeil gates the portal on mount to fix a hydration mismatch
- Toasts: drop the StackDismiss ring and stack countdown — each toast runs
  its own timer; remove the Mod+E clear-notifications command; align toast
  typography and icons with chip chrome
- Breadcrumbs: use the canonical '…' placeholder while names load
- incident.io: fix display name and catalog slug (with redirect)
- Add dev:capped / dev:full:capped scripts with a 4GB heap cap

* feat(scheduled-tasks): calendar views; rename Mothership to Sim/Chat

Add month/time calendar views for scheduled tasks with toolbar, event
chips, and a create-task modal, backed by calendar-grid and
schedule-events utils (with tests) and a use-calendar hook. Replace the
old schedule-modal/context-menu flow.

Rename the "Mothership" agent to "Sim" and the chat surface to "Chat"
across landing copy, constitution, block metadata, API error messages,
and copilot/data-drain internals. Drop unused workspace route layouts.

* fix(emcn): force dropdown menus modal inside dialogs so they scroll

A non-modal DropdownMenu portals outside an open dialog's
react-remove-scroll subtree, so its content cannot be wheel-scrolled
(e.g. the time picker in the scheduled-task create modal). ModalContent
now marks its subtree via an InsideModal context, and the emcn
DropdownMenu root upgrades itself to modal inside dialogs so it mounts
its own scroll lock and focus scope; page-level menus keep their
consumer-chosen modality.

Also stretch the create-task modal's date/time chip controls to full
width and drop the dead EDGE_GUTTER constant left behind by the
equal-tracks calendar layout.

* fix(scheduled-tasks): address review — midnight rollover, stub feedback, smooth Today scroll

- useCalendar: today was frozen at mount, so after midnight the isToday
  column highlight and the current-time indicator stayed on the previous
  day. today is now state refreshed by a sleep-resilient minute poll
  that only re-renders when the calendar day actually changes
- CreateTaskModal: the stub submit closed silently, reading as false
  success; it now shows an info toast that the task was not created
- ScheduleCalendar: Today presses scroll smoothly as an orientation
  cue; mount and scope switches keep instant positioning

* feat(emcn): view-only field primitives + scheduled-task modals

- ChipCopyInput (canonical view-only copy field), ChipTimePicker,
  ChipModalField type='copy', ChipTextarea viewOnly; new border chip
  variant and shared chipPrimaryFillTokens
- migrate ~40 consumers off disabled inputs and the deleted
  CopyableValueField; ChipConfirmModal description->text and
  secondaryActions[] API sweep
- scheduled-tasks: rename create-task-modal to task-modal, add
  task-details-modal + task-context-menu, useScheduledTasks hook
- home: extract prompt-editor (usePromptEditor) out of user-input

* feat(scheduled-tasks): persist + run tasks via the job-schedule backend

Wire the calendar UI to the existing sourceType='job' workflow_schedule
backend instead of local component state, so tasks persist and actually
run as Sim agent invocations.

- schema: add contexts (@-mentions resolved into the run), excludedDates
  (per-occurrence deletes), and endsAt (recurrence end) to workflow_schedule
  (migration 0235)
- contracts/schedules: expose one-time `time`, contexts, endsAt on create;
  add the exclude_occurrence action; nullable cron in the create response
- orchestration: persist the new fields, honor exclusions + end boundary via
  a shared computeNextRunAt, add performExcludeOccurrence
- execution: forward contexts to /api/mothership/execute and recompute the
  next run through computeNextRunAt
- mothership/execute: accept + resolve contexts like the interactive chat path
- frontend: replace the local hook with React Query (create/update/delete +
  exclude-occurrence), expand recurrences into calendar occurrences, add the
  recurrence control (frequency + end) and the recurring this/all delete dialog

* chore(scheduled-tasks): satisfy biome line-width on user-input imports

* fix(scheduled-tasks): log agent-context resolution failures in execute route

* fix(scheduled-tasks): keep context double-cast adjacent to its boundary annotation

* fix(scheduled-tasks): preserve @-mention contexts on edit; sync editor valueRef on input

* fix(scheduled-tasks): footer-wrap modal controls; audit fixes; cleanup

- chip-modal: footer secondary cluster now wraps (min-w-0 flex-wrap) with a
  non-shrinking action cluster, so scheduling controls can never clip Cancel/
  primary; recurrence labels compacted so the common case stays one row
- schedule-execution: failure path now completes a recurring job when maxRuns/
  endsAt/exclusions are exhausted (and a one-time/maxRuns job), mirroring the
  success path instead of leaving it active with a stale nextRunAt
- prompt-editor: commitValue keeps valueRef in lockstep with state on the
  mention-hook setter paths, completing the stale-ref fix
- task-modal: preserve @-mention contexts on edit (seed editor.setContexts);
  single emptiness source of truth
- recurrence-control: preserve prior count when toggling end type; drop a
  needless useMemo
- contracts: reuse scheduleContextSchema for the execute contexts shape

* feat(scheduled-tasks): valid future default launch time; test scheduleToTasks mapping

* chore(scheduled-tasks): convert added inline comments to TSDoc

* simplify(scheduled-tasks): reuse date-fns for launch/end math; drop dead 'running' status + single-use helper

---------

Co-authored-by: waleed <walif6@gmail.com>
2026-06-13 16:16:40 -07:00
Vikhyath Mondreti 7b5c7760c5 improvement(sim-trigger): change execution terminology to run (#5033) 2026-06-13 13:40:20 -07:00
Waleed 1205730c35 feat(blocks): add external-service url to block metadata (#5032)
* feat(blocks): add external-service url to block metadata

- Add optional `url` field to BlockMeta for the integration's own homepage (e.g. exa.ai, salesforce.com), distinct from docsLink which points at Sim's docs
- Populate `url` on all 209 integration blocks with verified homepages (protocol/internal blocks without a single vendor site are left without one)
- Document the field in the add-block skill and command, with rule + checklist entries
- Backfill missing docsLink on dspy and add a few accurate tag entries

* fix(blocks): drop tag backfills to avoid catalog drift

Revert the agiloft/exa/tailscale tag additions; integrations.json (used by landing/SEO) is not regenerated here, so the expanded tags would diverge from getAllBlockMeta(). Keep the url additions, which are not projected into integrations.json.

* fix(blocks): correct inaccurate tool URLs flagged in review

- spotify: open.spotify.com (web player) -> www.spotify.com brand homepage
- sts: point to the STS API reference (aws.amazon.com/sts 404s; STS has no standalone product page) instead of the shared IAM page
- microsoft: drop locale-specific /en-us/ segments across Excel, Teams, OneDrive, SharePoint, Outlook, Dataverse, Planner, Entra ID, and shorten OneDrive/Outlook to stable product roots
2026-06-13 12:37:50 -07:00
Waleed 746520c1a7 fix(mothership): streaming completion-flash fix + Tavily brand icon (#5030)
* fix(mothership): keep isAnimating latched so completed messages don't flash

The streamed-text reveal latches `mode` and `animated` via `keepStreamingTree`
to avoid the streaming→static handoff flash, but `isAnimating` was still wired
to `isRevealing`, which flips false the instant the reveal catches up. Streamdown
treats `isAnimating: false` as "streaming over" and rebuilds the whole message
without the per-word animation spans — that DOM rebuild is a visible flash when a
message finishes.

Wire `isAnimating` to the same `keepStreamingTree` latch so all three Streamdown
props stay constant across completion. Content is stable once revealed, so a
permanently-true `isAnimating` has no new tokens to fade and never re-animates.

* feat(tavily): official brand icon and white block background

Replace the Tavily block icon with the official brand mark and set the block
bgColor to white. Ran generate-docs so the docs app icon, the Tavily docs page,
and the integrations catalog pick up the new icon/color.
2026-06-13 11:24:54 -07:00
Vikhyath Mondreti b74a56dde3 fix(db-part-4): enforce consistent cross-resource lock ordering (#5027) 2026-06-13 11:10:10 -07:00
Waleed 65c70298e5 fix(chat): escape attachment filename and validate file URL scheme to prevent XSS (#5028) 2026-06-13 11:08:17 -07:00
Waleed e51bc5715a fix(skills): reuse shared upload field in skill import modal; logo-only Quartr icon (#5026)
* fix(skills): reuse shared upload field in skill import modal; logo-only Quartr icon

- Replace the hand-rolled drop zone in the skill import modal with the shared
  ChipModalField type='file' control (same component the Knowledge Base and
  Help & Support modals use), so the upload zone is visually consistent.
- Migrate the GitHub-URL and paste-content rows to ChipModalField so every
  field shares the canonical px-4 gutter and error rendering, and align the
  'or' dividers to match.
- Drop the monospace font on the paste textarea so its text matches the rest
  of the modal.
- Quartr icon now renders the logo mark only (no wordmark) as a black mark on
  a white rounded tile.

* fix(emcn): restore upload spinner via loading prop on ChipModalField file control

Addresses review feedback — the shared file drop zone now accepts an optional
loading prop that renders an animated spinner and blocks further picks while an
async import is in flight, restoring the feedback the skill import modal lost
when it migrated off its bespoke drop zone.
2026-06-13 10:59:00 -07:00
Waleed eb1009da1c improvement(react-query): codebase-wide audit — server-state hooks, webhook coherence, resume migration (#5024)
* improvement(react-query): codebase-wide audit — server-state hooks, webhook coherence, resume migration

* chore(react-query): add static pattern linter + address review feedback

- add scripts/check-react-query-patterns.ts (staleTime/signal/key-factory/inline-key
  enforcement) wired into CI as check:react-query; strict zone hooks/queries/**, ratchet elsewhere
- fix(resume): use same-origin relative path for resume POST (getBaseUrl could cross origin
  on whitelabel/preview hosts and drop session cookies) — Cursor Bugbot
- remove explanatory inline comments in favor of TSDoc per repo convention
2026-06-13 10:14:07 -07:00
51733b8d85 fix(db): correct misleading error message when DATABASE_REPLICA_URL is malformed (#5023)
* 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(db): correct misleading error message when DATABASE_REPLICA_URL is malformed

The error message said reads fall back to the primary when unset, but the
code throws a fatal error instead. The misleading parenthetical contradicted
actual behavior and could waste time during incident response when an operator
sees this message and expects graceful degradation.

---------

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: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: Theodore Li <theo@sim.ai>
2026-06-13 09:18:51 -07:00
Vikhyath Mondreti 9a4c9d2af1 fix(db-part-3): bound cross-request shared promises against pool wedge (#5021)
* fix(db-part-3):  bound cross-request shared promises against pool wedge

* address comments
2026-06-12 23:58:03 -07:00
Waleed 005fa1027b perf(mothership): virtualize chat transcript and isolate input from stream re-renders (#5019)
* perf(mothership): virtualize chat transcript and isolate input from stream re-renders

Long chats rendered every message into the DOM with no windowing — a custom
rAF "progressive list" only smeared the mount cost across frames without
capping it. At ~1000 messages this was 52k DOM nodes and a 21s main-thread
block on open, and the input toolbar re-rendered on every streamed token.

- Virtualize the message list with @tanstack/react-virtual using dynamic
  measureElement, stable per-row keys, and a tuned size estimate. Only the
  visible window mounts, so load cost is now flat regardless of transcript
  length. Remove the now-redundant useProgressiveList hook.
- Memoize UserInput and stabilize its callbacks (useCallback in MothershipChat
  and home) so streaming ticks no longer re-render the entire input toolbar.
- Keep the existing useAutoScroll for streaming stick-to-bottom (it reads the
  virtualizer's real scrollHeight) and add a per-chat scrollToIndex for initial
  positioning before paint.

Measured on a cloned 1032-message chat: time-to-rendered 26.3s -> 1.7s,
main-thread blocked 21.4s -> 0.8s, DOM nodes 52k -> 1.4k, typing-while-
streaming p-max 104ms -> 26ms. Adds scripts/perf/ harness used to validate.

* address review: pending-chat scroll, flash on tail unmount, empty-row gap, per-role estimate

- Pending-chat initial scroll (Cursor, high): seed the scrolled-chat guard with
  a unique sentinel so a not-yet-persisted chat (undefined chatId) with messages
  still scrolls to bottom instead of being treated as already-scrolled.
- Streaming-row flash (review of virtualization): pin the last row in the
  rendered window via rangeExtractor so scrolling it out of the overscan window
  and back mid-stream can't unmount/remount it and re-fire the reveal fade.
- Empty assistant row gap (Greptile): move the row gap from the virtual-item
  wrapper into the row content so a null-rendering (finalised, empty) assistant
  turn collapses to zero height instead of leaving a pb-6 blank slot.
- Per-role row-height estimate instead of a single blended constant, so the
  scrollbar drifts less as off-screen rows resolve.
- Drop the scripts/perf harness from the PR.

* fix(mothership): preserve user-row top spacing in virtualized layout

Restoring pt-3/pt-2 on the user row keeps the exact inter-row rhythm from the
old space-y-6 layout: assistant→user gaps stay 24+12px and user→assistant stay
24px, instead of becoming uniform when the gap moved to per-row pb.

* fix(mothership): don't re-scroll when a pending chat persists its id

The per-chat initial-scroll guard treated undefined→persisted-id as a chat
switch and scrolled to the bottom again, yanking the viewport down if the user
had scrolled up mid-stream. Treat that transition as the same conversation:
adopt the id without re-scrolling. Genuine chat switches (id→different id) still
re-scroll.
2026-06-12 23:05:57 -07:00
Waleed 26224b8edb chore(providers): remove claude-fable-5 model (#5020) 2026-06-12 19:02:32 -07:00
c3c341632f fix(mothership): tenant-check outputTable writes and route them through replaceTableRows (#5011)
* 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(mothership): tenant-check outputTable writes and route them through replaceTableRows

maybeWriteOutputToTable / maybeWriteReadCsvToTable accepted any table id
without verifying it belongs to the caller's workspace, so a foreign
table's rows could be wiped and replaced cross-tenant. They also wrote
rows with raw drizzle keyed by column *name*, bypassing the service
layer's job-slot lock, validation, plan row limits, rowCount
maintenance, and the stable column-id keying every other writer uses.

Both handlers now reject tables outside the caller's workspace and
delegate to replaceTableRows with name→id remapped rows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(mothership): fail outputTable writes per-row when any row matches no columns

Review feedback: the all-rows-empty guard let mixed batches slip
unmatched rows through as empty objects. Reject on the first row that
maps to zero columns, naming the row. Adds the missing CSV-suite parity
tests (no-matching-headers, service-error surfacing).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Waleed <walif6@gmail.com>
Co-authored-by: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com>
Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 21:49:42 -04:00
Theodore Li fb9e481f32 feat(mailer): gate outbound email on AppConfig access-control ban list (#5018)
* Revert "improvement(auth): layer disposable-email-domains into signup email validation (#5010)"

This reverts commit 2c0a10a561.

* feat(mailer): gate outbound email on AppConfig access-control ban list

* ci(migrations): restore dev schema-push TTY rename/drop guard dropped by #5010 revert
2026-06-12 21:39:33 -04:00
Waleed 98948c0d9d fix(tables): heartbeat export job before upload so the stale janitor can't kill a live finalize (#5017)
* fix(tables): heartbeat export job before upload so the stale janitor can't kill a live finalize

* chore(tables): rename stale-janitor counters to cover all table job types
2026-06-12 18:06:22 -07:00
Theodore Li 43017d72e0 Revert "improvement(auth): layer disposable-email-domains into signup email validation (#5010)" (#5014)
This reverts commit 2c0a10a561.
2026-06-12 20:42:09 -04:00
Waleed cd324d5657 refactor(deployments): consolidate version reads, status mapping, and v1 auth prologue (#5013)
* refactor(deployments): consolidate version-list reads onto listWorkflowVersions

The UI deployments list route and the mothership get_deployment_log handler
each had their own inline version query; both now consume the shared
persistence helper, so every deployment surface (UI, v1, admin, tools,
mothership) reads versions through one code path.

* refactor(deployments): shared status mapper, single-version fetch, and v1 workflow resolver

- statusForOrchestrationError maps orchestration error codes to HTTP statuses
  in one place (was an identical ternary in six routes: UI deploy/activate,
  v1 deploy/rollback, tool deploy/promote)
- getWorkflowDeploymentVersion consolidates the single-version fetch used by
  the UI version GET and the deployments tool version route
- resolveV1DeploymentWorkflow extracts the v1 mutation prologue (active-record
  load, admin permission check, 404 masking) shared by deploy, undeploy, and
  rollback
2026-06-12 17:12:06 -07:00
Waleed 58cff68b5e feat(deployments): add v1 deployment endpoints and Deployments block (#5009)
* feat(deployments): add v1 deployment endpoints and Deployments block

* fix(deployments): require deployed workflow for rollback, normalize warnings, guard orphaned workspaceId

* fix(deployments): workspace-bound tool routes, optional-body parsing, version bounds, and 404 masking

- Tool routes now require the executing workspace ID and reject cross-workspace targets
- v1 deploy/rollback read optional bodies via parseOptionalJsonBody (size-capped, 400 on malformed JSON)
- Version numbers bounded to the Postgres integer range
- v1 mutation routes mask access failures as 404, matching the v1 detail route
- listWorkflowVersions returns description and normalizes admin-api deployedByName (parity with mothership get_deployment_log)
- Workflow selector no longer auto-selects the first workflow (new autoSelectFirstOption opt-out)
- Shared deployment version metadata field schemas across UI/v1/tool contracts

* chore(api-validation): bump route baseline for rebased staging (826)

* fix(docs): use real ID formats in OpenAPI examples

Workflow, workspace, folder, knowledge-base, document, and execution IDs are
plain UUIDv4; workspace file IDs are wf_<shortId>; table and row IDs are
tbl_/row_ + de-dashed UUID. Replaces all fake prefixed example IDs (wf_abc123,
ws_xyz789, exec_..., kb_..., etc.) accordingly and marks the deploy body
description as nullable to match the shared schema.

* feat(deployments): resolve workflow names in block UI, add workflow_undeployed Sim trigger event

- Deployments block now uses the workflow-selector subblock (same as the
  Workflow block), so the canvas tile shows the workflow name instead of the
  raw ID; reverts the now-unneeded dropdown autoSelectFirstOption prop
- Adds workflow_undeployed to the Sim workspace-event trigger, emitted by
  performFullUndeploy through a shared lifecycle-event dispatch loop
2026-06-12 16:38:08 -07:00
Theodore LiandClaude Fable 5 2c0a10a561 improvement(auth): layer disposable-email-domains into signup email validation (#5010)
* ci(migrations): fail dev schema push with an actionable error on rename/drop prompt

`drizzle-kit push --force` only suppresses the data-loss confirm, not the
rename-vs-drop disambiguation prompt. That prompt fires whenever a diff both
adds and drops tables/columns at once (e.g. migration 0231 created
sim_trigger_state while dropping the workspace_notification_* tables), and in
CI it crashes with a bare "Interactive prompts require a TTY" stack trace.

Catch that specific failure in the dev push step and emit a GitHub error
annotation explaining the cause and the fix (drop the stale objects on the dev
DB to match schema.ts — the same DROPs the versioned migration already applied
to staging/prod), instead of leaving an opaque trace. Exit status is preserved
either way.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* improvement(auth): layer disposable-email-domains into signup email validation

Compose the disposable-email-domains list (exact + wildcard) into better-auth-harmony's validator alongside its bundled Mailchecker list, so signup rejects an email if either flags it. Server-only module to keep the dataset out of the client bundle.

* improvement(auth): defer disposable-domains dataset behind lazy dynamic import

Address review: load the ~120K-entry dataset on first use instead of at module import, so deployments with SIGNUP_EMAIL_VALIDATION_ENABLED off never pay the cost. Add a bare wildcard-base test case.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 18:44:39 -04:00
Vikhyath Mondreti 020baad9b3 improvement(organization): invite validation experience (#5008)
* improvement(organization): invite validation experience

* address comments
2026-06-12 14:50:54 -07:00
Waleed 31e166f77f fix(jira): add classic JSM scopes to close granular scope-set gap (#5005)
* fix(jira): add classic JSM scopes to close granular scope-set gap

* fix(jira): note read:user:jira requirement for granular-only tokens in docs
2026-06-12 14:47:04 -07:00
Waleed 0c2dbac066 fix(tables): header "T…" flicker — emcn barrel Table component shadowed the Table icon in loading fallbacks (#5007) 2026-06-12 14:20:41 -07:00
ccda764ae2 feat(integrations): add Documentation link to service-account connect modals (#5004)
* 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

* feat(credentials): add Atlassian service account credentials

* improvement(credentials): tighten Atlassian service account plumbing

- Collapse fetchOAuthTokenBundle into fetchOAuthToken (returns the bundle)
- Reuse serviceAccountJsonSchema in the JSON form instead of hand-rolled checks
- Use parseAtlassianErrorMessage for log details; drop one-line bearer helper
- Extract ATLASSIAN_SERVICE_ACCOUNT_PROVIDER_ID/_SECRET_TYPE constants
- Use Drizzle .returning() instead of post-insert SELECT
- Helper for the duplicated 401/403 + non-OK pattern in the validator

* docs(credentials): add Atlassian service account setup guide

- New /integrations/atlassian-service-account doc covers token creation,
  scope selection, and adding the credential to Sim
- Form's "View setup guide" link now points at the doc
- Fix the existing Google form link that pointed to the wrong path

Screenshot TODOs left inline as MDX comments for the docs team.

* docs(credentials): add Atlassian service account screenshots

- Auth type picker, Sim add-credential modal, Jira block credential dropdown
- Scope-picker screenshot still TODO

* docs(credentials): add Atlassian scope picker screenshot

* fix(credentials): address greptile feedback on Atlassian SA

- Drop stale 'email and API token' copy from the service description
  (we only collect a token + domain, no email field)
- Move duplicate display-name check inside the create transaction so
  concurrent POSTs can't both pass the check and insert duplicates

* fix(docs): move Atlassian screenshots to docs/public

Docs site serves /static/* from apps/docs/public, not apps/sim/public —
matches the existing google-service-account screenshot convention.

* fix(credentials): address review feedback on Atlassian SA

- SSRF: only accept *.atlassian.net / *.jira-dev.com hosts before fetching
  tenant_info, blocking probes against localhost/internal IPs
- Confluence spaces selector: pull cloudId from the SA secret instead of
  calling accessible-resources, which 401s for scoped service-account tokens
- Case-insensitive https?:// strip so HTTPS://team.atlassian.net normalizes
  correctly

* chore: merge staging and bump API validation route baseline to 727

* perf(credentials): single-resolve in confluence spaces selector

Atlassian SAs were hitting resolveOAuthAccountId twice (once via
refreshAccessTokenIfNeeded, once directly to read cloudId) and
decrypting the secret twice (via getAtlassianServiceAccountToken
inside refresh, then again via getAtlassianServiceAccountSecret).

Resolve once up front and branch the whole flow on the result —
SA path skips refresh entirely and pulls token+cloudId from a
single secret read.

* refactor(credentials): consolidate Atlassian SA creation into /api/credentials

Atlassian service-account creation lived in its own route, contract, and
mutation hook, copy-pasting ~140 lines of insert/membership/audit/posthog
boilerplate from /api/credentials. Two endpoints means two authz paths,
two audit shapes, two TOCTOU stories — they will drift.

Fold Atlassian into the existing service_account branch of /api/credentials,
dispatching by providerId. The Atlassian validator (tenant_info + Bearer
/myself, SSRF host allowlist, typed error codes) lives in
lib/credentials/atlassian-service-account.ts and is the only Atlassian-
specific piece left. AtlassianValidationError maps to a {code, error} 400
in the existing catch block; the rest of the flow (transaction, members,
audit, posthog, dup-check) is now shared with Google SA + env credentials.

Delete:
- /api/auth/atlassian-service-account route
- contracts/atlassian-service-account.ts + barrel export
- useCreateAtlassianServiceAccount hook
- API audit baseline 727 → 726

Both forms (Google JSON-key, Atlassian token+domain) now call
useCreateWorkspaceCredential with the appropriate body shape.

* fix(credentials): close TOCTOU and restore typed errors after consolidation

- Add inner duplicate-guard inside the create transaction (DuplicateCredentialError)
  to close the race that the outer findExistingCredentialBySource leaves open.
  service_account rows have no DB-level unique index on (workspaceId, providerId,
  displayName), so this is the actual safety net. Tx-internal check applies to
  Google + env_workspace too — race-safety win for all credential types.
- Re-emit {code: 'duplicate_display_name', error: ...} on conflict so the form's
  ERROR_MESSAGES.duplicate_display_name mapping is reachable again.
- Thread Atlassian-specific audit metadata (atlassianDomain, atlassianCloudId)
  back into recordAudit; consolidation had dropped them.
- Use ATLASSIAN_SERVICE_ACCOUNT_PROVIDER_ID constant in contract superRefine.
- Drop `error: any` in catch in favor of `error: unknown` + getPostgresErrorCode.

* chore(credentials): drop dead createWorkspaceCredentialBodySchema + updateWorkspaceCredentialBodySchema

Both shadowed the actually-used schemas (createCredentialBodySchema /
updateCredentialByIdBodySchema) and were missing the apiToken/domain
Atlassian fields. A future change could pick the wrong one and silently
drop those fields. Confirmed zero non-definition references in the repo
(grep across apps/, packages/, scripts/ minus build artifacts).

* fix(credentials): scope inner duplicate re-check to service_account

OAuth dedupes by accountId, env_* by envKey — both have DB-level partial
unique indexes that surface as 23505. The previous inner re-check fired
for all types and always threw DuplicateCredentialError, which mapped to
'duplicate_display_name' in the UI even when the real conflict was a
duplicate OAuth account or env key. Restrict the in-tx re-check to
service_account (the only type without a DB-level index) and let the
23505 handler emit a generic message for everything else.

* feat(integrations): add Documentation link to service-account connect modals

* fix(integrations): point service-account modal docs links at Sim guides

* fix(integrations): rename service-account modal docs link to Setup guide

---------

Co-authored-by: Waleed <walif6@gmail.com>
Co-authored-by: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com>
Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
2026-06-12 17:08:11 -04:00
Theodore LiandClaude Opus 4.8 c02c7b88a3 fix(tables): scope optimistic stop-cancel to the active filtered view (#4996)
A filtered select-all Stop only cancels matching rows server-side, but
the optimistic update flipped in-flight cells across every cached rows
query — stale unfiltered views showed workflows as cancelled until the
refetch. snapshotAndMutateRows gains an onlyKey option; the cancel
mutation passes the active view's exact cache key (filter + sort) when a
filter is present, and onSettled's invalidation reconciles other views.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 15:19:15 -04:00
Waleed bc92a2cc49 improvement(files): fit-width previews and chip-chrome viewer controls (#5002)
* improvement(files): fit-width previews and chip-chrome viewer controls

- PDF and DOCX previews now treat 100% zoom as fit-to-width instead of
  capping at the page's natural print size, removing the dead gutters in
  wide panels (pdf.js re-renders the canvas at the target width and DOCX
  uses CSS zoom, so both stay crisp)
- PreviewToolbar page/zoom controls move from 24px ghost Buttons with
  off-token labels to canonical emcn chips (icon-only Chip pills, text-sm
  --text-body value labels)
- XLSX sheet tabs move from underline-style ghost Buttons to a chip
  cluster using the active pill state
- Audio preview swaps the music emoji for the lucide Music icon on
  design-system tokens

* improvement(files): debounce PDF panel-resize re-rasterisation

With fit-to-width every pageWidth change re-rasterises all page canvases,
so per-tick updates during a panel-divider drag re-rendered the document
continuously. First measurement still applies immediately.

* fix(files): don't let a zero-width first measurement consume the immediate resize slot

A hidden container reports zero width from the ResizeObserver; treating
that as the initial measurement pushed the real first width onto the
debounce path and delayed initial render.

* improvement(files): module cleanup — dedupe media previews, debounce docx refits

- Merge the near-identical AudioPreview/VideoPreview into one MediaPreview
  (shared fetch/blob-URL/error/loading path; only the player differs)
- Debounce docx resize refits the same way the PDF preview debounces width
  measurements (the initial fit comes from the render path, not the observer)
- Document the load-bearing buffer copy in pdf-viewer (pdf.js transfers and
  detaches the ArrayBuffer it receives)
2026-06-12 11:55:01 -07:00
Waleed 37e7121406 improvement(billing): self-heal null usage limits and debounce api-key last-used writes (#5000)
* improvement(billing): self-heal null usage limits and debounce api-key last-used writes

* fix(billing): make usage-limit self-heal best-effort and respect concurrent writes

* improvement(api-key): widen last-used staleness window to 10 minutes
2026-06-12 11:44:33 -07:00
Waleed a5b92b1502 fix(tables): align sidebar dividers, disclosure spacing, and header height with the editor and page header (#5003) 2026-06-12 11:41:18 -07:00
Waleed 636bd74f06 fix(integrations): resolve OAuth connect UI by service id instead of display name (#5001)
* fix(integrations): resolve OAuth connect UI by service id instead of display name

* test(integrations): pin OAuth service resolution for all catalog integrations; fix credential branding reverse lookup

* fix(docs-gen): blank string literals and comments before brace scanning in extractOAuthServiceId
2026-06-12 11:38:01 -07:00
Waleed 2c4d9e98f9 improvement(emcn): show per-chip error tooltips on invalid email chips (#4998)
* improvement(emcn): show per-chip error tooltips on invalid email chips

* improvement(emcn): fall back to generic reason for invalid email chips
2026-06-12 10:56:37 -07:00