canva, canvas-lms, cards, docusign, epic, imanage, procore, quickbooks, shopify, workday and teams publish dist without source maps or src. A watch build still writes maps for local debugging.
Hanzo AI for DocuSign
AI over agreements, right where they're signed. @hanzo/docusign adds four
actions to any DocuSign envelope β summarize, extract key terms,
risk-flag clauses, and draft a memo β and auto-summarizes every envelope
the moment it completes.
Two surfaces, one shared core:
- Panel (
dist/index.html) β an Extension App / linked-app page that opens with an envelope in context and runs the four actions over its agreement text. - Service (
dist/server.js) β a small Node service that handles the DocuSign OAuth callback and the Connect webhook: onenvelope β completedit fetches the combined PDF, extracts the text, and summarizes it via api.hanzo.ai.
Both are built on the shared Hanzo foundation β @hanzo/ai (the headless
OpenAI-compatible model client) and @hanzo/iam (Hanzo identity) β and speak the
gateway's /v1 wire protocol, identical to @hanzo/pdf, @hanzo/meetings,
@hanzo/slack, and @hanzo/teams.
Analysis is informational, grounded strictly in the agreement text β not legal advice. The system prompt forbids inventing parties, dates, or clauses.
Architecture
src/
config.ts env boundary: gateway, model, DocuSign OAuth/API bases, HMAC, server config
client.ts the createAiClient contract (thin @hanzo/ai; one-line re-export when published)
docusign-oauth.ts Authorization Code Grant request shaping + userinfo/base_uri discovery (pure)
docusign-api.ts eSignature REST v2.1 request wrappers + response parsing (pure)
webhook.ts Connect HMAC verification + event routing (pure, node:crypto)
pdf-text.ts PDF β text (pure itemsToText + injectable pdf.js loader)
hanzo.ts document windowing/truncation + prompt assembly + the single `ask` path (pure)
actions.ts the four AI actions as prompts over `ask` (one code path to the model)
envelope.ts the analysis pipeline: fetch combined PDF β extract β window β action
panel.ts pure panel logic (launch-context parse, textβpages)
app.ts browser panel: DOM glue over the pure modules
server.ts Node service: /oauth/install, /oauth/callback, /connect, /healthz
index.html panel markup styles.css panel styles
Everything logic-heavy is pure and unit-tested; app.ts (browser) and
server.ts (Node) are the only impure entry points. The gateway is reached
through one shape β createAiClient().chat.completions.create() β so swapping
in the published @hanzo/ai headless client is a one-import change.
Build & test
pnpm --filter @hanzo/docusign install
pnpm --filter @hanzo/docusign test # vitest β 103 tests
pnpm --filter @hanzo/docusign typecheck # tsc --noEmit, clean
pnpm --filter @hanzo/docusign build # esbuild -> dist/
build.js bundles the panel (app.js), copies index.html + styles.css,
emits the pdf.js worker (pdf.worker.mjs), and bundles the Node service
(server.js).
Register the DocuSign app
Do this once in the DocuSign Developer / Admin console (demo) or the production Admin.
- Apps and Keys β Add App and Integration Key. Note the Integration Key
(this is your OAuth
client_id). - Authentication: choose Authorization Code Grant. Generate a Secret
Key β this is
DOCUSIGN_CLIENT_SECRET, held only by the service, never in the browser bundle. - Redirect URIs: add your service callback, e.g.
https://docusign.hanzo.ai/oauth/callback(DOCUSIGN_REDIRECT_URI). - OAuth scopes: the app requests
signature openid.signaturegrants the eSignature REST API (read envelopes/documents);openidlets/oauth/userinforeturn the account list and each account's regionalbase_uri(we never hard-code a REST host β it's discovered at login). - Grant consent once by visiting
GET /oauth/installon the service; it redirects to DocuSign's consent screen, and the callback stores the account's token +base_urifor the webhook to use.
DocuSign environments:
DOCUSIGN_ENV |
Account server (OAuth) |
|---|---|
demo |
https://account-d.docusign.com |
production |
https://account.docusign.com |
The REST base (https://<region>.docusign.net/restapi/v2.1) comes from
/oauth/userinfo per account β no configuration needed.
Configure the Connect webhook (+ HMAC)
Connect is DocuSign's webhook. Under Settings β Connect β Add Configuration β Custom:
- URL to publish:
https://docusign.hanzo.ai/connect(the service'sPOST /connect). - Events: enable Envelope β Completed (the service acts only on
completed; every other status is acknowledged and ignored). - Data format: JSON (the aggregate payload).
- HMAC security (required): under the configuration's HMAC section,
add a secret key. Copy it into
DOCUSIGN_CONNECT_HMAC_KEY.
How verification works
DocuSign signs every Connect message. For each configured HMAC key it adds a header:
X-DocuSign-Signature-1: base64( HMAC-SHA256( hmacKey, <raw request body> ) )
The service recomputes the digest over the raw body bytes (never a
re-serialized object β key order/whitespace would differ) and compares in
constant time (node:crypto timingSafeEqual) against every
X-DocuSign-Signature-N header. A message that fails is a 401 before any
document is fetched β that is the entire trust boundary. This is covered by
test/webhook.test.ts and verified end-to-end against the running service.
Environment
The service reads only the environment; nothing is committed. In production these
come from KMS (kms.hanzo.ai) synced via KMSSecret CRDs β never a checked-in
env file.
| Variable | Required | Meaning |
|---|---|---|
DOCUSIGN_CLIENT_ID |
yes | Integration Key (OAuth client id; public) |
DOCUSIGN_CLIENT_SECRET |
yes | OAuth secret key (server only) |
DOCUSIGN_REDIRECT_URI |
yes | Registered OAuth callback URL |
DOCUSIGN_CONNECT_HMAC_KEY |
yes | Connect HMAC secret (server only) |
DOCUSIGN_ENV |
no | demo (default) or production |
HANZO_API_KEY |
no | sk-β¦ gateway bearer the webhook summarizes with; without it the webhook verifies + routes but skips summarization |
PORT |
no | Listen port (default 8789) |
The service fails fast (throws) at startup if any required secret is missing.
Endpoints
| Method + path | Purpose |
|---|---|
GET /oauth/install |
302 β DocuSign consent |
GET /oauth/callback |
exchange code β token, discover base_uri via /oauth/userinfo, store the account authorization |
POST /connect |
verify HMAC, then on completed fetch the combined PDF and summarize |
GET /healthz |
readiness |
The summarize / extract / risk flow
In the panel β the user (or DocuSign, via the launch URL's envelope context) provides the agreement text; a chip (or a freeform question) runs:
agreement text β asPages β buildDocumentContext (windowed to the char budget,
truncation reported honestly) β buildMessages (grounded system prompt, agreement
fenced as data) β ask β api.hanzo.ai /v1/chat/completions β result
On completion β the Connect webhook runs the same core, server-side:
POST /connect β verify HMAC β routeEvent (completed?) β analyzeEnvelope:
GET /envelopes/{env} (metadata: title, status)
GET /envelopes/{env}/documents/combined (all documents as one PDF)
extractPdfText (pdf.js) (per-page text)
buildDocumentContext (window to budget)
runAction('summarize') (β api.hanzo.ai)
β summary (deliver hook: store in hanzoai/docdb, post back as an envelope
custom field, or notify the sender β a per-deployment choice)
The four actions are the same runAction id in both surfaces β the chip a
user clicks and the webhook auto-summary share one code path to the model.
Deploy
The panel is static; the service is a small container. Per the Hanzo stack:
- Panel β hosted over hanzoai/static behind hanzoai/ingress at
docusign.hanzo.ai(servedist/index.html,app.js,styles.css,pdf.worker.mjs). - Service β a container running
node dist/server.js, imageghcr.io/hanzoai/docusign:<tag>(built in CI/CD,--platform linux/amd64), behindhanzoai/ingress, with secrets from KMS viaKMSSecret. Point the DocuSign redirect URI and the Connect URL atdocusign.hanzo.ai.
No nginx, no caddy β hanzoai/ingress + hanzoai/static only.