mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
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>
This commit is contained in:
co-authored by
Waleed Latif
parent
69289d26e7
commit
87aeca6f0c
@@ -2221,21 +2221,108 @@ export function EyeIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function ConfluenceIcon(props: SVGProps<SVGSVGElement>) {
|
||||
/**
|
||||
* Corporate Atlassian mark, used for family-wide Atlassian credentials — one
|
||||
* API token authenticates Jira, Jira Service Management, and Confluence, so no
|
||||
* single product mark represents it. Individual products keep their own icons.
|
||||
*/
|
||||
export function AtlassianIcon(props: SVGProps<SVGSVGElement>) {
|
||||
const id = useId()
|
||||
const gradientId = `atlassian_gradient_${id}`
|
||||
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
width='24'
|
||||
height='24'
|
||||
viewBox='0 3 21 24'
|
||||
/*
|
||||
* The mark's artwork spans ~66 units; the box is padded to 84.5 so it
|
||||
* fills ~78% of its viewBox, matching the inset Atlassian ships on the
|
||||
* Jira and Confluence marks (artwork 16→116 inside 128). Without the
|
||||
* padding this renders ~30% heavier than its siblings in the same tile.
|
||||
*/
|
||||
viewBox='-9.2 -8.9 84.5 84.5'
|
||||
focusable='false'
|
||||
fill='none'
|
||||
aria-hidden='true'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id={gradientId}
|
||||
gradientUnits='userSpaceOnUse'
|
||||
x1='28.536019'
|
||||
y1='35.528544'
|
||||
x2='11.406018'
|
||||
y2='65.208544'
|
||||
>
|
||||
<stop offset='0' stopColor='#0052cc' />
|
||||
<stop offset='0.92' stopColor='#2684ff' />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path
|
||||
fill='#1868DB'
|
||||
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'
|
||||
fill={`url(#${gradientId})`}
|
||||
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'
|
||||
/>
|
||||
<path
|
||||
fill='#2684ff'
|
||||
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'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function ConfluenceIcon(props: SVGProps<SVGSVGElement>) {
|
||||
const id = useId()
|
||||
const topGradientId = `confluence_top_${id}`
|
||||
const bottomGradientId = `confluence_bottom_${id}`
|
||||
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
width='24'
|
||||
height='24'
|
||||
viewBox='0 0 128 128'
|
||||
focusable='false'
|
||||
fill='none'
|
||||
aria-hidden='true'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id={bottomGradientId}
|
||||
gradientUnits='userSpaceOnUse'
|
||||
x1='26.791'
|
||||
y1='28.467'
|
||||
x2='11.792'
|
||||
y2='19.855'
|
||||
gradientTransform='scale(4)'
|
||||
>
|
||||
<stop offset='0' stopColor='#0052cc' />
|
||||
<stop offset='0.918' stopColor='#2380fb' />
|
||||
<stop offset='1' stopColor='#2684ff' />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id={topGradientId}
|
||||
gradientUnits='userSpaceOnUse'
|
||||
x1='5.209'
|
||||
y1='2.523'
|
||||
x2='20.208'
|
||||
y2='11.136'
|
||||
gradientTransform='scale(4)'
|
||||
>
|
||||
<stop offset='0' stopColor='#0052cc' />
|
||||
<stop offset='0.918' stopColor='#2380fb' />
|
||||
<stop offset='1' stopColor='#2684ff' />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path
|
||||
fill={`url(#${bottomGradientId})`}
|
||||
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'
|
||||
/>
|
||||
<path
|
||||
fill={`url(#${topGradientId})`}
|
||||
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'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
@@ -2768,19 +2855,58 @@ export function LinkupIcon(props: SVGProps<SVGSVGElement>) {
|
||||
}
|
||||
|
||||
export function JiraIcon(props: SVGProps<SVGSVGElement>) {
|
||||
const id = useId()
|
||||
const middleGradientId = `jira_middle_${id}`
|
||||
const bottomGradientId = `jira_bottom_${id}`
|
||||
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 30 30'
|
||||
viewBox='0 0 128 128'
|
||||
width='24'
|
||||
height='24'
|
||||
focusable='false'
|
||||
fill='none'
|
||||
aria-hidden='true'
|
||||
>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id={middleGradientId}
|
||||
gradientUnits='userSpaceOnUse'
|
||||
x1='22.034'
|
||||
y1='9.773'
|
||||
x2='17.118'
|
||||
y2='14.842'
|
||||
gradientTransform='scale(4)'
|
||||
>
|
||||
<stop offset='0.176' stopColor='#0052cc' />
|
||||
<stop offset='1' stopColor='#2684ff' />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id={bottomGradientId}
|
||||
gradientUnits='userSpaceOnUse'
|
||||
x1='16.641'
|
||||
y1='15.564'
|
||||
x2='10.957'
|
||||
y2='21.094'
|
||||
gradientTransform='scale(4)'
|
||||
>
|
||||
<stop offset='0.176' stopColor='#0052cc' />
|
||||
<stop offset='1' stopColor='#2684ff' />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path
|
||||
fill='#1868DB'
|
||||
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'
|
||||
fill='#2684ff'
|
||||
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'
|
||||
/>
|
||||
<path
|
||||
fill={`url(#${middleGradientId})`}
|
||||
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'
|
||||
/>
|
||||
<path
|
||||
fill={`url(#${bottomGradientId})`}
|
||||
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'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
@@ -7558,6 +7684,31 @@ export function SixtyfourIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The "sim" brand wordmark (v1.0 brand guide simLogotype paths — the same mark
|
||||
* the navbar/login header renders), inked with the theme-adaptive
|
||||
* `--text-body`. Used as the icon for the Auto model option; the wide viewBox
|
||||
* letterboxes itself inside square icon slots.
|
||||
*/
|
||||
export function SimAutoIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox='0 0 441 212'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
aria-hidden='true'
|
||||
>
|
||||
<g fill='var(--text-body)'>
|
||||
<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' />
|
||||
<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' />
|
||||
<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' />
|
||||
<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' />
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function SimTriggerIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
@@ -8776,3 +8927,20 @@ export function LogfireIcon(props: SVGProps<SVGSVGElement>) {
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function ZohoDeskIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox='0 0 24 24'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
aria-hidden='true'
|
||||
>
|
||||
<path
|
||||
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'
|
||||
fill='currentColor'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -248,6 +248,7 @@ import {
|
||||
ZendeskIcon,
|
||||
ZepIcon,
|
||||
ZeroBounceIcon,
|
||||
ZohoDeskIcon,
|
||||
ZoomIcon,
|
||||
ZoomInfoIcon,
|
||||
} from '@/components/icons'
|
||||
@@ -535,6 +536,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
zendesk: ZendeskIcon,
|
||||
zep: ZepIcon,
|
||||
zerobounce: ZeroBounceIcon,
|
||||
zoho_desk: ZohoDeskIcon,
|
||||
zoom: ZoomIcon,
|
||||
zoominfo: ZoomInfoIcon,
|
||||
}
|
||||
|
||||
@@ -261,6 +261,8 @@
|
||||
"zendesk",
|
||||
"zep",
|
||||
"zerobounce",
|
||||
"zoho-desk-service-account",
|
||||
"zoho_desk",
|
||||
"zoom",
|
||||
"zoom-service-account",
|
||||
"zoominfo"
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
---
|
||||
title: Zoho Desk Self Clients
|
||||
description: Set up a Zoho Self Client so your workflows can call Zoho Desk without a personal OAuth login
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps'
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
A Zoho **Self Client** is an OAuth client that has no redirect URL and no end user. Instead of sending someone through a consent screen, your workflows authenticate to Zoho Desk with the client's own ID and secret, and Sim mints a short-lived access token on demand — no user consent to expire, and no personal login that breaks when someone leaves the team.
|
||||
|
||||
This is the recommended way to use Zoho Desk in production workflows: the credential belongs to your organization rather than a person, the granted scopes are explicit, and tokens are minted fresh whenever a workflow runs.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
You need a Zoho account with access to the [Zoho API Console](https://api-console.zoho.com) for the same organization your Zoho Desk portal belongs to, and the Zoho Desk **organization ID** for that portal.
|
||||
|
||||
<Callout type="info">
|
||||
A Self Client can authenticate against the **US, EU, IN, or AU** accounts server — pick your region with the **Data center** field when you add the credential, or leave it blank for US. Organizations in the JP, CA, SA, CN, or UK data centers are not supported yet. API calls are then routed to the Desk host for that same region, so data residency is honored end to end.
|
||||
|
||||
The interactive **OAuth** connection is a separate path and remains **US-only** (`accounts.zoho.com`), so a non-US organization can connect only through a Self Client.
|
||||
</Callout>
|
||||
|
||||
<Callout type="warn">
|
||||
Zoho Desk **webhooks are a Professional-edition and above feature**. The Zoho Desk trigger in Sim provisions a webhook subscription, so it does not work on Free or Standard plans. The trigger also requires a personal **OAuth** connection rather than a Self Client — see [Triggers](#triggers-still-need-oauth) below.
|
||||
</Callout>
|
||||
|
||||
## Setting Up the Self Client
|
||||
|
||||
### 1. Create the Self Client
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
Sign in at [api-console.zoho.com](https://api-console.zoho.com) with the Zoho account that owns the Desk portal
|
||||
</Step>
|
||||
<Step>
|
||||
Click **Add Client**, choose **Self Client**, and click **Create** — then **OK** on the confirmation
|
||||
|
||||
{/* TODO(screenshot): Zoho API Console Add Client dialog with Self Client selected */}
|
||||
</Step>
|
||||
<Step>
|
||||
Open the new client and switch to the **Client Secret** tab. Copy the **Client ID** and **Client Secret** — these are two of the three values you'll paste into Sim
|
||||
|
||||
{/* TODO(screenshot): Self Client Client Secret tab showing Client ID and Client Secret */}
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Callout type="info">
|
||||
You do **not** need the **Generate Code** tab. That tab produces a one-time authorization code for the code-exchange flow; the client-credentials flow Sim uses needs only the client ID, the client secret, and your organization ID.
|
||||
</Callout>
|
||||
|
||||
### 2. Find Your Organization ID
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
Open Zoho Desk and go to **Setup** (the gear icon) → **Developer Space** → **API**
|
||||
</Step>
|
||||
<Step>
|
||||
Copy the numeric **Organization ID** (also called the Org ID or portal ID) shown there — for example `600123456`
|
||||
|
||||
{/* TODO(screenshot): Zoho Desk Setup > Developer Space > API showing the Organization ID */}
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Callout type="warn">
|
||||
This must be the organization ID of the Desk portal you want the workflows to act on. If your Zoho account has more than one Desk portal, using the wrong ID makes Zoho either reject the token request or issue a token scoped to the wrong portal.
|
||||
</Callout>
|
||||
|
||||
### 3. Know Your Data Center
|
||||
|
||||
Zoho hosts each organization in one data center, and the accounts server that issues tokens is per region. Look at the URL you use to sign in to Zoho Desk and pick the matching code:
|
||||
|
||||
| Data center | Sign-in domain | Code to enter |
|
||||
| --- | --- | --- |
|
||||
| United States | `zoho.com` | `us` (or leave blank) |
|
||||
| Europe | `zoho.eu` | `eu` |
|
||||
| India | `zoho.in` | `in` |
|
||||
| Australia | `zoho.com.au` | `au` |
|
||||
|
||||
Organizations in the JP, CA, SA, CN, and UK data centers cannot be connected yet.
|
||||
|
||||
### 4. Scopes
|
||||
|
||||
Sim requests exactly the scopes its Zoho Desk tools and trigger exercise:
|
||||
|
||||
```
|
||||
Desk.tickets.READ
|
||||
Desk.tickets.UPDATE
|
||||
Desk.contacts.READ
|
||||
Desk.agents.READ
|
||||
Desk.basic.READ
|
||||
Desk.webhooks.CREATE
|
||||
Desk.webhooks.DELETE
|
||||
aaaserver.profile.READ
|
||||
```
|
||||
|
||||
Sim sends this list on every token request, so there is nothing to pre-configure on the Self Client itself. If Zoho rejects the request with an invalid-scope error, the Self Client's owner does not have access to one of the Desk modules above in that organization.
|
||||
|
||||
A scope that is granted but insufficient surfaces at run time as a `4xx` from the Zoho Desk API naming the scope problem.
|
||||
|
||||
### 5. Protect the Client Secret
|
||||
|
||||
The client secret is bearer material for your Zoho Desk organization, limited only by the scopes above. Treat it like a password — do not commit it to source control or share it publicly. Sim encrypts it at rest.
|
||||
|
||||
<Callout type="info">
|
||||
Regenerating or revoking the Self Client in the Zoho API Console invalidates the stored pair immediately. If you rotate it, update the credential in Sim right away.
|
||||
</Callout>
|
||||
|
||||
## Adding the Self Client to Sim
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
Open **Integrations** from your workspace sidebar
|
||||
</Step>
|
||||
<Step>
|
||||
Search for "Zoho Desk" and open it, then click **Add to Sim** and choose **Add Self Client**
|
||||
|
||||
{/* TODO(screenshot): Zoho Desk integration page with the Add Self Client connect option */}
|
||||
</Step>
|
||||
<Step>
|
||||
In the **Add Zoho Desk Self Client** dialog, paste the **Client ID**, the **Client secret**, and the numeric **Organization ID**. Set **Data center** to your region (`us`, `eu`, `in`, or `au`) — leave it blank for US. Optionally set a display name and description
|
||||
|
||||
{/* TODO(screenshot): Add Zoho Desk Self Client dialog with all fields filled in */}
|
||||
</Step>
|
||||
<Step>
|
||||
Click **Add Self Client**. Sim verifies the credentials by minting a real access token from Zoho — if it fails, the error tells you whether Zoho rejected the credentials or couldn't be reached.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Using the Self Client in Workflows
|
||||
|
||||
Add a Zoho Desk block to your workflow. In the credential dropdown, your Self Client appears alongside any OAuth credentials. Select it and configure the block as you normally would.
|
||||
|
||||
{/* TODO(screenshot): Zoho Desk block in a workflow with the Self Client selected as the credential */}
|
||||
|
||||
The block calls the Zoho Desk REST API with a freshly minted access token — the same requests as the OAuth flow, so the Zoho Desk tools work the same way, subject to the scopes above.
|
||||
|
||||
One exception is worth knowing: Zoho publishes no OAuth scope for the attachment download sub-path, so **Get Attachment** is the one operation whose scope requirement we could not confirm from Zoho's documentation. If it returns a scope error, the requested scope list needs widening.
|
||||
|
||||
### Triggers still need OAuth
|
||||
|
||||
The Zoho Desk **trigger** provisions and tears down its own webhook subscription against your Desk organization, and that provisioning path currently runs only against a personal OAuth connection. Connect a Zoho Desk account through OAuth for triggers, and use the Self Client for the blocks that read and update tickets.
|
||||
|
||||
Because the OAuth flow is US-only, an organization outside the US data center can use Zoho Desk blocks through a Self Client but cannot use the Zoho Desk trigger.
|
||||
|
||||
## Token Behavior
|
||||
|
||||
Access tokens minted from a Self Client live for one hour and there is **no refresh token** — Sim mints a new token whenever one is needed, and caches the current one until it is close to expiry. Two events invalidate the stored credential:
|
||||
|
||||
- **Revoking or deleting the Self Client** in the Zoho API Console — no new tokens can be minted
|
||||
- **Regenerating the client secret** — the stored pair stops working; paste the new secret into the credential in Sim
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "Why a Self Client instead of OAuth?", answer: "A Self Client authenticates as your Zoho organization, not as a person — nothing expires when someone leaves or their login lapses. Sim mints short-lived tokens from the stored client ID and secret whenever a workflow runs." },
|
||||
{ question: "Where do I find the Organization ID?", answer: "In Zoho Desk, go to Setup (gear icon) → Developer Space → API. The numeric Organization ID shown there is the value to paste. This is expected to be the same ID that Zoho Desk API calls send in the orgId header; if Zoho rejects it with missing_org_info, paste the full ZohoDesk.<your-org-id> value instead." },
|
||||
{ question: "Zoho rejects my credentials with invalid_client — why?", answer: "Either the client ID or secret was mistyped, or the client you created is not a Self Client. Only Self Clients support the client-credentials grant — in the Zoho API Console, Add Client → Self Client. Copy both values from the client's Client Secret tab." },
|
||||
{ question: "Zoho returns missing_org_info or rejects the organization — why?", answer: "Zoho could not resolve a Desk organization from the ID you pasted. Re-copy the numeric Organization ID from Setup → Developer Space → API in the Desk portal you want to use. If your Zoho account has multiple Desk portals, make sure it is the ID of the right one." },
|
||||
{ question: "Can I use a Self Client with a non-US Zoho account?", answer: "Yes, for the US, EU, IN, and AU data centers. Set the Data center field to us, eu, in, or au when you add the credential, and Sim mints tokens against that region's accounts server and calls the Desk host in the same region. Leaving it blank means US. The JP, CA, SA, CN, and UK data centers are not supported yet, and the interactive OAuth connection remains US-only." },
|
||||
{ question: "I picked the wrong data center — what happens?", answer: "The region's accounts server does not know your organization, so Zoho rejects the token request and Sim reports that it could not authenticate. Edit the credential and set the Data center to the region whose domain you sign in to Zoho Desk with." },
|
||||
{ question: "Why doesn't my Zoho Desk trigger work with the Self Client?", answer: "The trigger provisions a webhook subscription in your Desk organization, and that path runs against a personal OAuth connection only. Connect Zoho Desk through OAuth for triggers. Separately, Zoho Desk webhooks require a Professional-edition plan or above — they are unavailable on Free and Standard." },
|
||||
{ question: "How do I rotate the credentials?", answer: "Regenerate the client secret on the Self Client's Client Secret tab in the Zoho API Console, then update the credential in Sim with the new secret. The old secret stops working as soon as it's regenerated, so update Sim promptly." },
|
||||
]} />
|
||||
@@ -0,0 +1,515 @@
|
||||
---
|
||||
title: Zoho Desk
|
||||
description: Manage Zoho Desk tickets, comments, threads, and contacts
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="zoho_desk"
|
||||
color="#E42527"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Zoho Desk](https://www.zoho.com/desk/) is Zoho's customer support help desk. Support teams use it to receive tickets from email, web forms, chat, phone, and social channels, route them to the right department and agent, and track every customer conversation through to resolution.
|
||||
|
||||
With the Sim Zoho Desk integration, you can:
|
||||
|
||||
- **Read and filter tickets**: List tickets across an organization filtered by department, status, or priority, or fetch a single ticket by ID with its related contact, assignee, and department.
|
||||
- **Update tickets**: Change subject, status, priority, assignee, department, category, due date, and custom fields — useful for AI triage that classifies an incoming ticket and writes the result back.
|
||||
- **Work with conversations**: List and read ticket threads (the customer-facing email/chat exchange) and comments (internal agent notes), then add your own comment as public or private.
|
||||
- **Look up contacts**: Retrieve the contact behind a ticket to enrich it with data from your CRM or knowledge base.
|
||||
- **Download attachments**: Pull an attachment from a thread or comment into a Sim file you can pass to downstream blocks.
|
||||
- **Trigger on events**: Start a workflow when a ticket, comment, thread, contact, agent, task, or article changes in Zoho Desk.
|
||||
|
||||
**How it works in Sim:**
|
||||
Add a Zoho Desk block to your workflow, connect your Zoho account, and pick the Organization (portal) to work in — Sim loads the list for you from the connected account. Choose an operation and fill in its parameters; the block calls the Zoho Desk API and returns structured data for downstream blocks. For comment and thread bodies, Sim adds a derived plain-text `contentText` field alongside Zoho's raw HTML `content`, so an AI agent can read the message without HTML markup.
|
||||
|
||||
To trigger on Zoho Desk activity instead, use the block's trigger mode. Sim creates the webhook subscription in Zoho Desk for you and removes it automatically when the workflow is undeployed.
|
||||
|
||||
**Requirements and limitations**
|
||||
|
||||
> Zoho Desk webhooks require a Zoho Desk edition of **Professional or higher** — Free and Standard plans cannot create webhook subscriptions, so the trigger will fail to deploy on those plans.
|
||||
>
|
||||
> Connecting a Zoho account with **OAuth** — which the trigger requires — works only for the **US data center** (`accounts.zoho.com`). To use Zoho Desk blocks from the EU, India, or Australia data centers, connect a [Self Client](/integrations/zoho-desk-service-account) instead and set its data center. The Japan, Canada, Saudi Arabia, China, and UK data centers are not supported by either path.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Read and update Zoho Desk tickets, manage comments and threads, look up contacts, and download attachments. Can also trigger workflows from Zoho Desk webhook events.
|
||||
|
||||
|
||||
|
||||
## Actions
|
||||
|
||||
### `zoho_desk_list_tickets`
|
||||
|
||||
List tickets from a Zoho Desk organization with optional filters. Returns a list projection: description, resolution, statusType and classification are only available from Get Ticket.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiDomain` | string | No | Zoho Desk data-center REST base URL |
|
||||
| `orgId` | string | Yes | Zoho Desk organization ID |
|
||||
| `from` | number | No | Pagination start index \(0-based, max 4999\) |
|
||||
| `limit` | number | No | Number of tickets to return \(1-100, default 10\) |
|
||||
| `departmentIds` | string | No | Filter by department ID \(comma-separated for multiple\) |
|
||||
| `status` | string | No | Filter by status, including custom statuses. Comma-separate to match multiple \(e.g. "Open,On Hold"\) |
|
||||
| `priority` | string | No | Filter by priority. Comma-separate to match multiple \(e.g. "High,Urgent"\) |
|
||||
| `sortBy` | string | No | Sort field: createdTime, customerResponseTime, or responseDueDate. Prefix with - for descending. |
|
||||
| `include` | string | No | Comma-separated related data to embed. Allowed: contacts, products, departments, team, isRead, assignee |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `tickets` | array | List of tickets |
|
||||
| ↳ `id` | string | Ticket ID |
|
||||
| ↳ `ticketNumber` | string | Human-readable ticket number |
|
||||
| ↳ `subject` | string | Ticket subject |
|
||||
| ↳ `description` | string | Ticket description \(raw; may be HTML\) |
|
||||
| ↳ `descriptionText` | string | Plain-text rendering of the description: HTML stripped when the body contains markup, otherwise the description verbatim |
|
||||
| ↳ `status` | string | Ticket status |
|
||||
| ↳ `statusType` | string | Status category \(Open/Closed/On Hold\) |
|
||||
| ↳ `priority` | string | Ticket priority |
|
||||
| ↳ `category` | string | Ticket category |
|
||||
| ↳ `subCategory` | string | Ticket sub-category |
|
||||
| ↳ `classification` | string | Ticket classification |
|
||||
| ↳ `channel` | string | Origin channel |
|
||||
| ↳ `departmentId` | string | Department ID |
|
||||
| ↳ `contactId` | string | Contact ID |
|
||||
| ↳ `accountId` | string | Account ID |
|
||||
| ↳ `assigneeId` | string | Assignee ID |
|
||||
| ↳ `email` | string | Contact email |
|
||||
| ↳ `phone` | string | Contact phone |
|
||||
| ↳ `dueDate` | string | Due date |
|
||||
| ↳ `responseDueDate` | string | Response due date |
|
||||
| ↳ `createdTime` | string | Created timestamp |
|
||||
| ↳ `modifiedTime` | string | Last modified timestamp |
|
||||
| ↳ `closedTime` | string | Closed timestamp |
|
||||
| ↳ `resolution` | string | Resolution text |
|
||||
| ↳ `threadCount` | string | Number of threads |
|
||||
| ↳ `commentCount` | string | Number of comments |
|
||||
| ↳ `webUrl` | string | Web URL to the ticket |
|
||||
| ↳ `isEscalated` | boolean | Whether the ticket is escalated |
|
||||
| ↳ `isOverDue` | boolean | Whether the ticket is overdue |
|
||||
| ↳ `isSpam` | boolean | Whether the ticket is marked spam |
|
||||
| ↳ `cf` | json | Custom field values, keyed by custom field API name |
|
||||
| `count` | number | Number of tickets returned |
|
||||
|
||||
### `zoho_desk_get_ticket`
|
||||
|
||||
Retrieve a single Zoho Desk ticket by ID.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiDomain` | string | No | Zoho Desk data-center REST base URL |
|
||||
| `orgId` | string | Yes | Zoho Desk organization ID |
|
||||
| `ticketId` | string | Yes | Ticket ID to retrieve |
|
||||
| `include` | string | No | Comma-separated related data to embed. Allowed: contacts, products, assignee, departments, contract, isRead, team, skills |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ticket` | object | The ticket |
|
||||
| ↳ `id` | string | Ticket ID |
|
||||
| ↳ `ticketNumber` | string | Human-readable ticket number |
|
||||
| ↳ `subject` | string | Ticket subject |
|
||||
| ↳ `description` | string | Ticket description \(raw; may be HTML\) |
|
||||
| ↳ `descriptionText` | string | Plain-text rendering of the description: HTML stripped when the body contains markup, otherwise the description verbatim |
|
||||
| ↳ `status` | string | Ticket status |
|
||||
| ↳ `statusType` | string | Status category \(Open/Closed/On Hold\) |
|
||||
| ↳ `priority` | string | Ticket priority |
|
||||
| ↳ `category` | string | Ticket category |
|
||||
| ↳ `subCategory` | string | Ticket sub-category |
|
||||
| ↳ `classification` | string | Ticket classification |
|
||||
| ↳ `channel` | string | Origin channel |
|
||||
| ↳ `departmentId` | string | Department ID |
|
||||
| ↳ `contactId` | string | Contact ID |
|
||||
| ↳ `accountId` | string | Account ID |
|
||||
| ↳ `assigneeId` | string | Assignee ID |
|
||||
| ↳ `email` | string | Contact email |
|
||||
| ↳ `phone` | string | Contact phone |
|
||||
| ↳ `dueDate` | string | Due date |
|
||||
| ↳ `responseDueDate` | string | Response due date |
|
||||
| ↳ `createdTime` | string | Created timestamp |
|
||||
| ↳ `modifiedTime` | string | Last modified timestamp |
|
||||
| ↳ `closedTime` | string | Closed timestamp |
|
||||
| ↳ `resolution` | string | Resolution text |
|
||||
| ↳ `threadCount` | string | Number of threads |
|
||||
| ↳ `commentCount` | string | Number of comments |
|
||||
| ↳ `webUrl` | string | Web URL to the ticket |
|
||||
| ↳ `isEscalated` | boolean | Whether the ticket is escalated |
|
||||
| ↳ `isOverDue` | boolean | Whether the ticket is overdue |
|
||||
| ↳ `isSpam` | boolean | Whether the ticket is marked spam |
|
||||
| ↳ `cf` | json | Custom field values, keyed by custom field API name |
|
||||
|
||||
### `zoho_desk_update_ticket`
|
||||
|
||||
Update fields on an existing Zoho Desk ticket.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiDomain` | string | No | Zoho Desk data-center REST base URL |
|
||||
| `orgId` | string | Yes | Zoho Desk organization ID |
|
||||
| `ticketId` | string | Yes | Ticket ID to update |
|
||||
| `subject` | string | No | Ticket subject |
|
||||
| `status` | string | No | Ticket status \(e.g. Open, Closed\) |
|
||||
| `priority` | string | No | Ticket priority \(e.g. High\) |
|
||||
| `assigneeId` | string | No | Assignee \(agent\) ID |
|
||||
| `departmentId` | string | No | Department ID |
|
||||
| `category` | string | No | Ticket category |
|
||||
| `subCategory` | string | No | Ticket sub-category |
|
||||
| `dueDate` | string | No | Due date \(ISO 8601\) |
|
||||
| `description` | string | No | Ticket description |
|
||||
| `resolution` | string | No | Resolution notes recorded on the ticket |
|
||||
| `classification` | string | No | Ticket classification: Problem, Request, Question, or Others |
|
||||
| `customFields` | json | No | Custom field values as a JSON object, keyed by custom field API name |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ticket` | object | The updated ticket |
|
||||
| ↳ `id` | string | Ticket ID |
|
||||
| ↳ `ticketNumber` | string | Human-readable ticket number |
|
||||
| ↳ `subject` | string | Ticket subject |
|
||||
| ↳ `description` | string | Ticket description \(raw; may be HTML\) |
|
||||
| ↳ `descriptionText` | string | Plain-text rendering of the description: HTML stripped when the body contains markup, otherwise the description verbatim |
|
||||
| ↳ `status` | string | Ticket status |
|
||||
| ↳ `statusType` | string | Status category \(Open/Closed/On Hold\) |
|
||||
| ↳ `priority` | string | Ticket priority |
|
||||
| ↳ `category` | string | Ticket category |
|
||||
| ↳ `subCategory` | string | Ticket sub-category |
|
||||
| ↳ `classification` | string | Ticket classification |
|
||||
| ↳ `channel` | string | Origin channel |
|
||||
| ↳ `departmentId` | string | Department ID |
|
||||
| ↳ `contactId` | string | Contact ID |
|
||||
| ↳ `accountId` | string | Account ID |
|
||||
| ↳ `assigneeId` | string | Assignee ID |
|
||||
| ↳ `email` | string | Contact email |
|
||||
| ↳ `phone` | string | Contact phone |
|
||||
| ↳ `dueDate` | string | Due date |
|
||||
| ↳ `responseDueDate` | string | Response due date |
|
||||
| ↳ `createdTime` | string | Created timestamp |
|
||||
| ↳ `modifiedTime` | string | Last modified timestamp |
|
||||
| ↳ `closedTime` | string | Closed timestamp |
|
||||
| ↳ `resolution` | string | Resolution text |
|
||||
| ↳ `threadCount` | string | Number of threads |
|
||||
| ↳ `commentCount` | string | Number of comments |
|
||||
| ↳ `webUrl` | string | Web URL to the ticket |
|
||||
| ↳ `isEscalated` | boolean | Whether the ticket is escalated |
|
||||
| ↳ `isOverDue` | boolean | Whether the ticket is overdue |
|
||||
| ↳ `isSpam` | boolean | Whether the ticket is marked spam |
|
||||
| ↳ `cf` | json | Custom field values, keyed by custom field API name |
|
||||
|
||||
### `zoho_desk_list_comments`
|
||||
|
||||
List comments on a Zoho Desk ticket.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiDomain` | string | No | Zoho Desk data-center REST base URL |
|
||||
| `orgId` | string | Yes | Zoho Desk organization ID |
|
||||
| `ticketId` | string | Yes | Ticket ID |
|
||||
| `from` | number | No | Pagination start index \(0-based\) |
|
||||
| `limit` | number | No | Number of comments to return \(1-100, default 50\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `comments` | array | List of comments |
|
||||
| ↳ `id` | string | Comment ID |
|
||||
| ↳ `content` | string | Comment content \(raw; may be HTML\) |
|
||||
| ↳ `contentType` | string | Content type \(plainText/html\) |
|
||||
| ↳ `contentText` | string | Plain-text rendering of content \(HTML stripped when contentType is html\) |
|
||||
| ↳ `isPublic` | boolean | Whether the comment is public |
|
||||
| ↳ `commenterId` | string | Commenter ID |
|
||||
| ↳ `commenter` | object | Who wrote the comment |
|
||||
| ↳ `name` | string | Display name |
|
||||
| ↳ `firstName` | string | First name |
|
||||
| ↳ `lastName` | string | Last name |
|
||||
| ↳ `email` | string | Email address |
|
||||
| ↳ `type` | string | Commenter type \(AGENT/END_USER\) |
|
||||
| ↳ `roleName` | string | Role name |
|
||||
| ↳ `photoURL` | string | Avatar URL |
|
||||
| ↳ `commentedTime` | string | Commented timestamp |
|
||||
| ↳ `modifiedTime` | string | Modified timestamp |
|
||||
| ↳ `attachments` | array | Comment attachments |
|
||||
| ↳ `id` | string | Attachment ID |
|
||||
| ↳ `name` | string | File name |
|
||||
| ↳ `size` | string | File size as reported by Zoho |
|
||||
| ↳ `href` | string | Download href |
|
||||
| `count` | number | Number of comments returned |
|
||||
|
||||
### `zoho_desk_add_comment`
|
||||
|
||||
Add a comment to a Zoho Desk ticket.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiDomain` | string | No | Zoho Desk data-center REST base URL |
|
||||
| `orgId` | string | Yes | Zoho Desk organization ID |
|
||||
| `ticketId` | string | Yes | Ticket ID |
|
||||
| `content` | string | Yes | Comment content |
|
||||
| `contentType` | string | No | Content type: plainText or html. Defaults to plainText so agent-written text posts literally; pass 'html' to send markup \(Zoho's own API default is html\). |
|
||||
| `isPublic` | boolean | No | Whether the comment is public |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `comment` | object | The created comment |
|
||||
| ↳ `id` | string | Comment ID |
|
||||
| ↳ `content` | string | Comment content \(raw; may be HTML\) |
|
||||
| ↳ `contentType` | string | Content type \(plainText/html\) |
|
||||
| ↳ `contentText` | string | Plain-text rendering of content \(HTML stripped when contentType is html\) |
|
||||
| ↳ `isPublic` | boolean | Whether the comment is public |
|
||||
| ↳ `commenterId` | string | Commenter ID |
|
||||
| ↳ `commenter` | object | Who wrote the comment |
|
||||
| ↳ `name` | string | Display name |
|
||||
| ↳ `firstName` | string | First name |
|
||||
| ↳ `lastName` | string | Last name |
|
||||
| ↳ `email` | string | Email address |
|
||||
| ↳ `type` | string | Commenter type \(AGENT/END_USER\) |
|
||||
| ↳ `roleName` | string | Role name |
|
||||
| ↳ `photoURL` | string | Avatar URL |
|
||||
| ↳ `commentedTime` | string | Commented timestamp |
|
||||
| ↳ `modifiedTime` | string | Modified timestamp |
|
||||
| ↳ `attachments` | array | Comment attachments |
|
||||
| ↳ `id` | string | Attachment ID |
|
||||
| ↳ `name` | string | File name |
|
||||
| ↳ `size` | string | File size as reported by Zoho |
|
||||
| ↳ `href` | string | Download href |
|
||||
|
||||
### `zoho_desk_list_threads`
|
||||
|
||||
List conversation threads on a Zoho Desk ticket, newest first (Zoho sorts by sendDateTime descending by default). Returns a list projection: message bodies (content, summary, to/cc/bcc) come back only from Get Thread.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiDomain` | string | No | Zoho Desk data-center REST base URL |
|
||||
| `orgId` | string | Yes | Zoho Desk organization ID |
|
||||
| `ticketId` | string | Yes | Ticket ID |
|
||||
| `from` | number | No | Pagination start index \(0-based\) |
|
||||
| `limit` | number | No | Number of threads to return \(1-200, default 100\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `threads` | array | List of threads |
|
||||
| ↳ `id` | string | Thread ID |
|
||||
| ↳ `channel` | string | Thread channel |
|
||||
| ↳ `direction` | string | Direction \(in/out\) |
|
||||
| ↳ `content` | string | Thread content \(raw; may be HTML\) |
|
||||
| ↳ `contentType` | string | Content type |
|
||||
| ↳ `contentText` | string | Plain-text rendering of content \(HTML stripped when contentType is html\) |
|
||||
| ↳ `summary` | string | Thread summary |
|
||||
| ↳ `responderId` | string | Responder ID |
|
||||
| ↳ `createdTime` | string | Created timestamp |
|
||||
| ↳ `hasAttach` | boolean | Whether the thread has attachments |
|
||||
| ↳ `attachmentCount` | string | Number of attachments |
|
||||
| ↳ `fromEmailAddress` | string | From email address |
|
||||
| ↳ `to` | string | To email address |
|
||||
| ↳ `cc` | string | CC email address |
|
||||
| ↳ `bcc` | string | BCC email address |
|
||||
| ↳ `replyTo` | string | Reply-to email address |
|
||||
| ↳ `isForward` | boolean | Whether the thread is a forward |
|
||||
| ↳ `isContentTruncated` | boolean | Whether Zoho truncated the thread content; fetch fullContentURL for the rest |
|
||||
| ↳ `fullContentURL` | string | URL returning the untruncated thread content |
|
||||
| ↳ `plainText` | string | Zoho's own plain-text rendering of the thread, when it supplies one |
|
||||
| ↳ `status` | string | Delivery status of an outgoing thread \(SUCCESS/FAILED/DRAFT\) |
|
||||
| ↳ `isDescriptionThread` | boolean | Whether this thread is the ticket's original description |
|
||||
| ↳ `visibility` | string | Thread visibility \(e.g. public\) |
|
||||
| ↳ `canReply` | boolean | Whether the thread can be replied to |
|
||||
| ↳ `author` | object | Who sent the thread |
|
||||
| ↳ `name` | string | Display name |
|
||||
| ↳ `firstName` | string | First name |
|
||||
| ↳ `lastName` | string | Last name |
|
||||
| ↳ `email` | string | Email address |
|
||||
| ↳ `type` | string | Author type \(AGENT/END_USER\) |
|
||||
| ↳ `photoURL` | string | Avatar URL |
|
||||
| ↳ `attachments` | array | Thread attachments |
|
||||
| ↳ `id` | string | Attachment ID |
|
||||
| ↳ `name` | string | File name |
|
||||
| ↳ `size` | string | File size as reported by Zoho |
|
||||
| ↳ `href` | string | Download href |
|
||||
| `count` | number | Number of threads returned |
|
||||
|
||||
### `zoho_desk_get_thread`
|
||||
|
||||
Retrieve the full content of a single Zoho Desk ticket thread.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiDomain` | string | No | Zoho Desk data-center REST base URL |
|
||||
| `orgId` | string | Yes | Zoho Desk organization ID |
|
||||
| `ticketId` | string | Yes | Ticket ID |
|
||||
| `threadId` | string | Yes | Thread ID |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `thread` | object | The thread |
|
||||
| ↳ `id` | string | Thread ID |
|
||||
| ↳ `channel` | string | Thread channel |
|
||||
| ↳ `direction` | string | Direction \(in/out\) |
|
||||
| ↳ `content` | string | Thread content \(raw; may be HTML\) |
|
||||
| ↳ `contentType` | string | Content type |
|
||||
| ↳ `contentText` | string | Plain-text rendering of content \(HTML stripped when contentType is html\) |
|
||||
| ↳ `summary` | string | Thread summary |
|
||||
| ↳ `responderId` | string | Responder ID |
|
||||
| ↳ `createdTime` | string | Created timestamp |
|
||||
| ↳ `hasAttach` | boolean | Whether the thread has attachments |
|
||||
| ↳ `attachmentCount` | string | Number of attachments |
|
||||
| ↳ `fromEmailAddress` | string | From email address |
|
||||
| ↳ `to` | string | To email address |
|
||||
| ↳ `cc` | string | CC email address |
|
||||
| ↳ `bcc` | string | BCC email address |
|
||||
| ↳ `replyTo` | string | Reply-to email address |
|
||||
| ↳ `isForward` | boolean | Whether the thread is a forward |
|
||||
| ↳ `isContentTruncated` | boolean | Whether Zoho truncated the thread content; fetch fullContentURL for the rest |
|
||||
| ↳ `fullContentURL` | string | URL returning the untruncated thread content |
|
||||
| ↳ `plainText` | string | Zoho's own plain-text rendering of the thread, when it supplies one |
|
||||
| ↳ `status` | string | Delivery status of an outgoing thread \(SUCCESS/FAILED/DRAFT\) |
|
||||
| ↳ `isDescriptionThread` | boolean | Whether this thread is the ticket's original description |
|
||||
| ↳ `visibility` | string | Thread visibility \(e.g. public\) |
|
||||
| ↳ `canReply` | boolean | Whether the thread can be replied to |
|
||||
| ↳ `author` | object | Who sent the thread |
|
||||
| ↳ `name` | string | Display name |
|
||||
| ↳ `firstName` | string | First name |
|
||||
| ↳ `lastName` | string | Last name |
|
||||
| ↳ `email` | string | Email address |
|
||||
| ↳ `type` | string | Author type \(AGENT/END_USER\) |
|
||||
| ↳ `photoURL` | string | Avatar URL |
|
||||
| ↳ `attachments` | array | Thread attachments |
|
||||
| ↳ `id` | string | Attachment ID |
|
||||
| ↳ `name` | string | File name |
|
||||
| ↳ `size` | string | File size as reported by Zoho |
|
||||
| ↳ `href` | string | Download href |
|
||||
|
||||
### `zoho_desk_get_contact`
|
||||
|
||||
Retrieve a Zoho Desk contact by ID.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiDomain` | string | No | Zoho Desk data-center REST base URL |
|
||||
| `orgId` | string | Yes | Zoho Desk organization ID |
|
||||
| `contactId` | string | Yes | Contact ID to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `contact` | object | The contact |
|
||||
| ↳ `id` | string | Contact ID |
|
||||
| ↳ `firstName` | string | First name |
|
||||
| ↳ `lastName` | string | Last name |
|
||||
| ↳ `email` | string | Primary email |
|
||||
| ↳ `secondaryEmail` | string | Secondary email |
|
||||
| ↳ `phone` | string | Phone number |
|
||||
| ↳ `mobile` | string | Mobile number |
|
||||
| ↳ `accountId` | string | Associated account ID |
|
||||
| ↳ `ownerId` | string | Owner ID |
|
||||
| ↳ `type` | string | Contact type |
|
||||
| ↳ `title` | string | Job title |
|
||||
| ↳ `street` | string | Street |
|
||||
| ↳ `city` | string | City |
|
||||
| ↳ `state` | string | State |
|
||||
| ↳ `country` | string | Country |
|
||||
| ↳ `zip` | string | ZIP / postal code |
|
||||
| ↳ `description` | string | Description |
|
||||
| ↳ `cf` | json | Custom field values, keyed by custom field API name |
|
||||
|
||||
### `zoho_desk_get_attachment`
|
||||
|
||||
Download a Zoho Desk ticket attachment (from its href) as a file.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiDomain` | string | No | Zoho Desk data-center REST base URL |
|
||||
| `orgId` | string | Yes | Zoho Desk organization ID |
|
||||
| `href` | string | Yes | Attachment download href \(from a thread or comment attachment\) |
|
||||
| `fileName` | string | No | Optional file name for the downloaded file |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `file` | file | The downloaded attachment file |
|
||||
|
||||
### `zoho_desk_list_organizations`
|
||||
|
||||
List the Zoho Desk organizations (portals) the connected account can access.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiDomain` | string | No | Zoho Desk data-center REST base URL |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `organizations` | array | Accessible organizations |
|
||||
| ↳ `id` | string | Organization ID |
|
||||
| ↳ `companyName` | string | Company name |
|
||||
| ↳ `portalName` | string | Portal name |
|
||||
| `count` | number | Number of organizations returned |
|
||||
|
||||
|
||||
|
||||
## Triggers
|
||||
|
||||
A **Trigger** is a block that starts a workflow when an event happens in this service.
|
||||
|
||||
### Zoho Desk Event
|
||||
|
||||
Trigger a workflow when a Zoho Desk event occurs (ticket, comment, thread, contact, agent, task, or article changes).
|
||||
|
||||
#### Configuration
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `triggerCredentials` | string | Yes | This trigger creates and manages a webhook subscription in your Zoho Desk account. |
|
||||
| `orgId` | project-selector | Yes | The Zoho Desk organization \(portal\) to subscribe in. |
|
||||
| `manualOrgId` | string | Yes | Type an organization ID instead of picking one from the list. |
|
||||
| `eventType` | string | Yes | Event |
|
||||
| `triggerDepartmentIds` | string | No | Restrict events to these departments. Leave empty for all departments. |
|
||||
| `fields` | string | No | For Ticket Updated: only fire when one of these fields changes \(max 5\). Previous values are included in the payload. |
|
||||
| `direction` | string | No | Thread Direction |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `eventType` | string | The Zoho Desk event type \(e.g. Ticket_Add\) |
|
||||
| `eventTime` | string | Event time in milliseconds since epoch |
|
||||
| `orgId` | string | Zoho Desk organization ID |
|
||||
| `payload` | json | The full resource that changed \(ticket, comment, thread, etc.\). Comment and thread events gain a derived plain-text `contentText` alongside the raw `content` + `contentType`; ticket events gain `descriptionText` alongside `description`. |
|
||||
| `prevState` | json | Previous state of the resource \(update events only\) |
|
||||
|
||||
Reference in New Issue
Block a user