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

@nqminds/flair-api-client

v0.1.1

Published

Client for the CMDstream API — sign in, then call the free and metered tiers

Downloads

189

Readme

CMDstream API — reference client

Zero-dependency Node client (18+), and the wire specification for anyone implementing it in another language.

Two endpoints:

| endpoint | tier | what gates it | |---|---|---| | POST /api/run/<target> | free — the public flair-run targets | signed caller + quota | | POST /api/delivery/<op> | metered — paid work | signed caller + quota + entitlement ledger |

Getting started

npx @nqminds/flair-api-client login

Asks for your email, sends you a code, writes flair.config.json. That file holds your private key — treat it as a password. It is generated on your machine and never sent anywhere; we only ever see the public half.

npx @nqminds/flair-api-client whoami    # who am I, and when does this expire

Credentials stop working after about 13 months. Run login again before then.

The verification email can be slow, and that is out of our hands. Quitting while you wait is safe — the request is saved. When the code arrives:

npx @nqminds/flair-api-client login --code 4754

Same request, same key. Starting a fresh login instead sends a second email and invalidates the code in the first, so resume rather than retry.

Then use it from your own code:

npm install @nqminds/flair-api-client

Or don't — the wire specification below is complete, and plenty of people will sooner write twenty lines in their own language than take a dependency. Both are supported; the spec is the contract, this package is a convenience.

Making requests needs no dependencies at all: index.mjs is one file that imports nothing outside Node. npm install --omit=optional gives you a working caller. The Volt libraries are pulled in only to sign in, which happens once.

Quick start

import { createFlairClient } from "./index.mjs";

const client = createFlairClient({
  baseUrl: "https://flair.nqm.ai",
  configPath: "/path/to/your.volt.config.json",
});

const { result, quota } = await client.run("company-search", ["nquiringminds", "3"]);
console.log(result, `${quota.remaining}/${quota.limit} left this ${quota.window}`);

Long work streams by default and survives a dropped connection:

const { result } = await client.run("company", ["00233462", "psc/get_psc.sh"], {
  onProgress: ({ elapsedMs }) => process.stderr.write(`\r${Math.round(elapsedMs / 1000)}s`),
});

Metered work, which must already be paid for in the entitlement ledger:

await client.deliver("pdf-jobs", {
  companyNumber: "00233462",
  plannedJobs: [{ dateKey: "2024-12-31" }],
});

What you need

A Volt client config for your identity, holding:

  • credential.cert — your session certificate, written after you authenticate
  • credential.key — your private Ed25519 key. Never leaves your machine.
  • credential.vc — an array; element 0 is your issuer-signed email credential

node cli.mjs login produces all three. An anonymous self-minted key will not get in: the server requires a credential from the pinned issuer, and only a verified email address gets one.

There is no separate "API credential". It is an ordinary sign-in; what differs is the rate-limit plan attached to your identity, which we set on our side. Nothing about your credential says "API", and nothing needs to.


The wire specification

Implement this if you are not using Node.

The envelope

Every request body is your request fields plus three auth fields:

{
  "session": { /* see below — cert only, never your private key */ },
  "vc":      { /* ONE credential object, not the array */ },
  "pop":     { "iat": 1756000000000, "sig": "base64url…" },

  "args": ["nquiringminds", "3"]        // …and your actual request fields
}

session

Only credential.cert is read by the server. Send this shape:

{
  "client_name": "your-client",
  "volt": { "id": "", "address": "", "http_address": "" },
  "credential": { "cert": "-----BEGIN CERTIFICATE-----…", "session_id": "…" }
}

Do not post your whole Volt config. It contains credential.key, your private key. Posting it sends your private key to the server on every request, where it lands in request logs and proxy buffers. Send the cert and nothing else that is secret.

vc

credential.vc in a Volt config is an array. Send element 0, not the array. Posting the array is rejected as credential-invalid, which looks like an expired credential and is not.

pop — the part that is easy to get wrong

sig is an Ed25519 signature, base64url, over this exact UTF-8 string:

cmdstream:credits:v1|<label>|<iat>|<stableJSON(body without session/vc/pop)>
  • audience is the literal cmdstream:credits:v1
  • label identifies the route — see the table below
  • iat is your clock in epoch milliseconds, as a decimal string
  • body is everything except session, vc and pop, serialised stably

Stable JSON means:

  • object keys sorted ascending by code unit, at every level
  • undefined / absent values dropped entirely
  • arrays keep their order — only object keys sort
  • otherwise identical to JSON.stringify (same escaping, same number format)

Signing is over the raw message bytes. Ed25519 hashes internally — do not pre-hash, and do not pass a digest algorithm.

Labels

| route | label | |---|---| | POST /api/run/<target> | run:<target> | | POST /api/delivery/pdf-jobs | delivery:pdf-jobs | | POST /api/delivery/tech-classify | delivery:tech-classify |

The label is inside the signed message, so a signature minted for one route cannot be replayed at another. Use the wrong one and you get 401 pop-invalid.

Rules that are not obvious

Sign immediately before sending. A signature is valid for 60 seconds from iat (plus 30s tolerance for a fast clock). If you build a batch and then feed it through a queue, the tail arrives expired. This has already happened once in our own browser client, behind a connection limit.

Long work must stream or poll. The production gateway caps total connection duration, not idle time — heartbeats do not save you. Send Accept: application/x-ndjson and the response is a line-per-frame stream:

{"type":"accepted","jobId":"…"}          // take this first, before anything else
{"type":"progress","elapsedMs":10000}
{"type":"outcome","status":200,"ok":true,"result":{…}}

If the connection dies, POST the same URL with { "jobId": "…" } (signed, same label) every few seconds until state is settled. The work keeps running server-side regardless of your connection — the job id is how you collect it.

Polls need their own signature. Re-sign each one. A poll ten minutes into a job cannot reuse the signature that started it.

Versioning — what you can rely on

Every response carries x-api-version: 1, on success and on failure.

Within a version we may:

  • add a field to a response
  • add an optional field to a request
  • add a new target, op, or reason value
  • reword a human-readable error string

Within a version we will not:

  • remove or rename a response field
  • change the type or meaning of an existing field
  • make an optional request field required
  • change what an existing reason value means

So: read the fields you know, ignore the ones you don't, and branch on reason rather than on error text. A client that does those three things survives every change this version permits.

Anything outside that gets a new version number and a /v2/ path, and v1 keeps answering while you migrate. The header is how you'll notice.

Responses

Success:

{ "ok": true, "result": { /* whatever the backend script printed */ } }

Failure carries a machine-readable reason:

| status | reason | meaning | |---|---|---| | 400 | bad-target, bad-args, too-many-args | malformed request | | 401 | no-session | no cert sent | | 401 | pop-missing, pop-stale, pop-invalid | signature absent, expired, or wrong | | 402 | metered | free route, paid work — use /api/delivery/<op> | | 402 | (delivery) | you have not paid for these resources | | 403 | credential-absent, credential-invalid, credential-key-mismatch | credential problem | | 404 | unknown-target | not a target we serve | | 429 | quota-exceeded | see retry-after | | 502 | bad-output | the backend ran but produced nothing usable |

pop-invalid almost always means your stable-JSON differs from ours by a byte. Check key ordering at every nesting level first.

Quota

Every response carries:

x-quota-plan       browser | api
x-quota-window     minute | day
x-quota-limit      requests allowed in that window
x-quota-remaining
x-quota-reset      seconds until the window rolls
retry-after        only on a 429

Windows are fixed, not sliding, so allowance resets on the boundary rather than rolling. Polls count against a separate, much larger budget than work.

Limits are per identity, not per IP or per connection — so several machines sharing one credential share one budget. Ask us if you need it raised; it is one row on our side, no credential change.

One thing we would rather you heard from us

The free targets read public data, and the same data is reachable anonymously through the website. These rate limits therefore bound this API, not the underlying source. We are asking you to come through here because it is the supported path — it is the one we can see, support, raise limits on, and keep working. Scraping around it gets you no more data, no support, and no warning when something changes.

Free targets

company, charity, contracts and people are orchestrators: argument 1 is the entity id, argument 2 is the script to run, and the orchestrator enforces its own allowlist of those.

smartplot, company-search, company-advanced-search, officer-appointments, company-names and directory-scrape are direct scripts taking their own arguments.

Arguments must be single-line text: no newlines, no control characters, at most 8192 characters each, and at most 24 arguments (200 for company-names, 64 for people).