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

@choirhq/cap-partner

v0.2.1

Published

TypeScript/Node SDK for the Choir CAP (Channel-Aware Partner) protocol — build partner integrations that participate in Choir channels as first-class members.

Downloads

139

Readme

@choirhq/cap-partner (TypeScript / Node SDK)

Drop-in SDK for any Node service that wants to integrate with Choir over the CAP (Channel-Aware Partner) protocol v0.2. NestJS-native via the underlying fetch + Web Crypto; works equally well in Express, Fastify, raw scripts.

Functionally identical to the Python SDK — same wire format, same APIs, same tests. Pick whichever fits your stack.

Install

npm install @choirhq/cap-partner
# or:  pnpm add @choirhq/cap-partner  /  yarn add @choirhq/cap-partner

Requires Node 18+ (built-in fetch + crypto.subtle). Only runtime dependency: jose.

Quick start

1. Generate a signing key (once)

import { generateEs256Keypair } from '@choirhq/cap-partner';

const { pem } = await generateEs256Keypair();
await fs.writeFile('partner.key', pem);   // store in your secrets manager

Generate once and load from your secrets manager at boot. Don't generate per-restart — Choir caches your JWKS and an ephemeral key invalidates the cache.

2. Register your partner with Choir

A Choir staff member runs POST /cap/partners with:

{
  "iss": "my.partner.io",
  "name": "My Partner",
  "manifest_url": "https://my.partner.io/manifest.json",
  "jwks_url": "https://my.partner.io/jwks.json",
  "inbound_url": "https://my.partner.io/inbound"
}

Then each workspace admin separately approves the connection + grants you channel scopes from /admin/cap/partners/<id>.

3. Wire the client

import { CapPartnerClient } from '@choirhq/cap-partner';

const client = new CapPartnerClient({
    iss: 'my.partner.io',
    kid: '2026-q2',
    privateKeyPem: process.env.CAP_PRIVATE_KEY_PEM!,
    choirBaseUrl: 'https://choir.example.com',
});

// Post a message
await client.sendSay({
    workspace: 'acme', channel: 'ops',
    body: 'Hello from my partner.',
});

// Post an event
await client.sendEvent({
    workspace: 'acme', channel: 'alerts',
    eventType: 'order.delivered',
    attrs: { order_id: '4471', carrier: 'DHL' },
});

// Propose an action a human in the channel must approve
await client.sendPropose({
    workspace: 'acme', channel: 'finance-approvals',
    proposalId: 'refund-4471',
    title: 'Refund order #4471',
    description: '$234 — customer reports defective product',
    action: {
        tool_name: 'my.refund.execute',
        args: { order_id: '4471', amount: 234 },
    },
});

When a human approves or rejects, Choir POSTs a proposal.decided event envelope to your /inbound.

4. Serve the three required endpoints

See examples/sample-partner.ts for a complete Express implementation — copy + adapt.

  • GET /jwks.json — return await client.myJwks()
  • GET /manifest.json — return your manifest object
  • POST /inboundawait client.verifyInbound(req.body); dispatch by env.turn

Manifest

Your /manifest.json declares what tools the partner advertises. Choir admins refresh it from the admin UI; the cached version drives the per-channel tool grants.

const MANIFEST = {
    cap_version: '0.2',
    iss: 'my.partner.io',
    name: 'My Partner',
    description: 'What my partner does',
    vendor: {
        name: 'My Company',
        url: 'https://my.company',
        contact: '[email protected]',
    },
    tools: [
        {
            name: 'my.search',
            title: 'Search my catalog',
            description: 'Searches my product catalog by name or category.',
            input_schema: {
                type: 'object',
                properties: { q: { type: 'string' } },
                required: ['q'],
            },
            risk: 'safe',
        },
        {
            name: 'my.refund.execute',
            title: 'Execute refund',
            description: 'Refunds a transaction. Invoke through propose, not direct tool_request.',
            input_schema: {
                type: 'object',
                properties: {
                    order_id: { type: 'string' },
                    amount: { type: 'number' },
                },
                required: ['order_id', 'amount'],
            },
            risk: 'sensitive',
            requires_propose: true,
        },
    ],
    subscribes_to_events: ['proposal.decided'],
};

Validation rules (enforced by Choir's manifest service):

  • cap_version must be '0.1' or '0.2'
  • iss must match the iss Choir has registered for your partner
  • Tool names: lowercase, digits, ., _, - only; unique within the manifest
  • risk must be 'safe' or 'sensitive'
  • input_schema must be a JSON object (typically a JSON Schema)

Turn types (v0.2)

| Turn | Direction | What it's for | |---|---|---| | say | both | A normal message body in a channel | | event | both | A structured notification ({event_type, attrs}) | | propose | partner → Choir | An action awaiting human approval | | tool_request | both | Ask the other side to run a tool | | tool_result | both | Response to a tool_request, correlated by call_id |

Other turn types (ask, subscribe, transfer, escalate) are reserved and currently return turn_type_not_supported_in_v0.

Verifying inbound envelopes

app.post('/inbound', async (req, res) => {
    try {
        const env = await client.verifyInbound(req.body);
        // dispatch by env.turn — see examples/sample-partner.ts
        res.json({ ok: true });
    } catch (e) {
        res.status(401).json({ error: e.message });
    }
});

verifyInbound:

  1. fetches Choir's JWKS at {choirBaseUrl}/cap/{workspaceSlug}/.well-known/jwks.json (cached per workspace);
  2. reconstructs the canonical signed input from the envelope sans signature;
  3. verifies the ES256 signature;
  4. checks iat isn't in the future and exp hasn't passed.

On signature failure, refreshes the JWKS once (handles key rotation) before failing.

Running the sample partner

cd cap-sdks/typescript
npm install
CHOIR_BASE_URL=http://localhost:4000 npx ts-node examples/sample-partner.ts

If CAP_PRIVATE_KEY_PEM is unset, it generates an ephemeral keypair on boot with a clear warning. Production must supply a stable key.

Running tests

npm install
npm test

License

MIT.