npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@assinafy/chat-sdk

v1.0.0

Published

Chat SDK for Assinafy — build chat bots and conversational integrations on top of the Assinafy document-signing API.

Readme

@assinafy/chat-sdk

CI CodeQL npm version License: MIT

A unified TypeScript SDK for building chat bots and conversational integrations on top of the Assinafy document-signing API.

It includes a typed Assinafy v1 client, chat orchestration primitives, renderable card components, adapter foundations, state helpers, and optional LLM tool definitions.

Layers

| Layer | Subpath | Purpose | | --- | --- | --- | | Client | @assinafy/chat-sdk/client | Strongly-typed wrapper over every Assinafy v1 endpoint. | | Cards | @assinafy/chat-sdk/cards | Declarative rich-message primitives (Card, Text, LinkButton, Actions, …) + renderers (text / markdown / HTML). | | Adapters | @assinafy/chat-sdk/adapters | createMemoryAdapter() factory + BaseAdapter for vendors building real platform adapters + webhook-signature helpers. | | State | @assinafy/chat-sdk/state | MemoryStateAdapter (subscriptions + per-thread KV). Swap in Redis/Postgres for prod. | | AI tools | @assinafy/chat-sdk/ai | Provider-agnostic LLM tool definitions wrapping the client (Anthropic + OpenAI tool-call compatible). |

All five layers can be used independently. The top-level Chat class composes them.

Installation

Each release is published to both registries.

From npmjs (default):

npm install @assinafy/chat-sdk
# or
pnpm add @assinafy/chat-sdk
# or
yarn add @assinafy/chat-sdk

From GitHub Packages — add a project-local .npmrc first so the scope resolves there:

# .npmrc
@assinafy:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}

Then npm install @assinafy/chat-sdk will pull from GitHub Packages.

Requires Node 20+ (or any runtime with a global fetch + Blob).

Quick Start

import {
  Chat,
  createMemoryAdapter,
  MemoryStateAdapter,
  AssinafyClient,
  Card,
  Text,
  Divider,
  LinkButton,
  DocumentPreview,
  SignerStatus,
} from "@assinafy/chat-sdk";

const client = new AssinafyClient({
  apiKey: process.env.ASSINAFY_API_KEY!,
  accountId: process.env.ASSINAFY_ACCOUNT_ID!,
  baseUrl: "https://sandbox.assinafy.com.br/v1", // omit for production
});

const memory = createMemoryAdapter();

const chat = new Chat({
  userName: "Assinafy Bot",
  adapters: { memory },
  state: new MemoryStateAdapter(),
  client,
});

chat.onCommand("status", async (thread, msg) => {
  const id = msg.text.replace(/^\/status\s*/, "").trim();
  const doc = await client.documents.get(id);
  await thread.post({
    card: Card({
      title: "Document status",
      children: [
        DocumentPreview({
          documentId: doc.id,
          name: doc.name,
          status: doc.status,
          signingUrl: doc.signing_url ?? undefined,
        }),
        Divider(),
        SignerStatus(
          doc.assignment?.summary.signers.map((s) => ({
            name: s.full_name,
            email: s.email,
            completed: s.completed,
          })) ?? [],
        ),
      ],
    }),
    fallbackText: `Document ${doc.name} — ${doc.status}`,
  });
});

// Drive a message through the in-memory adapter:
await memory.receive({ text: "/status 103051797f91ce2d16a548b6a8a6" });
console.log(memory.lastSent);

Handlers

| Handler | When it fires | | --- | --- | | chat.onNewMention((thread, message) => …) | Inbound message that mentions the bot (or opens a new thread on platforms like email where every inbound message is implicitly addressed). | | chat.onSubscribedMessage((thread, message) => …) | Follow-up message in a thread the bot has previously thread.subscribe()d. | | chat.onNewMessage(/regex/, (thread, message) => …) | Any inbound message whose text matches the regex. | | chat.onCommand("status", (thread, message) => …) | Slash-command syntax (/status, !status). Pass a RegExp for full control. | | chat.onAction((thread, action) => …) | Button click / slash-command interaction. | | chat.onFallback((thread, message) => …) | Catch-all when nothing else matched. |

Posting messages

thread.post() accepts:

await thread.post("plain text");
await thread.post(Card({ title: "Hello", children: [Text("body")] }));
await thread.post({
  card: Card({ title: "Order Confirmed", children: [Text("Your order #1234 has been shipped.")] }),
  fallbackText: "Order #1234 confirmed",
});

Card primitives

Capitalized helpers:

import {
  Card, Text, CardText, Heading, Divider, Section, Fields, Actions,
  Button, LinkButton, Image, Table, Select, RadioSelect, Option,
  DocumentPreview, SignerStatus, Children,
} from "@assinafy/chat-sdk";

Card({
  title: "Document ready",
  children: Children(
    Heading(2, "Contract.pdf"),
    Text("Bill M asked you to sign."),
    Fields([
      { label: "Status", value: "Pending signature" },
      { label: "Expires", value: "in 7 days" },
    ]),
    Divider(),
    Actions([
      LinkButton({ label: "Sign now", url: doc.signing_url!, style: "primary" }),
      Button({ id: "decline", label: "Decline", value: doc.id, style: "danger" }),
    ]),
  ),
});

Lowercase aliases (card, text, divider, …) are also exported for tests and quick scripts.

Generic renderers ship for any adapter:

import { renderText, renderMarkdown, renderHtml } from "@assinafy/chat-sdk/cards";
const plain = renderText(message);
const md = renderMarkdown(message);
const html = renderHtml(message); // safe — values are escaped

Building a platform adapter

Adapters are constructed with a createXxxAdapter(config) factory:

import { BaseAdapter, type ChatHandle, type OutgoingMessage, type SentMessage } from "@assinafy/chat-sdk/adapters";

export interface SlackAdapterConfig {
  botToken?: string;     // falls back to SLACK_BOT_TOKEN
  signingSecret?: string; // falls back to SLACK_SIGNING_SECRET
}

class SlackAdapter extends BaseAdapter {
  readonly name = "slack";
  // override initialize, postMessage, openDM, editMessage, deleteMessage,
  // addReaction, removeReaction, startTyping as needed
}

export function createSlackAdapter(config: SlackAdapterConfig = {}): SlackAdapter {
  return new SlackAdapter(/* … */);
}

The Chat constructor calls adapter.initialize(chat) once at startup so the adapter can stash a reference. From your webhook handler, parse the request, then call:

await this.chat!.processMessage(this, normalizedIncomingMessage);
// or
await this.chat!.processAction(this, normalizedIncomingAction);

Webhook signature verification

import { verifyWebhookSignature, WebhookSignatureError } from "@assinafy/chat-sdk";

// Inside your adapter's webhook handler:
const signature = request.headers.get("x-resend-signature")!;
const body = await request.text();

try {
  verifyWebhookSignature({ secret: process.env.RESEND_WEBHOOK_SECRET!, body, signature });
} catch (err) {
  if (err instanceof WebhookSignatureError) return new Response("invalid", { status: 401 });
  throw err;
}

Supports plain HMAC-SHA256 (default), ${timestamp}.${body} payloads (pass timestamp), base64- or hex-encoded signatures, configurable hash algorithm, and an algo= prefix on the supplied signature.

Unsupported operations

Optional methods (editMessage, deleteMessage, addReaction, removeReaction, startTyping) throw NotImplementedError by default — override only what your platform supports.

The Assinafy client

Every Assinafy v1 endpoint is implemented and typed.

// Auth
await client.auth.login({ email, password });
await client.auth.createApiKey(password);
await client.auth.getApiKey();
await client.auth.deleteApiKey();

// Signers
const page = await client.signers.list(accountId, { search: "alice", perPage: 50 });
const signer = await client.signers.create(accountId, { full_name: "Alice", email: "[email protected]" });
await client.signers.update(accountId, signer.id, { whatsapp_phone_number: "+5511..." });
await client.signers.remove(accountId, signer.id);

// Documents
const doc = await client.documents.upload(accountId, { filename: "contract.pdf", body: pdfBuffer });
const docs = await client.documents.list(accountId, { status: "pending_signature" });
for await (const d of client.documents.iterate(accountId)) { /* … */ }
await client.documents.activities(doc.id);
const thumb = await client.documents.thumbnail(doc.id); // Response — stream/buffer as needed
await client.documents.remove(doc.id);

// Tags
await client.tags.create(accountId, { name: "important", color: "#ff8800" });
await client.tags.setForDocument(accountId, doc.id, ["important"]);

// Templates
const tpls = await client.templates.list(accountId);
const tpl = await client.templates.get(accountId, tpls.data[0].id); // detail incl. roles + default tags
const newDoc = await client.templates.instantiate(accountId, tpl.id, {
  signers: [{ role_id: tpl.roles![0].id, id: signer.id }],
});

// Assignments — connect signers to a document and start the flow.
const assignment = await client.assignments.create(doc.id, {
  method: "virtual",
  signers: [{ id: signer.id }],
  message: "Please sign by Friday.",
});
await client.assignments.estimateResendCost(doc.id, assignment.id, signer.id);

// Fields
const field = await client.fields.create(accountId, { type: "text", name: "Reference" });
await client.fields.validate(accountId, field.id, "ABC-123");
await client.fields.listTypes();

// Webhooks
await client.webhooks.listEventTypes();
await client.webhooks.getSubscription(accountId);
await client.webhooks.listDispatches(accountId, { perPage: 20 });

// Public document and signer flows
await client.documents.publicGet(doc.id);
await client.documents.sendPublicToken(doc.id, { recipient: "[email protected]", channel: "email" });
await client.documents.verify("SIGNATURE_HASH");
await client.signature.self(accessCode);
await client.signature.acceptTerms(accessCode);
await client.signature.upload(accessCode, "signature", pngBytes);

Pagination

list() methods return { data, pagination }. To walk every page, use the matching iterate() async iterator.

Errors

Every non-2xx response is mapped to an ApiError with .status, .body, .path, and .method.

import { ApiError } from "@assinafy/chat-sdk";

try {
  await client.documents.remove(id);
} catch (err) {
  if (err instanceof ApiError && err.status === 409) {
    // document is not in a deletable status
  }
}

Retries

Transient failures (429, 5xx, network errors) are retried with exponential backoff. Pass maxRetries: 0 to disable.

Authentication

new AssinafyClient({ apiKey: "..." });           // X-Api-Key header
new AssinafyClient({ accessToken: "..." });      // Bearer header
AssinafyClient.fromEnv();                        // reads ASSINAFY_API_KEY / ASSINAFY_ACCESS_TOKEN

AI tool-calling

createChatTools(client) returns provider-agnostic JSON-schema tool descriptors. Compatible with both Anthropic and OpenAI tool-calling.

import Anthropic from "@anthropic-ai/sdk";
import { createChatTools, runTool, toAiMessages, defaultSystemPrompt } from "@assinafy/chat-sdk/ai";

const tools = createChatTools(client);
const anthropic = new Anthropic();

const response = await anthropic.messages.create({
  model: "claude-sonnet-4-6",
  system: defaultSystemPrompt("Assinafy Bot"),
  messages: toAiMessages(history),
  tools: tools.map((t) => ({ name: t.name, description: t.description, input_schema: t.input_schema })),
  max_tokens: 1024,
});

for (const block of response.content) {
  if (block.type === "tool_use") {
    const result = await runTool(tools, block.name, block.input);
    // feed result back as `tool_result` in the next turn
  }
}

Restrict the toolset for guard-railed bots:

const readonly = createChatTools(client, { include: [
  "list_signers", "get_signer", "list_documents", "get_document", "document_activities",
] });

Environment variables

| Variable | Default | Purpose | | --- | --- | --- | | ASSINAFY_API_KEY | – | API key (X-Api-Key) | | ASSINAFY_ACCESS_TOKEN | – | Alternative: bearer token | | ASSINAFY_BASE_URL | https://api.assinafy.com.br/v1 | https://sandbox.assinafy.com.br/v1 for sandbox | | ASSINAFY_ACCOUNT_ID | – | Default account id |

const client = AssinafyClient.fromEnv();

Testing

npm test            # full suite (unit + integration)
npm run test:unit   # unit only — no credentials needed
npm run test:integration

Integration tests skip themselves automatically when ASSINAFY_API_KEY / ASSINAFY_ACCOUNT_ID aren't set, so the suite is safe to run in CI without secrets.

Examples

See examples/ for runnable scripts:

  • basic-bot.ts — minimal in-memory bot answering status questions.
  • ai-bot.ts — same bot, with an Anthropic tool-calling loop.
  • live-cli.ts — REPL that runs against the real sandbox.

License

MIT — see LICENSE.