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]>
6.1 KiB
Hanzo AI for Canva
A Canva Apps SDK side-panel app that puts Hanzo's models next to your design. Non-designers, marketers, and SMBs — Canva's core audience — get copywriting help without leaving the canvas:
- Generate copy — headlines, captions, and marketing copy from a short brief.
- Rewrite selected text — tighten, restyle, or restate the text you selected, in place.
- Translate — the selected text into any language, tone preserved.
- Content ideas — a numbered list of angles for a topic; click one to add it.
- Ask — a question about the design or its copy.
It reads the current selection and page via @canva/design, runs the model gateway
through the published @hanzo/ai headless client over api.hanzo.ai, and adds or
replaces text elements with the result. You paste your own Hanzo sk- key; the app
holds no secret and calls no /api/ path — only api.hanzo.ai/v1/....
Architecture — one way to do everything
| Module | Responsibility | Depends on |
|---|---|---|
src/design-core.ts |
PURE. The action catalog, prompt builders, design-context assembly + truncation, and response parsing. No SDK, no DOM. Fully unit-tested. | — |
src/hanzo.ts |
Thin wrapper over @hanzo/ai — the only place the model gateway is called. |
@hanzo/ai |
src/canva.ts |
Thin adapter over @canva/design — read the selection, replace it, insert a new text element. |
@canva/design |
src/config.ts |
Gateway base URL, default model, context budget, sk- key guard. |
— |
src/app.tsx |
The App UI Kit panel — binds the three above to widgets; holds only UI state. | @canva/app-ui-kit |
src/index.tsx |
Entry — mounts App under AppUiProvider. |
react-dom, @canva/app-ui-kit |
The design-core mirrors @hanzo/figma's design-core so the two design
apps expose the same five actions with the same context/prompt/parse contracts.
The read-selection → insert-text flow
- On mount,
canva.tsonSelectionChangesubscribes toselection.registerOnChange({ scope: 'plaintext' }). Each change yields aSelectionSnapshot(count, joinedtext); the panel gates Rewrite / Translate oncount > 0and shows a preview of the selected text. - You pick an action, add a brief/question/language as needed, and press Run.
design-core.buildActionMessages(id, inputs, { selection })resolves the action's task, fences the observed design as data (with an honest truncation note if it was cut), and returns the OpenAI-shaped message list.hanzo.runChat(messages, { token, model })calls@hanzo/aichat.completions.create(non-streaming) and returns the assistant text.design-core.parseCopy/parseIdeasstrips code fences, wrapping quotes, and list markers into paste-ready text.- You press Replace selection (Rewrite →
canva.replaceSelectedText, which reads a fresh draft, overwritesdraft.contents[].text, anddraft.save()s) or Add to design (everything else →canva.insertText→addElementAtCursorwith a newTextElement). Ideas insert one line per click.
Develop locally
Prerequisites: Node 18+, pnpm, and a Canva account.
1. Create the app in the Canva Developer Portal
- Go to https://www.canva.com/developers/apps and Create an app.
- Under App details, copy the App ID.
- Under Add features → Editing, enable the app to run in the editor side panel (this app needs the Design editing scope: read the selection, add/replace elements).
- Under Configure your app, set the Development URL to the CLI/preview origin (default
http://localhost:8080).
2. Configure
cd packages/canva
cp .env.template .env
# put your App ID into CANVA_APP_ID
3. Install, test, build
pnpm install # from the repo root (workspace)
pnpm --filter @hanzo/canva test # vitest — the pure design-core + hanzo shaping
pnpm --filter @hanzo/canva typecheck # tsc --noEmit, strict
pnpm --filter @hanzo/canva build # esbuild → dist/{index.html,app.js,app.css}
4. Preview in Canva
Serve the bundle and point the Developer Portal's Development URL at it. With the Canva CLI:
npx @canva/cli@latest login
pnpm --filter @hanzo/canva watch # rebuilds dist/ on change
npx @canva/cli@latest apps preview # opens the app in the Canva editor, hot-reloading
Open any design, launch Hanzo AI from the app side panel, paste your sk- key,
pick a model, and run an action. Select text on the canvas to enable Rewrite and
Translate.
Getting a Hanzo sk- key
Sign in at https://hanzo.id and mint an API key (prefixed sk-). The panel
validates the prefix before use and sends it as the bearer to api.hanzo.ai. The key
lives only in the panel's session state — it is never persisted or bundled.
Submit to the Canva Apps Marketplace
- Finish and test the app in preview; ensure
pnpm buildis clean anddist/loads. - Host
dist/on your production origin (behindhanzoai/ingress+ thehanzoai/staticplugin — never nginx/caddy) and set the app's production App source URL to it. - In the Developer Portal, complete App listing (name, icon, description, screenshots) and Submit for review.
- Canva reviews for the requested scopes (Design editing here) and UX. Address feedback and resubmit; on approval the app is published to the Apps Marketplace.
Tests
test/design-core.test.ts covers the pure core: whitespace collapse, context
assembly + budget truncation, message fencing (data vs. task, the truncation note,
the no-context path), the five prompt builders (brief/tweak/language/count/question
embedding and the count clamp), the id → messages path, and the response parsers
(fence/quote stripping, idempotence, list-marker stripping). test/hanzo.test.ts
asserts the @hanzo/ai request shaping against a mock client (model, stream:false,
temperature, messages, abort signal, empty-content error, model listing).
test/config.test.ts covers the /v1 endpoint builders and the sk- key guard.
Test Files 3 passed (3)
Tests 40 passed (40)