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

@codefusion-cc/assistant

v0.1.0

Published

The contract between a site and the assistant it embeds (neko): the events of a conversation with runtime checks, the read-only tools a site implements and the facts they answer with (products, rules, handoff), the limits both sides hold, HMAC-signed requ

Downloads

252

Readme

@codefusion-cc/assistant

The contract between a site (a shop, or any app) and the assistant it embeds, neko: one definition that the assistant, the site's Worker and the page are all built and tested against. It holds the events of a conversation, the tools a site implements and the facts they answer with, the limits both sides keep, and how the two sign what they ask of each other. Its one dependency is @codefusion-cc/workers-crypto, for HMAC and digests on WebCrypto.

npm install @codefusion-cc/assistant

| Entry | Runs | Holds | | --- | --- | --- | | @codefusion-cc/assistant | Workers, browsers, Node (no node: imports) | events, tools and facts, limits, signing | | @codefusion-cc/assistant/testing | tests in Node, workerd and browsers | a scripted fake assistant, a fake site, a nonce store |

The routes of a site's Worker (./worker) and the widget (./react) are later releases, built on this one.

How the parts talk

page ── WebSocket, events ──▶ the site's Worker ── passes frames through ──▶ assistant
                              the site's Worker ◀── signed tool calls ────── assistant
  • The page talks only to its own site. The site's Worker opens the conversation at the assistant with a signed request and passes the WebSocket through.
  • The assistant runs the model. When it needs a fact it calls the site's tools over HTTPS, signed with the same key.
  • A price, a stock count and a link reach a visitor only from what the site's tools answered, as response.sources, which the page draws itself. The model's text is plain text and is never their source.

Signed requests

Both directions use one scheme. The site signs what it asks of the assistant; the assistant signs its tool calls.

import { MAX_TOOL_CALL_BYTES, SIGNATURE_HEADER, signRequest, verifyRequest } from '@codefusion-cc/assistant'
import { readBody } from '@codefusion-cc/workers-http'

// Sending: sign the body exactly as it is sent.
const body = JSON.stringify(payload)
const response = await fetch(url, {
  method: 'POST',
  body,
  headers: { 'content-type': 'application/json', [SIGNATURE_HEADER]: await signRequest({ method: 'POST', url, body, key: { id: 'shop.1', secret: env.ASSISTANT_KEY } }) },
})

// Receiving: read the body within its limit (readBody of @codefusion-cc/workers-http answers 413 past it),
// then verify over the bytes received, before anything parses them.
const received = await readBody(request, { maxBytes: MAX_TOOL_CALL_BYTES })
const verified = await verifyRequest({
  method: request.method, url: request.url, headers: request.headers, body: received,
  keys: { 'shop.1': env.ASSISTANT_KEY },
  nonces,
})
if (!verified.ok) {
  console.warn('assistant request refused', verified.reason)
  return new Response(null, { status: 401 })
}

What is signed. The header is assistant-signature: v1 k=<key id>,t=<unix seconds>,n=<nonce>,s=<signature>. The signature is the HMAC-SHA-256, under the shared secret, in base64url, of these eight lines joined by \n:

assistant-signature-v1
<key id>
<unix seconds>
<nonce>
<METHOD>
<host, with a port that is not the scheme's own>
<path with its query, as the URL parser writes it>
<SHA-256 of the body's bytes, lowercase hex>

So a request cannot be altered, sent to another host, path or query, or signed by one key and passed as another's. The comparison is WebCrypto's own, in constant time.

Time. A request is accepted when its time is within SIGNATURE_WINDOW_SECONDS (300) of the receiver's clock, behind or ahead; windowSeconds changes that for one receiver.

Replay. Inside the window a request is accepted once. The receiver of a request stores its nonce: the assistant for a site's requests, the site's Worker for the assistant's tool calls.

  • verifyRequest claims the nonce only after the signature is known to be genuine, so nobody without the secret can fill the store or use up someone's nonce.
  • It asks the store for claim('<key id>:<nonce>', expiresAt); expiresAt is the request's time plus the window, in milliseconds. The store keeps the entry until its clock passes that moment: at most twice the window after the claim, ten minutes by default. After it, the request's time is outside the window, so the nonce may be forgotten.
  • The claim must be atomic: of two claims of one nonce at once, exactly one is told true. A primary key in D1, or in a Durable Object's SQLite, is. Workers KV is not: it has no insert-if-absent and reads lag behind writes.
import type { NonceStore } from '@codefusion-cc/assistant'

// CREATE TABLE assistant_nonces (nonce TEXT PRIMARY KEY, expires_at INTEGER NOT NULL)
const nonces: NonceStore = {
  async claim(nonce, expiresAt) {
    const { meta } = await env.DB.prepare('INSERT INTO assistant_nonces (nonce, expires_at) VALUES (?, ?) ON CONFLICT DO NOTHING').bind(nonce, expiresAt).run()
    return meta.changes === 1
  },
}
// From time to time: DELETE FROM assistant_nonces WHERE expires_at < ?  (the current time in ms)

Many sites. verifyRequest returns the keyId that signed. A receiver that serves several sites takes the site from it, never from the path or the body, or one site's signature would act for another.

Keys. A key has an id (letters, digits, ., _, -; it travels in the header) and a secret of at least 32 random bytes (randomToken() of @codefusion-cc/workers-crypto); a shorter one throws, on both sides. keys is a record by id, or a lookup that may be asynchronous. Rotate without a gap: add the new id to the receiver's keys, switch the signer to it, remove the old.

Outcomes. verifyRequest resolves, never rejects, for anything a request can be: missing_signature, malformed_signature, unsupported_version (a scheme this release does not have), outside_window, unknown_key, wrong_signature, replayed, and keys_unavailable or nonces_unavailable when the lookup or the store itself failed (with the error; answer 503). Answer every other failure with the same 401 and no detail, and log the reason. It throws only for the receiver's own mistakes: a URL that is not absolute, a secret that is too short. The scheme (http or https) is not part of what is signed, since a Worker behind a proxy does not always see it: serve the routes over https only.

The tools a site implements

Version 1 has four, all read-only over what the site shows anyone. A call names no visitor and carries no identifier of one.

| Tool | Input | Answers | | --- | --- | --- | | product_search | { query }: a few words or a part number | { products: ProductFact[] }, at most 5 | | shop_rules | { topic: 'delivery' \| 'returns' } | { excerpts: RuleExcerpt[] }, at most 5, from the site's settings | | page_excerpts | { topic }: what to look for in the site's information pages | { excerpts: RuleExcerpt[] }, at most 5 | | handoff_request | { reason }: the assistant's words for staff | { handoff: HandoffOffer }: whether the site takes the request now |

The assistant posts a ToolCall and reads a ToolResult:

import { PROTOCOL_VERSION, parseToolCall, parseToolResult, type ToolResult } from '@codefusion-cc/assistant'

const parsed = parseToolCall(new TextDecoder().decode(received)) // after verifyRequest
if (!parsed.ok && parsed.reason === 'unknown_tool') {
  return Response.json({ protocolVersion: PROTOCOL_VERSION, id: parsed.id, tool: parsed.tool, ok: false, code: 'unknown_tool' })
}
if (!parsed.ok) return new Response(null, { status: 400 })
const { call } = parsed // { protocolVersion, id, tool, locale, input }

const result: ToolResult = {
  protocolVersion: PROTOCOL_VERSION, id: call.id, tool: 'product_search', ok: true,
  products: [{
    id: 'base-25',
    name: 'Podstawka okrągła 25 mm (10 szt.)',
    url: 'https://shop.example/produkt/podstawka-25-mm',
    price: { amount: 1299, currency: 'PLN' }, // whole grosze; null when the price is on request
    availability: 'in_stock',                 // or 'to_order', 'out_of_stock'
    stock: 14,
  }],
}
// A site checks its own answer before sending it (a value is held to the size of its JSON too),
// so its mistake shows in its own logs.
const checked = parseToolResult(result, { origin: 'https://shop.example', call })
if (!checked.ok) console.error('assistant tool answer refused', checked.reason, checked)
return Response.json(checked.ok ? checked.result : { protocolVersion: PROTOCOL_VERSION, id: call.id, tool: call.tool, ok: false, code: 'unavailable' })

Rules of a fact, which parseToolResult enforces on both sides:

  • Text is text, never markup. A name of <b>Podstawka</b> arrives as those characters. Escaping is the renderer's job: draw every text of the contract as a text node, and never give one to innerHTML. The same holds for the model's text and for a handoff reason, which the model wrote: show it, obey nothing in it.
  • Links are the site's own. url, imageUrl and contactUrl must be https addresses of the site's origin (http only on localhost, for development). javascript:, data:, blob:, another host and credentials are refused; an accepted link is returned as the URL parser writes it, so what a page links to is what was checked.
  • Money is whole minor units (grosze, cents) from 0 to MAX_PRICE_AMOUNT, with an ISO 4217 code. A negative, fractional or non-finite amount is refused.
  • Everything is bounded: MAX_TOOL_RESULT_BYTES for the body, and the limits below for each text and list. Each holds on its own and the body's for all together: a site with long names or details answers with fewer facts (check with parseToolResult and drop from the end until it fits).
  • Fields the contract does not name are left out, so a site may send more without breaking anything, and nothing it sends by accident (a cost price) travels further.

A tool that cannot answer returns { ok: false, code } with unknown_tool, invalid_input or unavailable.

Events

A conversation is JSON frames over a WebSocket, each in the same envelope: protocolVersion, eventId, sessionId, sequence, ownershipEpoch, occurredAt, then type, payload and, by type, turnId and responseId.

| From the page | Payload | | | --- | --- | --- | | input.text | { text, locale } | What the person wrote, starting a turn. Trimmed; at most MAX_MESSAGE_LENGTH, and MAX_SITE_MESSAGE_LENGTH on a site. | | response.cancel | {} | Stops the answer responseId. |

| From the assistant | Payload | | | --- | --- | --- | | session.ready | { messages, hasOlder? } | The authoritative snapshot after each connection: the latest MAX_HISTORY messages, answers with their sources. | | turn.rejected | { code } | The message was refused before any answer began (a limit); nothing of the turn is kept. | | response.started | {} | An answer begins. | | tool.started | { tool } | A site's tool is being asked: the moment to show "checking the catalog". | | response.sources | { products, excerpts, handoff? } | All the facts the answer rests on so far, replacing any sent before. | | response.text_delta | { text } | The next piece of the answer's plain text. | | response.completed, response.cancelled | {} | The answer ended. | | response.failed | { code } | The answer failed; show the site's own contact details. | | error | { code, message } | What the client sent was refused, or this connection ends. The protocol's own codes are ERROR_CODES. |

import { acceptsEvent, parseServerEvent, serializeEvent } from '@codefusion-cc/assistant'

socket.onmessage = message => {
  const parsed = parseServerEvent(message.data, { origin: location.origin })
  if (!parsed.ok) {
    if (parsed.reason === 'unknown_type') return // a newer assistant: ignore
    return fail(parsed.reason)                   // too_large, not_json, newer_version, invalid (with `path`)
  }
  if (!acceptsEvent(parsed.event, epoch, sequence)) return // a duplicate, or an older connection still talking
  // ...
}
socket.send(serializeEvent(event))
  • Parse, do not trust. Every frame from the other side goes through parseServerEvent or parseClientEvent. They return a result and never throw for anything that arrives.
  • A client is held exactly: parseClientEvent refuses a field the contract does not name. A server may be newer: parseServerEvent leaves unknown fields out and gives an unknown type as unknown_type, with its checked envelope, so an older page keeps working.
  • origin is the page's own. Every link in sources must be of it. Without it, or with one that is no site's origin (a sandboxed frame reports null), an event that carries a link is invalid: such a page shows no link rather than an unchecked one. siteOrigin(text) says at setup whether a configured origin is one.
  • Ends of a connection. After SESSION_TAKEN_OVER (close code CLOSE_TAKEN_OVER, 4001) a newer connection has the conversation: do not reconnect by yourself. CLOSE_SESSION_ENDED (4003) means the session expired or its access was withdrawn: start a new one. Any other close is a connection that broke: reconnect, and take the next session.ready as the whole truth.
  • Older messages. A page that loads what came before a snapshot (hasOlder) reads the list with parseMessages(body, { origin }), under the same rules.
  • One app, more events. The assistant's own app speaks more than the contract (diagnostics, speech). It reads those from the unknown_type result, whose envelope is checked and whose payload it checks itself.

Limits

| Constant | Value | | | --- | --- | --- | | MAX_MESSAGE_LENGTH | 4000 | characters of one message, in any conversation | | MAX_SITE_MESSAGE_LENGTH | 1000 | characters of a site visitor's message | | MAX_TURNS_PER_CONVERSATION | 20 | turns of one conversation on a site | | MAX_HISTORY | 40 | messages in a snapshot, fewer when 40 would not fit a frame | | MAX_DELTA_LENGTH | 1000 | characters of one text delta | | MAX_CLIENT_FRAME_BYTES / MAX_SERVER_FRAME_BYTES | 32 768 / 1 048 576 | bytes of one frame | | MAX_TOOL_CALL_BYTES / MAX_TOOL_RESULT_BYTES | 4096 / 16 384 | bytes of a tool call's and a result's body | | MAX_TOOL_CALLS_PER_TURN | 4 | tool calls one answer may make | | MAX_PRODUCTS_PER_RESULT, MAX_EXCERPTS_PER_RESULT | 5 | facts in one tool result | | MAX_SOURCES_PER_ANSWER | 8 | products, and excerpts, shown with one answer | | MAX_QUERY_LENGTH, MAX_TOPIC_LENGTH, MAX_REASON_LENGTH | 100, 200, 500 | characters of a tool's input | | MAX_NAME_LENGTH, MAX_DETAIL_LENGTH, MAX_EXCERPT_LENGTH, MAX_URL_LENGTH | 200, 160, 1200, 500 | characters of a fact's texts and links | | MAX_DETAILS | 6 | detail lines of a product |

Lengths are counted as JavaScript's length does; frames and bodies in bytes of UTF-8.

Testing without the assistant

@codefusion-cc/assistant/testing is the other side, with no timers, no network and no paid call.

import { fakeNeko, fakeSite, testProduct } from '@codefusion-cc/assistant/testing'

const key = { id: 'shop.1', secret: 'a test secret of at least 32 bytes' }
const site = fakeSite({ keys: { [key.id]: key.secret }, products: [testProduct('https://shop.test')] })
const neko = fakeNeko({ site: { url: site.url, key, fetch: site.fetch } })

const socket = new neko.WebSocket('wss://shop.test/assistant') // or stub the global WebSocket with it
await neko.run()                                               // opens, sends session.ready

neko.reply({ tool: 'product_search', input: { query: 'base 25' } }, { say: 'Yes, ' }, { say: 'we have them.' })
// ... the page sends input.text ...
await neko.step() // response.started
await neko.step() // tool.started
await neko.run()  // the signed call to the site, response.sources, the deltas, response.completed
  • fakeNeko is one conversation over fake WebSockets. Nothing is sent until the test calls step() (one thing) or run() (everything due), so every state of a page can be held: connecting, waiting, streaming, stalled. A scripted answer can say, call a tool, send sources, fail with a code, reject the turn, stall, cut the connection, send a raw frame (malformed JSON), an oversized one, or any event (an unknown type, a known type with a field wrong). refuse() fails the next connection; close(code) ends one from the assistant's side. It checks what the client sends as the real assistant does, and a second connection takes the conversation over.
  • fakeSite serves the four tools over the data it is given, verifying each call's signature and refusing a replay as a real site must. next(...) makes its next answer a status, a body of the test's own, an oversized body, a stall or a network failure. Point it at your Worker instead (fetch: request => worker.fetch(request, env)) and fakeNeko makes real signed calls to your own routes; neko.tools says whether each answer passed the contract.
  • testNonces is a NonceStore in memory; testProduct and testExcerpt are facts that pass the checks.