Skip to content

Commit 87aeca6

Browse files
feat(zoho-desk): add Zoho Desk integration (#6157)
* feat(zoho-desk): add Zoho Desk integration Add a full Zoho Desk integration: tools, block, icon, and a webhook trigger. Tools (tools/zoho_desk): list/get/update tickets, list/add comments, list/get threads, get contact, list organizations, and download attachments as UserFiles via an internal route. Registered in tools/registry.ts. Block (blocks/blocks/zoho-desk.ts): operation dropdown, OAuth credential, an organization selector backed by GET /organizations, per-operation fields, and BlockMeta templates. Wires the Zoho Desk trigger. OAuth (zoho-desk provider): authorize/token at accounts.zoho.com with access_type=offline + prompt=consent; the Desk REST base is derived from the token response api_domain and persisted so calls honor data residency instead of assuming desk.zoho.com. Every call sends Authorization: Zoho-oauthtoken and the orgId header. Trigger + webhook handler (triggers/zoho_desk, lib/webhooks/providers/zoho-desk.ts): Sim creates and tears down the Zoho Desk webhook subscription. Inbound events are verified with JWT RS256 (X-ZDesk-JWT) against the data-center JWKS, ACKed via the durable queue to meet Zoho's 5s deadline, and fail loudly on Free/Standard editions that cannot create webhooks. * fix(zoho-desk): OAuth PKCE, DC scope-marker parsing, SSRF, and e2e fixes OAuth: forward code_verifier in the custom getToken (PKCE is enabled, so the exchange must echo the verifier or Zoho rejects the request with invalid_request). Surface Zoho's error/error_description, which it returns in the JSON body with HTTP 200, instead of collapsing every failure into "no access token". Data-center base parsing: better-auth persists Zoho's scopes comma-joined with no spaces, so the greedy \S+ marker regex swallowed the whole scope list into the host. Stop the capture at a comma or whitespace in both read sites (token route and webhook handler), so apiDomain resolves to the real Desk host. Attachment SSRF: replace the permissive host regex (which accepted attacker domains like zoho.attacker.com) with a strict Zoho-apex suffix allowlist. Block: guard Number() pagination so a non-numeric typo can't send NaN; add the ignoreSourceId -> sourceId loop-guard header to update_ticket (matching add_comment). Organizations route: surface fetch/Zoho failures with a real status instead of a 200 with an empty list, so the org selector no longer fails silently. * fix(zoho-desk): webhook creation, attachment naming, and HTML content handling Webhook trigger (verified end-to-end against a live Enterprise org): - Omit ignoreSourceId; Zoho rejects a non-Zoho UUID with INVALID_DATA. Drop the generateId() fallback and its providerConfig persistence. - Answer Zoho's create-time notification-URL probe via the existing pending webhook verification mechanism (GET/HEAD matchers) so subscription creation no longer 405s. - mapZohoWebhookError now surfaces Zoho's real errorCode / message / field errors instead of a catch-all edition message, and attaches an HTTP status so 4xx flow through NonRetryableDeploymentError while 429/5xx stay retryable. - Propagate the real status through deploy.ts so failed creates don't retry-loop. get_attachment polish: - Return the downloaded file's name under `name` (ToolFileData key) instead of `filename`, and derive it (explicit -> Content-Disposition -> URL segment -> fallback) so attachments are no longer stored as "untitled". - Gate the add_comment-only `contentType` param so it isn't sent to get_attachment. HTML content handling (Zoho content fields emit raw HTML): - Add a Zoho-local html-to-text converter mirroring the Outlook dual-field pattern: when contentType is 'html', derive a plain-text `contentText` alongside the untouched raw `content` + `contentType`; plainText mirrors. - Apply to comments (list/add), threads (list/get), the ticket description (descriptionText), and the webhook trigger payload. Trigger org selector: Organization is now a credential-scoped combobox that lists the connected account's Zoho Desk organizations. * fix(zoho-desk): review round - DC-base derivation, org-loader resilience, batched-event visibility - deriveZohoDeskBaseFromApiDomain: preserve an already-regional desk.zoho.<tld> api_domain instead of falling back to the US (.com) data center, and map the DC TLD from any zoho(apis).<tld> host - keeps Desk calls in the right data center for residency. - fetchZohoDeskOrganizationOptions: wrap the token/org fetch in try/catch and degrade to an empty list (the org field is a free-text combobox, so manual entry still works) instead of hard-failing the selector on token/DC/network errors. - formatInput: warn (not silently drop) if Zoho ever delivers more than one event in a single payload. * fix(zoho-desk): harden attachment download against redirect-based SSRF/token leak Replace the raw fetch in the attachment route with secureFetchWithValidation (the same guarded fetch the copilot file-download tool uses). The download URL is user/LLM-influenced and Zoho may redirect, so auto-following redirects could send the OAuth token / orgId to an untrusted or internal host. The guarded fetch pins the resolved IP, blocks private/reserved targets on every hop, drops the Authorization header if a redirect leaves the origin (stripAuthOnRedirect), and enforces the 50MB cap while streaming. The strict Zoho apex allowlist still gates the initial origin as defense in depth. * fix(zoho-desk): only add the edition hint when Zoho's error indicates it mapZohoWebhookError appended the "requires Professional edition or higher" guidance to every 403, but a 403 can also mean a wrong org, a missing scope, or a bad token. Gate the hint on Zoho's own errorCode / message matching the permission/edition pattern instead of the bare status, so unrelated 403s surface Zoho's real reason without the misleading suffix. Adds a test for the non-edition 403 path. * fix(zoho-desk): stop duplicating /api/v1 when resolving a relative attachment href A relative attachment href that already starts with `api/v1` (as Zoho's hrefs often do) was concatenated onto getZohoDeskApiBase (which ends in /api/v1), producing `/api/v1/api/v1/...` and a failing download. Extract a tested resolveZohoAttachmentUrl helper that uses absolute hrefs as-is and strips a leading slash + `api/v1/` prefix from relative ones before joining, so the path is correct for absolute, root-relative, and api/v1-prefixed hrefs alike. * fix(zoho-desk): reject an empty update_ticket PATCH with a clear error update_ticket built its PATCH body from optional fields via filterUndefined, so a call with no fields set sent `{}` and surfaced an opaque Zoho failure. Guard the body builder to throw an actionable "provide at least one field" error before the request. Adds a test for the empty and populated body paths. * fix(zoho-desk): fall back to the credential Desk domain in webhook JWT verify verifyAuth chose the JWKS host from providerConfig.apiDomain and otherwise defaulted to the US host (desk.zoho.com), so a non-US webhook row missing apiDomain would verify against the wrong JWKS and reject legitimate events. When apiDomain is absent, resolve it from the OAuth credential's __zoho_domain__ scope marker (mirroring deleteSubscription). The persisted-apiDomain fast path stays DB-free to respect the 5s delivery deadline. Adds tests for both paths. * fix(zoho-desk): apply the Zoho host allowlist to the organizations route The organizations route built its URL from the client-supplied apiDomain and attached the OAuth token without the https-Zoho-host allowlist the attachment route already enforced, so a session-access caller could point the server at an arbitrary origin and leak the token. Extract the shared isZohoHost allowlist and an assertZohoUrl guard into tools/zoho_desk/utils (two consumers now), guard the organizations URL before fetching, and refactor the attachment route to reuse the shared helper. Adds tests for the allowlist and guard. * fix(zoho-desk): propagate provider 4xx in the stable webhook prepare path The v2 stable deploy preparation flattened every registration failure (except path conflicts) to HTTP 500, so a provider-attached permanent 4xx - e.g. Zoho's edition/validation failures from createSubscription - retried instead of failing the deploy terminally. Propagate the attached status (`?? 500`), matching the legacy save path's status-aware mapping so both deploy paths route 4xx through NonRetryableDeploymentError. * fix(zoho-desk): make createSubscription config failures non-retryable createSubscription threw plain Errors (no status) for missing orgId, event type, or credentials, and for a Zoho success with no webhook id - so the deploy outbox mapped them to 500 and retried permanent configuration failures. Attach a 4xx via statusError (400 for missing config/credentials; 422 for the no-id anomaly, where a retry risks duplicate webhooks) so they fail the deploy terminally like the mapped Zoho API 4xx responses. Tests assert the 400 status on the guard paths. * fix(zoho-desk): enrich prevState with contentText symmetrically with payload formatInput derived plain-text contentText only on payload, so an update event for a comment/thread left prevState as raw HTML while payload carried contentText - inconsistent shapes for before/after comparisons. Apply withDerivedContentText to prevState too. Test asserts both are enriched. * docs(zoho-desk): regenerate integration docs Regenerate zoho_desk.mdx from the current tool definitions: removes the stale add_comment `ignoreSourceId` input row (the field was dropped because Zoho rejects arbitrary values) and adds the derived `contentText` / `descriptionText` plain-text fields on comments, threads, and tickets. * fix(zoho-desk): validate the persisted Desk base against the strict host allowlist deriveZohoDeskBaseFromApiDomain trusted any host matching `desk.zoho.[a-z.]+`, so a crafted api_domain like `desk.zoho.com.attacker.com` passed and was persisted as the credential's `__zoho_domain__` REST base - later receiving the OAuth token on every Desk tool/webhook call. Gate the derivation on the strict isZohoHost apex allowlist (which rejects that lookalike), extracted with assertZohoUrl into a dependency-free host-allowlist module so the auth token-exchange path validates hosts without pulling in the tool utilities. The attachment and organizations routes now import the shared guard from there. Also: formatInput now emits the normalized null trigger shape for an empty/ malformed event array instead of leaking a raw `[]` to downstream steps. Tests cover the empty-array shape and the lookalike-host rejection. * fix(zoho-desk): correct API field names, scopes, and host validation Validation pass against Zoho's published Desk API surfaced six defects that typecheck, lint, and the existing suite all passed over, because each one fails silently against the live API rather than erroring. Wire-name mismatches (Zoho ignores unknown keys, so all three were silent): - update_ticket sent `customFields`; the ticket PATCH body names it `cf`. `customFields` exists only as a deprecated alias on other Desk resources and on the separate validate-field-updates endpoint, so updates reported success and applied nothing. - ZOHO_DESK_TICKET_PROPERTIES and ZOHO_DESK_CONTACT_PROPERTIES advertised a `customFields` output; both resources return `cf`. The declared field always resolved undefined and the real one was undeclared. - list_tickets sent `departmentId`; the query param is `departmentIds`, so the department filter was dropped and every department's tickets came back. Content handling: - deriveZohoContentText matched `contentType === 'html'`, but Zoho spells the discriminator per resource: comments use `html`, threads use the MIME form `text/html`. Every thread's `contentText` was therefore raw markup - the exact opposite of the field's purpose. Now normalized across both spellings, parameterized values, and casing, with regression tests. Scopes (least privilege): - Desk.tickets.ALL -> Desk.tickets.READ + Desk.tickets.UPDATE. No tool creates or deletes a ticket; ALL additionally granted ticket DELETE. - Dropped Desk.search.READ (no search tool exists) and Desk.webhooks.READ / .UPDATE (the provider only creates and deletes), plus their orphaned SCOPE_DESCRIPTIONS entries. Host validation - the webhook provider was the only token-carrying path not anchored to the Zoho apex allowlist, including the JWKS fetch, where an unrecognized host would have stood in as the JWT issuer: - createSubscription, deleteSubscription, and verifyAuth now route their base through a shared allowlist check. - getZohoDeskApiBase validates rather than trusting injection precedence. - The organizations route uses secureFetchWithValidation with stripAuthOnRedirect, matching the attachment route it had diverged from. Block and trigger: - The trigger's department field is renamed `triggerDepartmentIds`; sharing the `departmentIds` id let a value typed as a list_tickets filter become the webhook subscription's filter when switching modes. - `isPublic` no longer serializes onto all ten operations, matching the existing gating for `contentType`. - from/limit reject negatives and fractions instead of forwarding them. - update_ticket gains description, resolution, and classification (all already declared as outputs), and a departmentId input so a ticket can be moved. Accuracy corrections to user-facing text, all against the published parameter tables: `from` is 0-based (0-4999, default 0), not 1-based; per-endpoint limits are tickets 1-100/10, comments 1-100/50, threads 1-200/100; sortBy lists Zoho's actual allowed values; the two `include` sets genuinely differ per endpoint; status and priority accept comma-separated lists. Also: path IDs are trimmed via requireZohoDeskId so a pasted trailing space fails with a clear message instead of a %20 404; comment `commenter` and thread `status`/`isDescriptionThread`/`visibility`/`canReply` are now declared; ZOHO_CLIENT_ID/SECRET added to the oauth test env; docs page gains a MANUAL-CONTENT intro covering capabilities, the Professional-edition webhook requirement, and the US-data-center limitation. Not verified from documentation, needs a live account before merge: - the OAuth scope for the attachment content sub-path (Zoho publishes none, and there is an unanswered SCOPE_MISMATCH report against it) - 12 of the 17 offered webhook event ids (5 are confirmed); Ticket_Delete is documented but not offered - the ticket `descriptionContentType` key, and the POST /api/v1/webhooks body shape, neither of which appears in any reachable Zoho reference * chore(zoho-desk): regenerate tool metadata The param and description corrections in the previous commit changed the generated tool surface, so tool-metadata:check failed in CI. Regenerated; the diff is two Zoho-only lines. * fix(zoho-desk): stop posting null for untouched update_ticket fields `filterUndefined` strips only `undefined`, but an untouched subBlock never arrives as `undefined`: the workflow serializer initializes every subBlock value to `null` (stores/workflows/utils.ts) and extractBlockParams writes those nulls straight into tool params, with nothing between the serializer and request.body filtering them. Reproduced against the real serializer and block with only `status` set: basic {"subject":null,"status":"Closed"} advanced {"subject":null,"status":"Closed","priority":null,...,"cf":null} `subject` leaks even in basic mode because it declares no `mode`, so shouldSerializeSubBlock never drops it. Zoho documents subject as a writable field, so every status-only edit either failed the PATCH or blanked the ticket's subject; in advanced mode the whole update surface nulled out, including `cf`. Two things hid this. The empty-PATCH guard was unreachable from the block (the body always carried at least `subject`), and the existing test called buildBody with fields *absent* rather than null - the shape the block never produces - so it could not fail on the real path. Replaces filterUndefined with a local omitUnset that drops undefined, null, and '' (a cleared input means "leave unchanged", not "set to empty"). Adds three tests using the real serializer shape, all verified to fail before the fix. Also fixes the same null-blindness in the block's param mapping, where Number(null) === 0 injected from=0 on every operation, and corrects the shared limit placeholder, which claimed max 100 while list_threads allows 200. * feat(zoho-desk): add Self Client service-account credential Adds a second way to connect Zoho Desk, alongside the interactive OAuth flow: a Zoho Self Client, pasted as client id + client secret + organization id. Built on the existing client-credential-accounts framework rather than a new credential path, so it behaves like the Zoom Server-to-Server and Box CCG accounts already in the repo - a short-lived token minted on demand, no refresh token. Two Zoho behaviors the generic framework does not cover: - `scope` must be COMMA-separated on Zoho's token endpoint; a space-separated list is rejected as an invalid scope. The list comes from getCanonicalScopesForProvider('zoho-desk'), so the Self Client and the OAuth flow can never drift apart on scopes. - Zoho reports OAuth failures in the JSON body, frequently with HTTP 200 (e.g. {"error":"invalid_client"}), so the success body is inspected for an `error` field before the token is read - a status-only check would accept a failed mint. deriveZohoDeskBaseFromApiDomain moves out of auth.ts into the dependency-free host-allowlist module so the minter and the OAuth path share one derivation instead of duplicating it, and the mint response's api_domain now flows through to tools as `apiDomain` (the SA branch of the token route previously returned none, so SA calls would have assumed desk.zoho.com). Docs: hand-authored zoho-desk-service-account.mdx following the existing *-service-account.mdx pages, registered in meta.json and in the generator's keep-list so stale-page cleanup does not delete it. Known limitation, documented in the descriptor helpText and the docs page: webhook triggers still require an OAuth connection. Webhook provisioning resolves credentials through getCredentialOwner/refreshAccessTokenIfNeeded, which is OAuth-account-only for every provider in the repo - not a Zoho-specific gap. Unverified from documentation, needs a live Zoho org before merge: - the `ZohoDesk.` soid prefix. Zoho documents only the syntax {servicename}.{zsoid} with a single CRM example; no first-party doc states the Desk prefix. normalizeZohoDeskSoid passes through any value already containing a '.', so an operator can paste a corrected full soid without a code change. - whether zsoid is the same identifier as the Desk orgId header value. - whether the client-credentials endpoint accepts Desk.webhooks.CREATE/DELETE for a Self Client. - whether the mint response populates api_domain for Desk (documented for CRM); if absent the derivation falls back to the US Desk host. * fix(zoho-desk): derive descriptionText for ticket-shaped payloads Cursor Bugbot: webhook ticket events reached workflows as raw HTML with no plain-text sibling. `withDerivedContentText` only looked at `content` / `contentType`, but ticket resources carry their body on `description` / `descriptionContentType`, so trigger output disagreed with get_ticket. The helper now derives both, which also removed two inconsistencies on the tool side: get_ticket had its own inline copy of the derivation (now one shared implementation that cannot drift), and update_ticket returned its PATCH response raw despite the shared output map declaring descriptionText. `descriptionContentType` remains the one field name unconfirmed in any Zoho reference. It degrades safely - an absent key makes deriveZohoContentText return the value unchanged, so descriptionText mirrors description rather than breaking, exactly as get_ticket already behaved - and it is now one helper to correct if Zoho names it differently. * feat(zoho-desk): let the service account pick its data center Zoho's accounts server is per region, and the integration pinned every call to the US host. For the interactive OAuth flow that is currently unavoidable - better-auth's authorize/token URLs are static per provider - but the service account mints its own token, so the region can simply be chosen. This makes the Self Client the only way a non-US Zoho org can connect. Adds an optional `dataCenter` field to the client-credential framework. Optional matters: ClientCredentialAccountFieldId and ClientCredentialAccountFields are shared with Zoom, Box and Salesforce, whose descriptors and minters are unchanged. Blank keeps the previous behavior (US), so existing credentials are unaffected. Only us/eu/in/au are offered - the four regions where both the accounts server and the Desk REST host are confirmed. CA is deliberately absent: Zoho's accounts docs say accounts.zohocloud.ca while Zoho's own Desk SDK says accounts.zoho.ca, and the two cannot both be right. JP/SA/CN/UK lack a confirmed Desk host. The Desk base is now derived from the selected region rather than inferred from the mint response, which also removes a dependency on `api_domain` being populated for Desk (Zoho documents it for CRM only). When `api_domain` IS present and disagrees with the region, it wins - it is authoritative about where the token actually works - and the mismatch is logged so a mis-selected region is diagnosable. deriveZohoDeskBaseFromApiDomain gains a `try` variant returning undefined so an untrusted api_domain can no longer masquerade as an authoritative US answer and silently override a correct region. A wrong region fails loudly rather than silently: the minter runs as verification on both create and reconnect, so the credential is never persisted in a broken state. Because Zoho reports it as `invalid_client` - a Self Client only exists on its own region's accounts server - the operator hint for that code now names the data center as a candidate cause. Copy is scoped per path rather than blanket "US only": the OAuth service description, trigger setup instructions, and the docs intro now say which path each limitation applies to, and the service-account page documents the four regions with a sign-in-domain to region-code table. * fix(zoho-desk): strip ticket description HTML, classify body-reported refresh failures Final validation pass findings. descriptionText never stripped anything. It was gated on a `descriptionContentType` discriminator that Zoho does not send: the Ticket_Add webhook sample ships `"description": "<div>Description</div>"` with no such key, and the ticket GET/PATCH response field lists have no content-type sibling either. So get_ticket, update_ticket, and every webhook ticket payload emitted descriptionText as a byte-identical copy of the raw HTML, while the declared output promised stripped text. The tests did not catch it because they fabricated the shape - both fixtures constructed `descriptionContentType: 'html'`, a key Zoho never emits, proving the branch works without proving it is ever taken. Ticket descriptions are HTML by convention, so the strip is now unconditional (html-to-text is a near-identity on genuinely plain text), an explicit descriptionContentType is still honored if Zoho ever adds one, and the fixtures now use Zoho's real shape with no content-type key anywhere. A body-reported refresh failure was unclassified. Zoho answers a revoked refresh token with HTTP 200 and `{"error":"invalid_client"}`; refreshOAuthToken only checked `data.ok === false` (a Slack-ism), so the request fell through to the "no access token" guard and returned no errorCode. isTerminalRefreshError could therefore never recognize invalid_client as terminal, the credential was never marked dead, and every later execution retried a refresh that cannot succeed - with the user shown "No access token in refresh response" instead of a reconnect prompt. The body is now classified before the status is trusted, matching what the token exchange and the service-account mint already did. That guard also stopped logging the whole response body, which carries live tokens on a partial success. Also: an unrecognized dataCenter now fails with a named error instead of quietly resolving to US and surfacing as an opaque invalid_client (blank still means US); the webhook JWKS cache is bounded, since its key derives from a providerConfig field that SYSTEM_MANAGED_FIELDS protects from diffing but not from being written; and the attachment `size` output no longer asserts bytes, a unit Zoho documents as KB. * feat(zoho-desk): canonical selectors and BlockMeta skills The block picked its organization with an ad-hoc `combobox` + `fetchOptions`. Only five blocks in the repo did that, and the other four are core blocks (agent/credential/function/logs) - no other OAuth integration used it. Every other resource a user has to identify was a bare short-input taking an opaque numeric id. Zoho Desk now uses the same machinery as the other 25 selector providers: hooks/selectors/providers/zoho-desk/selectors.ts registered in the selector registry, consumed from the block as basic selector + advanced manual input sharing one canonicalParamId, for organization, update-ticket department, and the list-tickets department filter. The trigger's org field moves to the same selector. zoho-desk-org-options.ts is deleted rather than left beside the new path, so blocks/ has zero fetchOptions usages outside the core blocks. Wire params are unchanged (orgId, departmentId, departmentIds, assigneeId, ticketId, contactId) - this is a UI change, not an API change. The organizations route now resolves the credential server-side. It previously had the browser fetch an access token and POST it back, which an earlier audit flagged as the one place a Zoho token left the server; the new selector-credential resolver keeps it server-side for both the OAuth and service-account credential types and re-anchors every outbound host to the Zoho apex allowlist. No agents selector: the endpoint is documented but its OAuth scope is not, and the nearest evidence points at Desk.agents.READ, which we do not request. Adding it would force every existing Zoho Desk user to reconnect for a convenience field, so assigneeId stays a manual input until the scope can be confirmed against a live org. Adds the skills array BlockMeta was missing - 227 of 300 blocks declare one and this did not. Seven skills, each grounded in a use case Zoho or the ecosystem actually advertises (auto-triage, SLA escalation, digest, AI draft reply, customer context, engineering handoff, knowledge-gap report) and each exercising only tools in tools.access. CSAT surveys, ticket creation, dedup and keyword search were deliberately left out: the integration has no tool for them, and a skill implying an unsupported action is worse than a shorter list. * feat(zoho-desk): agents selector and free-text trigger organization Three improvements that were previously deferred only to avoid forcing existing users to reconnect or orphaning saved workflows. This integration is unmerged and has no users, so the constraint does not apply and the better option wins. assigneeId was the last field still asking for an opaque numeric id. It is now a canonical selector pair backed by a new zoho_desk.agents selector, which required adding the Desk.agents.READ scope - the reason it was skipped before. Route follows the departments one exactly: auth before parseRequest, host anchored to the Zoho apex allowlist, secureFetchWithValidation with stripAuthOnRedirect, and a page drain capped at 20 pages with 204 treated as end-of-list. Scope caveat: Zoho publishes no explicit scope line for the list-all GET /api/v1/agents. Every other endpoint in the Agents module documents Desk.agents.READ (get by id, get by email, roles/{id}/agents), and it is the only agents-module scope Zoho defines, so that is the basis. Inference across a module rather than a direct quote - worth one live call before merge, same as the existing attachment-scope note. The trigger regained free-text organization entry, lost when the org field became a selector. The earlier concern - that a manual value would land under its raw subBlock id and never reach the provider - turned out not to hold: buildProviderConfig already collapses canonical pairs and writes the active member under the canonical key. The real gap is narrower and does exist: when canonicalModes pins the group to basic while only the manual field has a value, the collapse deletes the canonical key even though the required-field check passes, so the deploy succeeds and then fails at subscription time. resolveConfigOrgId closes that, with a test. The block/trigger `orgId` id overlap stays shared, now with a comment. Two earlier audits disagreed; renaming turns out to be the wrong call. buildCanonicalIndex has an explicit guard for trigger-mode reuse and blocks.test.ts codifies it as a valid pattern, orgId means the same portal in both modes (unlike departmentIds, which is correctly distinct), and a separate triggerManualOrgId would put two advanced members in one canonical group - getCanonicalValues takes the first non-empty, so a stale tool-mode value could silently supply the trigger's organization. * fix(zoho-desk): make the attachment cap reachable, unbreak selector paging Final audit round. The 50 MB attachment ceiling could never be hit. This route returns the file as base64 inside its JSON body, and the executor reads internal tool responses through readToolResponseBody, capped at 10 MB. Base64 inflates 4/3, so ~7.5 MB of raw bytes is the real ceiling - and the old limit meant a larger attachment was downloaded, encoded and serialized in full (peaking near 250 MB of live allocation, with nothing bounding concurrent downloads) purely to be rejected afterwards. The cap is now the reachable size, so the limit enforces itself while the bytes are still streaming, and an overflow returns 413 with the actual ceiling instead of a generic 500. Raising it properly means uploading in the route and returning a file reference, as the WhatsApp media route does - not a bigger constant. Selector paging assumed a 0-based `from`. Zoho's docs contradict themselves: the pagination section says "range 0-4999, default 0" while the listing examples read as 1-based ("from=5 and limit=50 retrieves records 5 to 54"). Under the 1-based reading, stepping by exactly the page size re-fetches the boundary record and the dropdown shows a duplicate per page. Rather than pick a base that cannot be confirmed without a live tenant, the department and agent drains dedupe by id, which is correct under either reading. The organization list was unpaginated, and Zoho's listing APIs default to ten per page. An account with more accessible portals silently got a truncated dropdown, and since every other selector and every tool call is gated on orgId, a missing portal was unreachable except through the advanced manual field. Both the selector route and list_organizations now request the documented maximum. Docs: regenerated so the trigger table includes manualOrgId, and two service-account claims are hedged to match what the code already says it cannot verify - that zsoid equals the Desk orgId header value, and that every tool works under the requested scopes (Zoho publishes no scope for the attachment content sub-path). Also: status and priority move out of advanced mode - they are the fields most often changed on a ticket update; the custom-fields wand prompt now ends with the required "Return ONLY" clause; and the shared-orgId rationale comment cites the mechanism that actually applies (buildCanonicalIndex dedupe plus the first-non- empty rule in getCanonicalValues) rather than a blocks.test.ts branch that never evaluates this pair. * fix(zoho-desk): five-audit round - serializer trigger-advanced leak, scopes, paging Five independent audits (OAuth/scopes, tools-vs-docs, block/selectors, blast-radius, /validate-trigger). Findings, most severe first. A trigger-mode field was a live tool-mode required param. `shouldSerializeSubBlock` excluded `mode: 'trigger'` but not `'trigger-advanced'`, so the trigger's required `manualOrgId` validated on every tool operation. Reproduced against the real serializer: with the Organization field pinned to advanced, running List Organizations failed with "Missing required fields: Organization ID" - a field that operation does not even render, and which the user could not clear without switching operations. Fixed in the serializer rather than locally, because the Google Sheets/Drive/Calendar pollers have the identical shape. `limit=200` on /organizations was an undocumented parameter I added by extrapolating from /departments and /agents. Zoho documents NO parameters for that endpoint and its sample is a bare GET; the other siblings cap at 100 and Zoho answers out-of-range with 422. Since orgId gates every tool and both other selectors, a 422 there would have made the whole integration unreachable. Reverted to Zoho's documented shape. `descriptionText` was HTML-stripping plain text. The previous round made the strip unconditional after finding Zoho sends no `descriptionContentType`, but Zoho's REST samples show plain descriptions while only the webhook payload is HTML - and the webhook path runs this over contact/account/department bodies too. html-to-text is not identity on plain text: it decodes entities and deletes tag-shaped content ("a < b > c", XML snippets). Now sniffs for markup first. `omitUnset` made every documented field-clear impossible. Zoho's own PATCH sample uses `"classification": ""` and `"productId": ""` to clear. Dropping `''` meant no scalar field could be cleared. Now drops only undefined/null - the serializer-null case it was written for - and forwards `''`. status/priority leaked between operations. One shared subBlock served both the list_tickets filter and the update_ticket value, and subBlock values survive an operation switch, so a filter of "Open,On Hold" could be PATCHed onto a ticket and an update value could silently filter a later list. Split per operation. Auth: `invalid_code` added to TERMINAL_ERRORS - it is Zoho's code for a revoked refresh token, so without it the previous round's refresh fix never actually dead-flagged the credential it was written for. The shared refresh body-error branch now also requires `!data.access_token`, so no provider can have a successful refresh misclassified. The token route now uses the validating `extractZohoDeskBaseFromScope` instead of a private regex with no https/allowlist check - that value is injected into every tool call. Scope list falls back to the requested scopes when Zoho omits `scope`, which would otherwise flag every credential as needing reconnect. The Self Client mint no longer sends `aaaserver.profile.READ`, a scope that grant never uses. Trigger: `includePrevState` now set for every *_Update event, not just tickets - it defaults to false, so prevState was permanently null for contact/agent/task/ article updates while the trigger advertised it. `departmentIds` is only sent for events Zoho documents as accepting it, and the field is conditioned accordingly. Empty filters serialize as `null`, matching Zoho's examples, rather than `{}`. JWKS fetch bounded to 1.5s - jose's default is 5000ms, exactly Zoho's whole delivery deadline, and Zoho publishes no retry. The create-time validation POST fallback is now matched by the pending-verification probe. Ticket_Delete added. All 17 webhook event ids, the POST /api/v1/webhooks body contract, and the JWT claim/JWKS specifics are now confirmed verbatim against Zoho's webhook documentation - previously 12 of 17 events and the entire subscription contract were unverified. * revert(zoho-desk): back out both shared lib/oauth changes Reverting two changes to shared OAuth code because their premise is inferred rather than proven, and neither meets the bar for touching a path every provider runs. `refreshOAuthToken` body-error branch. The premise was that Zoho reports refresh failures with HTTP 200 and an `error` body. That is documented and empirically confirmed for the authorization-code EXCHANGE (see the comment on getToken in auth.ts), but I never confirmed it for the REFRESH grant specifically - and if Zoho returns a proper 4xx there, the existing `!response.ok` path already classifies it via extractErrorCode, making the branch dead code that every one of the ~34 providers still executes on each refresh. A shared branch whose only justification is an unverified inference about one provider is not worth its blast radius. `invalid_code` in TERMINAL_ERRORS. Same problem, worse downside: the code is sourced from a Zoho community post rather than official docs, TERMINAL_ERRORS is consulted for every provider, and a false positive marks a credential dead for an hour. Not adding it simply preserves today's behavior (retry rather than dead-flag), so reverting costs nothing that was previously working. Both are cheap to reinstate, correctly scoped, once a live Zoho account shows what a revoked refresh token actually returns. Kept: the token-redaction on the "no access token" warn, which is an unambiguous improvement independent of Zoho. Also kept, deliberately, is the serializer `trigger-advanced` exclusion - that one rests on a reproduced bug rather than an inference, and it aligns the serializer with the convention the rest of the codebase already follows (blocks.test.ts treats `trigger` and `trigger-advanced` identically in six places, as does the copilot block-metadata tool, and blocks/types.ts documents trigger-advanced as "the advanced side of a trigger field"). * fix(zoho-desk): carry the stored data center through a credential reconnect A reconnect rebuilds the service-account secret blob from the submitted fields only, and the connect modal never prefills - correctly, since for every other field in this family the stored value is a secret the admin must retype. The data center is the first non-secret member of that set, so it was being silently dropped: rotating a client secret on an EU/IN/AU credential moved it back to the US accounts server, where the next mint fails with an opaque invalid_client. performUpdateCredential now reads the stored dataCenter out of the existing blob when the caller does not supply one. The read is failure-tolerant - an undecryptable or unparseable blob yields undefined rather than throwing, so it can never block a reconnect, and the provider default applies as before. Raised independently by three reviewers; I twice argued it was acceptable because the mint fails loudly rather than corrupting silently. That was true and beside the point - the operator still had to guess why. * fix(zoho-desk): delta-audit findings - prevState scope, status leak, HTML sniffer An audit of the commits the earlier five audits never saw. All four findings are in code written as fixes for those audits, which is where this branch has repeatedly introduced new problems. `includePrevState` was sent for Ticket_Comment_Update. The previous commit gated it on an `_Update` suffix and claimed Zoho supports it on every update event. Zoho's webhook doc lists the attribute on Ticket/Contact/Agent/Task/Article update events but NOT on Ticket_Comment_Update, which documents only `departmentIds`. That made it an undocumented filter key on a live subscription create - the same class of risk the same commit reverted `limit=200` for, so it failed that commit's own stated bar. Now an explicit set rather than a suffix rule. The status/priority split did not stop the leak it was written for. The mapping used `operation === 'list_tickets' ? filterValue : updateValue`, whose bare else covers all eight other operations - so a stale Update Ticket status was forwarded into get_ticket, list_comments and the rest. Harmless on the wire (those tools ignore it) but exactly the stale-value pattern the neighbouring gates exist to prevent. Both fields are now scoped to the two operations that declare them. The HTML sniffer destroyed plain text. `/<[a-z!\/][^>]*>/` fires on any `<` followed by a letter with a later `>`, so realistic ticket bodies lost content: "if x<y then z>0" became "if x0", and "replace <username> with the real name" lost the placeholder. It now requires a real element - a paired tag, a self-closing tag, a comment/doctype - or an entity, and the entity arm covers hex references it previously missed. Regression tests verified by reverting to the loose pattern and watching them go red. The reconnect data-center carry-forward is scoped to client-credential providers. As written it added a DB read plus a decrypt to every service-account reconnect for every provider - Slack, Atlassian, all token-paste providers - to carry a field only Zoho has. Also: the JWKS cache-bound TSDoc had been orphaned onto the wrong constant by an earlier insertion, and `cooldownDuration` was dropped since it restated jose's default while only `timeoutDuration` needed justifying. * test(zoho-desk): cover the webhook subscription filter rules The subscription filter logic had no test coverage at all, and it is where the last two rounds both found bugs - includePrevState on an event Zoho does not document it for, and departmentIds sent to events that accept no filters. Adds six cases against the real createSubscription: includePrevState is set for each of the five documented update events and NOT for Ticket_Comment_Update, departmentIds is kept for a filterable event and dropped for one that is not, and an event with no filters serializes as null rather than an empty object. Verified the guard bites: reverting PREV_STATE_EVENTS to the `endsWith('_Update')` rule turns the Ticket_Comment_Update case red. The Ticket_Comment_Update assertion checks the with-departments case as well as the bare one - asserting only `not.toHaveProperty` on the bare filter would pass vacuously, since that filter is legitimately null. --------- Co-authored-by: Waleed Latif <walif6@gmail.com>
1 parent 69289d2 commit 87aeca6

80 files changed

Lines changed: 7242 additions & 27 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/docs/components/icons.tsx

Lines changed: 174 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2221,21 +2221,108 @@ export function EyeIcon(props: SVGProps<SVGSVGElement>) {
22212221
)
22222222
}
22232223

2224+
/**
2225+
* Corporate Atlassian mark, used for family-wide Atlassian credentials — one
2226+
* API token authenticates Jira, Jira Service Management, and Confluence, so no
2227+
* single product mark represents it. Individual products keep their own icons.
2228+
*/
2229+
export function AtlassianIcon(props: SVGProps<SVGSVGElement>) {
2230+
const id = useId()
2231+
const gradientId = `atlassian_gradient_${id}`
2232+
2233+
return (
2234+
<svg
2235+
{...props}
2236+
width='24'
2237+
height='24'
2238+
/*
2239+
* The mark's artwork spans ~66 units; the box is padded to 84.5 so it
2240+
* fills ~78% of its viewBox, matching the inset Atlassian ships on the
2241+
* Jira and Confluence marks (artwork 16→116 inside 128). Without the
2242+
* padding this renders ~30% heavier than its siblings in the same tile.
2243+
*/
2244+
viewBox='-9.2 -8.9 84.5 84.5'
2245+
focusable='false'
2246+
fill='none'
2247+
aria-hidden='true'
2248+
xmlns='http://www.w3.org/2000/svg'
2249+
>
2250+
<defs>
2251+
<linearGradient
2252+
id={gradientId}
2253+
gradientUnits='userSpaceOnUse'
2254+
x1='28.536019'
2255+
y1='35.528544'
2256+
x2='11.406018'
2257+
y2='65.208544'
2258+
>
2259+
<stop offset='0' stopColor='#0052cc' />
2260+
<stop offset='0.92' stopColor='#2684ff' />
2261+
</linearGradient>
2262+
</defs>
2263+
<path
2264+
fill={`url(#${gradientId})`}
2265+
d='m 19.636018,30.518546 a 1.88,1.88 0 0 0 -3.2,0.35 l -16.2299998,32.46 a 1.94,1.94 0 0 0 1.73,2.81 H 24.536018 a 1.87,1.87 0 0 0 1.74,-1.1 c 4.87,-10 1.92,-25.37 -6.64,-34.52 z'
2266+
/>
2267+
<path
2268+
fill='#2684ff'
2269+
d='m 31.546018,1.038546 a 42.81,42.81 0 0 0 -2.5,42.27 l 10.95,21.73 a 1.94,1.94 0 0 0 1.73,1.08 h 22.6 a 2,2 0 0 0 1.67,-2.79 l -31.15,-62.29 a 1.83,1.83 0 0 0 -3.3,0 z'
2270+
/>
2271+
</svg>
2272+
)
2273+
}
2274+
22242275
export function ConfluenceIcon(props: SVGProps<SVGSVGElement>) {
2276+
const id = useId()
2277+
const topGradientId = `confluence_top_${id}`
2278+
const bottomGradientId = `confluence_bottom_${id}`
2279+
22252280
return (
22262281
<svg
22272282
{...props}
22282283
width='24'
22292284
height='24'
2230-
viewBox='0 3 21 24'
2285+
viewBox='0 0 128 128'
22312286
focusable='false'
22322287
fill='none'
22332288
aria-hidden='true'
22342289
xmlns='http://www.w3.org/2000/svg'
22352290
>
2291+
<defs>
2292+
<linearGradient
2293+
id={bottomGradientId}
2294+
gradientUnits='userSpaceOnUse'
2295+
x1='26.791'
2296+
y1='28.467'
2297+
x2='11.792'
2298+
y2='19.855'
2299+
gradientTransform='scale(4)'
2300+
>
2301+
<stop offset='0' stopColor='#0052cc' />
2302+
<stop offset='0.918' stopColor='#2380fb' />
2303+
<stop offset='1' stopColor='#2684ff' />
2304+
</linearGradient>
2305+
<linearGradient
2306+
id={topGradientId}
2307+
gradientUnits='userSpaceOnUse'
2308+
x1='5.209'
2309+
y1='2.523'
2310+
x2='20.208'
2311+
y2='11.136'
2312+
gradientTransform='scale(4)'
2313+
>
2314+
<stop offset='0' stopColor='#0052cc' />
2315+
<stop offset='0.918' stopColor='#2380fb' />
2316+
<stop offset='1' stopColor='#2684ff' />
2317+
</linearGradient>
2318+
</defs>
2319+
<path
2320+
fill={`url(#${bottomGradientId})`}
2321+
d='M19.492 86.227a249.047 249.047 0 00-3.047 4.933c-.867 1.45-.433 3.336 1.016 4.207l19.863 12.188c1.45.87 3.332.433 4.203-1.016a139.349 139.349 0 012.899-4.934c7.832-12.91 15.804-11.46 30.011-4.64l19.72 9.281c1.593.727 3.335 0 4.058-1.45l9.426-21.323c.722-1.453 0-3.336-1.454-4.063-4.203-1.887-12.464-5.805-19.714-9.43-26.82-12.914-49.586-12.043-66.98 16.247zm0 0'
2322+
/>
22362323
<path
2237-
fill='#1868DB'
2238-
d='M20.6 20.23c-6.58-3.18-8.51-3.66-11.28-3.66-3.25 0-6.03 1.36-8.51 5.16l-.407.62c-.333.51-.407.7-.407.92s.111.4.518.66l4.18 2.6c.221.15.406.22.59.22.22 0 .37-.11.59-.44l.666-1.02c1.03-1.57 1.96-2.09 3.14-2.09 1.03 0 2.26.293 3.77 1.02l4.37 2.05c.444.22.93.11 1.15-.403l2.07-4.54c.222-.512.07-.842-.444-1.1M1.41 12.22c6.58 3.18 8.51 3.66 11.28 3.66 3.26 0 6.03-1.35 8.51-5.16l.407-.622c.332-.512.41-.695.41-.915s-.11-.402-.518-.658L17.31 5.93c-.222-.147-.407-.22-.592-.22-.222 0-.37.11-.592.44l-.665 1.02c-1.04 1.57-1.96 2.09-3.14 2.09-1.04 0-2.26-.293-3.77-1.02L4.18 6.18c-.444-.22-.925-.11-1.15.402L.962 11.12c-.222.51-.74.84.444 1.1'
2324+
fill={`url(#${topGradientId})`}
2325+
d='M108.508 37.773a249.047 249.047 0 003.047-4.933c.87-1.45.433-3.336-1.016-4.207L90.676 16.445c-1.45-.87-3.332-.433-4.203 1.016a133.55 133.55 0 01-2.899 4.934c-7.832 12.91-15.804 11.46-30.011 4.64l-19.72-9.281c-1.593-.727-3.331 0-4.058 1.45l-9.422 21.323c-.726 1.453 0 3.34 1.45 4.063 4.203 1.887 12.468 5.805 19.714 9.43 26.825 12.77 49.586 12.042 66.98-16.247zm0 0'
22392326
/>
22402327
</svg>
22412328
)
@@ -2768,19 +2855,58 @@ export function LinkupIcon(props: SVGProps<SVGSVGElement>) {
27682855
}
27692856

27702857
export function JiraIcon(props: SVGProps<SVGSVGElement>) {
2858+
const id = useId()
2859+
const middleGradientId = `jira_middle_${id}`
2860+
const bottomGradientId = `jira_bottom_${id}`
2861+
27712862
return (
27722863
<svg
27732864
{...props}
27742865
xmlns='http://www.w3.org/2000/svg'
2775-
viewBox='0 0 30 30'
2866+
viewBox='0 0 128 128'
27762867
width='24'
27772868
height='24'
27782869
focusable='false'
2870+
fill='none'
27792871
aria-hidden='true'
27802872
>
2873+
<defs>
2874+
<linearGradient
2875+
id={middleGradientId}
2876+
gradientUnits='userSpaceOnUse'
2877+
x1='22.034'
2878+
y1='9.773'
2879+
x2='17.118'
2880+
y2='14.842'
2881+
gradientTransform='scale(4)'
2882+
>
2883+
<stop offset='0.176' stopColor='#0052cc' />
2884+
<stop offset='1' stopColor='#2684ff' />
2885+
</linearGradient>
2886+
<linearGradient
2887+
id={bottomGradientId}
2888+
gradientUnits='userSpaceOnUse'
2889+
x1='16.641'
2890+
y1='15.564'
2891+
x2='10.957'
2892+
y2='21.094'
2893+
gradientTransform='scale(4)'
2894+
>
2895+
<stop offset='0.176' stopColor='#0052cc' />
2896+
<stop offset='1' stopColor='#2684ff' />
2897+
</linearGradient>
2898+
</defs>
27812899
<path
2782-
fill='#1868DB'
2783-
d='M11.03 21.99h-2.22c-3.35 0-5.75-2.05-5.75-5.05h11.93c.619 0 1.02.44 1.02 1.06v12.01c-2.98 0-4.98-2.42-4.98-5.78zm5.89-5.97h-2.22c-3.35 0-5.75-2.01-5.75-5.01h11.93c.618 0 1.06.402 1.06 1.02V24.04c-2.98 0-5.02-2.42-5.02-5.78zm5.93-5.93h-2.22c-3.35 0-5.75-2.05-5.75-5.05h11.93c.618 0 1.02.439 1.02 1.02v12.01c-2.98 0-4.98-2.42-4.98-5.78z'
2900+
fill='#2684ff'
2901+
d='M108.023 16H61.805c0 11.52 9.324 20.848 20.847 20.848h8.5v8.226c0 11.52 9.328 20.848 20.848 20.848V19.977A3.98 3.98 0 00108.023 16zm0 0'
2902+
/>
2903+
<path
2904+
fill={`url(#${middleGradientId})`}
2905+
d='M85.121 39.04H38.902c0 11.519 9.325 20.847 20.844 20.847h8.504v8.226c0 11.52 9.328 20.848 20.848 20.848V43.016a3.983 3.983 0 00-3.977-3.977zm0 0'
2906+
/>
2907+
<path
2908+
fill={`url(#${bottomGradientId})`}
2909+
d='M62.219 62.078H16c0 11.524 9.324 20.848 20.848 20.848h8.5v8.23c0 11.52 9.328 20.844 20.847 20.844V66.059a3.984 3.984 0 00-3.976-3.98zm0 0'
27842910
/>
27852911
</svg>
27862912
)
@@ -7558,6 +7684,31 @@ export function SixtyfourIcon(props: SVGProps<SVGSVGElement>) {
75587684
)
75597685
}
75607686

7687+
/**
7688+
* The "sim" brand wordmark (v1.0 brand guide simLogotype paths — the same mark
7689+
* the navbar/login header renders), inked with the theme-adaptive
7690+
* `--text-body`. Used as the icon for the Auto model option; the wide viewBox
7691+
* letterboxes itself inside square icon slots.
7692+
*/
7693+
export function SimAutoIcon(props: SVGProps<SVGSVGElement>) {
7694+
return (
7695+
<svg
7696+
{...props}
7697+
viewBox='0 0 441 212'
7698+
fill='none'
7699+
xmlns='http://www.w3.org/2000/svg'
7700+
aria-hidden='true'
7701+
>
7702+
<g fill='var(--text-body)'>
7703+
<path d='M0 160.9H29.51C29.51 169.08 32.46 175.61 38.37 180.48C44.27 185.12 52.25 187.44 62.31 187.44C73.24 187.44 81.65 185.34 87.56 181.14C93.46 176.71 96.41 170.85 96.41 163.55C96.41 158.24 94.77 153.82 91.49 150.28C88.43 146.74 82.75 143.86 74.44 141.65L46.24 135.01C32.03 131.47 21.42 126.05 14.43 118.75C7.65 111.45 4.26 101.83 4.26 89.88C4.26 79.93 6.78 71.3 11.81 64C17.05 56.7 24.16 51.06 33.12 47.08C42.3 43.09 52.8 41.1 64.6 41.1C76.41 41.1 86.57 43.2 95.1 47.41C103.84 51.61 110.62 57.47 115.43 64.99C120.46 72.52 123.08 81.48 123.3 91.87H93.79C93.57 83.47 90.84 76.94 85.59 72.3C80.34 67.65 73.02 65.33 63.62 65.33C54 65.33 46.57 67.43 41.32 71.63C36.07 75.83 33.45 81.59 33.45 88.89C33.45 99.73 41.32 107.14 57.06 111.12L85.26 118.09C98.81 121.19 108.98 126.28 115.76 133.35C122.53 140.21 125.92 149.61 125.92 161.56C125.92 171.74 123.19 180.7 117.73 188.44C112.26 195.96 104.72 201.82 95.1 206.03C85.7 210.01 74.55 212 61.65 212C42.85 212 27.87 207.35 16.72 198.06C5.57 188.77 0 176.38 0 160.9Z' />
7704+
<path d='M232.8 212H202.13L202.13 49.76H229.54V77.39C232.8 68.34 239.11 60.66 247.81 54.7C256.73 48.52 267.5 45.43 280.12 45.43C294.26 45.43 306.01 49.29 315.36 57.02C324.72 64.75 330.81 75.01 333.64 87.82H328.09C330.27 75.01 336.25 64.75 346.04 57.02C355.83 49.29 367.9 45.43 382.26 45.43C400.54 45.43 414.89 50.84 425.34 61.66C435.78 72.47 441 87.26 441 106.03V212H410.98V113.65C410.98 100.84 407.71 91.02 401.19 84.17C394.88 77.11 386.29 73.58 375.41 73.58C367.79 73.58 361.05 75.34 355.17 78.88C349.52 82.19 345.06 87.04 341.8 93.45C338.53 99.85 336.9 107.36 336.9 115.97V212H306.55V113.32C306.55 100.51 303.4 90.8 297.09 84.17C290.78 77.33 282.19 73.91 271.31 73.91C263.69 73.91 256.95 75.67 251.08 79.21C245.42 82.52 240.96 87.38 237.7 93.78C234.43 99.96 232.8 107.36 232.8 115.97V212Z' />
7705+
<path d='M184.83 20.55C184.83 31.9 175.64 41.1 164.29 41.1C152.95 41.1 143.76 31.9 143.76 20.55C143.76 9.2 152.95 0 164.29 0C175.64 0 184.83 9.2 184.83 20.55Z' />
7706+
<path d='M179.43 212H149.16V49.76C153.76 51.91 158.88 53.12 164.29 53.12C169.7 53.12 174.83 51.91 179.43 49.76V212Z' />
7707+
</g>
7708+
</svg>
7709+
)
7710+
}
7711+
75617712
export function SimTriggerIcon(props: SVGProps<SVGSVGElement>) {
75627713
return (
75637714
<svg
@@ -8776,3 +8927,20 @@ export function LogfireIcon(props: SVGProps<SVGSVGElement>) {
87768927
</svg>
87778928
)
87788929
}
8930+
8931+
export function ZohoDeskIcon(props: SVGProps<SVGSVGElement>) {
8932+
return (
8933+
<svg
8934+
{...props}
8935+
viewBox='0 0 24 24'
8936+
fill='none'
8937+
xmlns='http://www.w3.org/2000/svg'
8938+
aria-hidden='true'
8939+
>
8940+
<path
8941+
d='M12 2.75c-4.28 0-7.75 3.47-7.75 7.75v3.1A2.6 2.6 0 0 0 3 16.35v1.3A2.6 2.6 0 0 0 5.6 20.25h1.15a.9.9 0 0 0 .9-.9v-4.9a.9.9 0 0 0-.9-.9H6.05v-2.15a5.95 5.95 0 0 1 11.9 0v2.15h-.7a.9.9 0 0 0-.9.9v4.9c0 .17.05.33.13.47-.5.6-1.24.98-2.08.98h-1.02a1.4 1.4 0 0 0-1.31-.9h-1a1.4 1.4 0 0 0 0 2.8h1a1.4 1.4 0 0 0 1.31-.9h1.02c2.06 0 3.74-1.63 3.83-3.67a2.6 2.6 0 0 0 1.44-2.33v-1.3a2.6 2.6 0 0 0-1.25-2.22v-3.1c0-4.28-3.47-7.75-7.75-7.75Z'
8942+
fill='currentColor'
8943+
/>
8944+
</svg>
8945+
)
8946+
}

apps/docs/components/ui/icon-mapping.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ import {
248248
ZendeskIcon,
249249
ZepIcon,
250250
ZeroBounceIcon,
251+
ZohoDeskIcon,
251252
ZoomIcon,
252253
ZoomInfoIcon,
253254
} from '@/components/icons'
@@ -535,6 +536,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
535536
zendesk: ZendeskIcon,
536537
zep: ZepIcon,
537538
zerobounce: ZeroBounceIcon,
539+
zoho_desk: ZohoDeskIcon,
538540
zoom: ZoomIcon,
539541
zoominfo: ZoomInfoIcon,
540542
}

apps/docs/content/docs/en/integrations/meta.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,8 @@
261261
"zendesk",
262262
"zep",
263263
"zerobounce",
264+
"zoho-desk-service-account",
265+
"zoho_desk",
264266
"zoom",
265267
"zoom-service-account",
266268
"zoominfo"

0 commit comments

Comments
 (0)