Files
zooqueenandHanzo Dev f717dd89ad keys: the key a user pastes is sk-, not the retired hk-
28 apps told the user to paste an `hk-` key โ€” input placeholders, settings
help text, READMEs, the error thrown when the field is empty โ€” and five of
them validated that paste against /^hk-/ before sending it as a bearer.
Nothing mints that prefix any more: cloud accepts pk- and sk-, IAM mints
sk-. So the guard admitted a credential that authenticates nothing, and the
panel failed later at the gateway with nothing on screen to explain it.

The three leak detectors keep hk- and gain sk-. Salesforce's source scan
and the two Zendesk tests exist to catch a key committed to source or
echoed back through a request; retired keys are still in circulation and
leak exactly as badly, while a detector that only knew hk- would have gone
blind to every key minted from here on.

`sk-` used to be the example of the foreign key the guard screened out โ€” an
OpenAI key. It cannot mean that now, and shape alone cannot tell one
vendor's sk- from another's, so those comments say what the check really
proves and point at the /v1/models call that proves the rest. The three
tests that pinned "sk- is rejected" now pin "hk- is rejected", which is the
invariant that survived.

Verified: 2029 tests pass across the 28 packages; typecheck clean on 25 of
them (hubspot, jupyter and teams fail identically at HEAD on JSX and ESM
type conflicts in files this does not touch); salesforce validate-source
green.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 18:07:16 -07:00

6.3 KiB
Raw Permalink Blame History

@hanzo/zendesk โ€” Hanzo AI for Zendesk

A Zendesk Support app (ZAF v2, ticket_sidebar) that puts Hanzo AI next to every ticket. It reads the current ticket and its conversation, and gives the agent one-click actions โ€” Summarize ticket, Draft reply (in the agent's voice), Suggest next steps, Explain the issue, and a freeform Ask โ€” then writes the result back into the reply editor or adds it as an internal note.

Built on the published @hanzo/ai headless client against api.hanzo.ai (/v1/...). No UI framework โ€” a plain HTML/TypeScript iframe panel bundled with esbuild.

How it works

Zendesk ticket_sidebar iframe
        โ”‚
   src/panel.ts โ”€โ”€ DOM glue only
        โ”œโ”€โ”€ src/zaf.ts     read ticket context ยท write back ยท secure-proxy fetch
        โ”œโ”€โ”€ src/ticket.ts  PURE: conversation โ†’ windowed text (+ honest scope note)
        โ””โ”€โ”€ src/hanzo.ts   PURE shaping + @hanzo/ai createAiClient (ONE model path)
        โ”‚
   api.hanzo.ai /v1/chat/completions   (via the Zendesk proxy โ€” see "Security")

Ticket context โ†’ model

zaf.getTicketContext reads ticket.subject, ticket.requester.name, and ticket.comments in one batched client.get([...]). ticket.buildContext then windows the conversation to a character budget (48k), keeping the newest comments (a support reply is grounded in the latest exchange) and dropping the oldest first โ€” always including at least the most recent comment, hard-cut if it alone is over budget. Each comment is labelled with its author and whether it was a public reply or an internal note. ticket.contextNote prepends one honest sentence so the model (and the agent) knows whether older context was omitted โ€” we never claim the whole thread was sent when it wasn't.

Reply write-back

The output isn't inserted automatically. The agent clicks:

  • Insert as reply โ†’ client.set('ticket.comment.type', 'publicReply') then client.invoke('ticket.comment.appendText', text) โ€” appends to the reply editor without clobbering what the agent already typed.
  • Add as note โ†’ the same, with type: 'internalNote'.

Security โ€” the Hanzo key never reaches the browser

The Hanzo API key (sk-โ€ฆ) is a secure installation setting (manifest.json โ†’ parameters[hanzo_api_key], "secure": true, "type": "password"). Zendesk stores secure settings server-side and does not expose them to the iframe โ€” so the panel can't (and doesn't) read the key.

Instead, model traffic is sent through Zendesk's request proxy: zaf.secureFetch implements a fetch that calls client.request(...) with secure: true and an Authorization: Bearer {{setting.hanzo_api_key}} header. Zendesk substitutes the real key on its server, out of reach of the browser. We pass that fetch to createAiClient({ baseUrl: 'https://api.hanzo.ai', fetch }), so we use the published @hanzo/ai client while the secret stays secure. No key is ever stored in localStorage, committed, or logged.

The optional Default model (default_model) is a non-secure setting, so the panel reads it from client.metadata().settings and falls back to zen5.

Build

pnpm install --filter @hanzo/zendesk
pnpm --filter @hanzo/zendesk build      # โ†’ dist/  (ZAF app bundle)
pnpm --filter @hanzo/zendesk test       # vitest
pnpm --filter @hanzo/zendesk typecheck  # tsc --noEmit

build.js (esbuild + Node stdlib) produces the bundle zcli expects:

dist/
โ”œโ”€โ”€ manifest.json
โ”œโ”€โ”€ translations/en.json
โ””โ”€โ”€ assets/
    โ”œโ”€โ”€ iframe.html      the ticket_sidebar page (manifest location url = assets/iframe.html)
    โ”œโ”€โ”€ panel.js         bundled from src/panel.ts (ESM, @hanzo/ai included)
    โ”œโ”€โ”€ styles.css
    โ”œโ”€โ”€ logo.png         128px Marketplace icon
    โ””โ”€โ”€ logo-small.png   in-panel icon

The ZAF SDK is not bundled โ€” iframe.html loads it from Zendesk's CDN (https://static.zdassets.com/zendesk_app_framework_sdk/2.0/zaf_sdk.min.js), which exposes the ZAFClient global.

Validate, package, and upload (zcli)

Uses @zendesk/zcli (a devDep).

# 1. Authenticate once against your Zendesk account.
npx zcli login -i
#   (or set ZENDESK_SUBDOMAIN, ZENDESK_EMAIL, ZENDESK_API_TOKEN)

# 2. Validate the built bundle.
npx zcli apps:validate dist

# 3. Package it into a zip (writes dist/tmp/*.zip).
npx zcli apps:package dist

# 4. Upload + install it as a PRIVATE app on your account.
#    Writes zcli.apps.config.json with the assigned app_id โ€” do not commit it.
npx zcli apps:create dist

# 5. Push later changes to the same installed app.
npx zcli apps:update dist

pnpm --filter @hanzo/zendesk validate / package wrap steps 2โ€“3.

Configure the app

After install, in Admin Center โ†’ Apps and integrations โ†’ Zendesk Support apps, open the app's settings and:

  1. Paste your Hanzo API key (sk-โ€ฆ) into Hanzo API key (secure).
  2. Optionally set a Default model (defaults to zen5).
  3. Save. The panel appears in the ticket sidebar.

Generate a Hanzo API key at https://hanzo.ai.

Marketplace

To list publicly, set "private": false in manifest.json, fill in translations/en.json (short_description โ‰ค 80 chars, long_description, installation_instructions), then submit the packaged zip via the Zendesk Marketplace developer flow (Admin Center โ†’ Marketplace โ†’ submit an app). See the Zendesk app publishing guide.

Layout

File Role
src/config.ts Constants: gateway base URL, default model, setting names, context budget.
src/ticket.ts Pure. Ticket conversation โ†’ windowed, labelled, truncated text + honest scope note.
src/hanzo.ts Pure shaping + the one @hanzo/ai model path (quick-action catalog, buildMessages, stream/complete).
src/zaf.ts The ZAF boundary: init, ticket read, write-back, secure-proxy fetch.
src/panel.ts / panel.html / styles.css The iframe UI.
manifest.json ZAF v2 app manifest (location ticket_sidebar, secure key parameter).
translations/en.json App name, descriptions, and parameter labels.
build.js esbuild โ†’ dist/ ZAF app bundle.

License

MIT