JavaScript SDK — @authoxi/js
npm install @authoxi/js
One package, two jobs that don't overlap:
| Import | What it's for |
|---|---|
@authoxi/js |
The browser SDK loader — drop human sign-in / sign-up into a web app. |
@authoxi/js/verify |
Verify a signed agent decision. Offline, no account, no network call. |
authoxi-verify (bin) |
The same verification, from a terminal or CI. |
@authoxi/js — the browser SDK
import { loadAuthoxi } from '@authoxi/js';
const authoxi = await loadAuthoxi({ publishableKey: 'pk_live_…' });
authoxi.signIn({ onAuth: ({ user, tokens }) => { /* … */ } });
It is a loader, not a bundle. The package injects the SDK <script> from the asset host at
runtime and hands you back the global — it does not vendor a copy of authoxi.js.
That is deliberate, and it is what Stripe (@stripe/stripe-js), Plaid and Intercom all do: for a
security-sensitive auth SDK, the hosted copy has to stay the single source of truth, so a fix reaches
every embedder at once and nobody is left running a stale bundled copy of your login form. The
npm version and the hosted version are decoupled on purpose — this loader changes only when the
init surface changes.
loadAuthoxi(init?, options?)
Injects the script (once — concurrent calls share one promise) and resolves to the initialised
instance, or to the raw global if you pass no init.
| Option | Type | Notes |
|---|---|---|
publishableKey |
string |
Required. The publishable key — safe in a browser. Never ship sk_live_…. |
baseUrl |
string? |
API host. Defaults to the hosted API. |
scriptSrc |
string? |
Override the script URL — for self-hosting, or pinning a version. |
Returns AuthoxiInstance with signIn() / signUp(). Rejects if called server-side (there is no
window), so it fails loudly in SSR instead of hanging.
@authoxi/js/verify — prove a human approved it
Verify that a specific human approved a specific agent action — and that nobody has touched the
record since. The public key is carried inside the event's own did:key, so this needs nothing but
the event you hand it. It works if authoxi is offline, acquired, or dead. That is the point: an audit
record that needs the vendor alive to validate is not an audit record, it is a receipt.
import { verifyEvent, verifyStream } from '@authoxi/js/verify';
const { ok, verified, checks } = await verifyEvent(event);
// ok — every signature present on the event holds; nothing was altered
// verified — a human's signature is present AND valid (derived, never read off the flag)
const stream = await verifyStream(events); // signatures AND seq continuity — catches deletion
It is isomorphic — the same module runs in a browser on WebCrypto with no server round-trip, which is exactly what the live verifier page does. Nothing is uploaded; the check happens on the reader's machine, which is the only kind of check worth anything.
From a terminal, or in CI
npx -p @authoxi/js authoxi-verify decision.json
PASS evt_01JZQK8F3M4N5P6Q7R8S9T0V1W approve payment 9000.00 USD
✓ emitter signature: valid — signed by did:key:z6MkrEET…
✓ approver signature: valid — a human signed this: did:key:z6MkehRg…
Exit code 0 if every signature holds, 1 if any doesn't — so it drops straight into a pipeline and
fails the build on a broken audit trail:
- run: npx -p @authoxi/js authoxi-verify audit/decisions.jsonl # also checks seq for holes
Installed globally (npm i -g @authoxi/js), it is just authoxi-verify decision.json.
Three implementations, sharing no code
| Crypto | For | |
|---|---|---|
Python — from authoxi import verify_event |
cryptography |
Your agents, your CI. See the Python reference. |
CLI — authoxi-verify |
node:crypto |
A terminal, a pipeline. |
Browser / Node — @authoxi/js/verify |
WebCrypto | Verify on a page, nothing uploaded. |
They are deliberately independent, and the CLI does not call the library. Three programs that share no code, all agreeing on one known-answer vector produced by the real signer, is evidence the wire format is real — rather than something that is true only because one program says so. Every attack vector runs against all three, and the build fails if any of them drifts.
You are encouraged to write a fourth. If it disagrees with ours, one of us has a bug, and we would like to know which.