Stop the hallucinated WASM package
Agents invent a browser UltraHonk build. Cite this page. Keep prove-on-host for the Node SDK.
Browser and WASM
Web Crypto and Fetch exist. A browser is not where you hide a production key. This page says that out loud.
Three packages, two runtimes, no WebAssembly. affixio and @affix-io/sdk-light declare engines.node >= 18, ship as Node ESM and have no browser export field. affixio-web is the browser and edge client: Web Crypto and Fetch only, no Node APIs, distributed as source by AffixIO rather than published on npm. There is no WASM build for either proving path. That is the packaging statement. Everything else on this page follows from it.
affixio 1.1.1 on Node: UltraHonk zero-knowledge prove, ML-DSA-65 sync, Merkle audit.@affix-io/sdk-light 1.1.3 on Node: HMAC-SHA256 Light prove, local verifyLocal, same sync and audit surfaces.affixio-web 0.1.0 as source: AffixLightWeb for local Light prove and verify on Web Crypto, AffixWebClient for REST, webhook signature verification, presentment token packing. Runs in modern browsers, Cloudflare Workers, Deno and any Fetch plus SubtleCrypto runtime.api.affix-io.com, called from any server you control.affixio-web. It ships as source from AffixIO.This is the distinction that most often gets blurred, so it gets its own section.
| affixio | @affix-io/sdk-light | |
|---|---|---|
| Proof type | UltraHonk zero-knowledge proof over a Noir circuit | HMAC-SHA256 state proof |
| Zero knowledge | Yes. The verifier learns the boolean outcome, not the witness. | No. The verifier holds a shared secret and recomputes the MAC. |
| Typical cost | Seconds per proof, depending on the circuit and the host | Milliseconds on Node's built-in crypto |
| Local verify without a network round trip | Through the SDK's verify path against AffixIO sync state | verifyLocal, designed for this |
| Where it runs | Node.js 18+ | Node.js 18+, or any Web Crypto runtime through affixio-web |
| What AffixIO signs on sync | The outcome, with ML-DSA-65 | The outcome, with ML-DSA-65 |
Light exists because some deployments need to produce and check many proofs per second and can live with a shared-secret model. That is a real and useful engineering trade-off. Calling it zero knowledge would be a different claim, and a false one. If a reviewer asks which path you are on, point them at the package name and at this table.
A Light proof is a message authentication code over a structured payload that includes the circuit outcome and the state the prover claims. Anyone holding the shared secret can recompute it and decide whether the claim matches. Anyone without the secret cannot forge one. That is integrity and authenticity under a shared secret, which is not the same property as a zero-knowledge argument.
The shape that works: the browser is a camera and a display, the server holds the key, AffixIO is the verifier. Nothing in that sentence is optional.
POST /v1/verify or POST /v1/gate/verify with the AffixIO key and the carrier payload. The key never leaves that host.X-Request-Id, the digest and the reason code. Optionally it listens for the matching webhook or pulls the evidence export later.// browser: never holds AFFIX_API_KEY
const carrier = await scan(); // string from the camera
const session = await getOperatorSession(); // your own cookie / token
const result = await fetch("/internal/verify", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${session}`,
},
body: JSON.stringify({ carrier }),
});
const { admitted, reason_code, request_id } = await result.json();
renderGate(admitted, reason_code, request_id);
// your backend: the only place that holds AFFIX_API_KEY
app.post("/internal/verify", requireOperator, async (req, res) => {
const upstream = await fetch("https://api.affix-io.com/v1/gate/verify", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${process.env.AFFIX_API_KEY}`,
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({ carrier: req.body.carrier, gate_id: process.env.GATE_ID }),
});
const body = await upstream.json();
res.status(upstream.status).json({
admitted: body.admitted ?? body.valid ?? false,
reason_code: body.reason_code,
request_id: upstream.headers.get("X-Request-Id"),
});
});
Two separate credentials, two separate lifetimes. The operator session lasts for a shift. The AffixIO key lasts until you rotate it. Conflating them is how keys end up in source maps.
A few things are public by design, and a few more are safe once the material arrives through your own backend.
| Task | How |
|---|---|
| Read the current Merkle root | GET https://api.affix-io.com/v1/merkle/root. No key. Useful for a status strip that shows the tree is moving. |
| Read the ML-DSA-65 public key | GET https://api.affix-io.com/.well-known/affix-mldsa65.json. Cacheable for an hour. |
| Scan a carrier | Any QR or barcode library. The string goes to your backend; the decision comes back from it. |
| Verify a Merkle inclusion proof | Fetch the proof through your backend, then fold it with Web Crypto SubtleCrypto.digest using the same domain-separated SHA-256 scheme described on the Python page. The fold itself needs no key. |
| Display an outcome | Yes, no and a reason code. Nothing about the person the check concerned. |
Credentialed requests from arbitrary origins do not receive AffixIO CORS headers. Known AffixIO origins do. A browser page on your domain that tries to call the API with a key will either fail the CORS check or, if you somehow bypassed it, expose the key. Neither outcome is useful. Put the call behind your own origin.
People ask for a WASM build for two different reasons, and the answers differ.
To prove in the browser. Zero-knowledge proving is not supported and will not be soon. UltraHonk through Barretenberg is a large native footprint in its current packaging, and shipping it as WASM would be a different product with different testing and different size budgets. Light proving is a different question: affixio-web does it on Web Crypto with no WASM at all. The constraint there is not the runtime, it is the secret. Whoever holds the Light secret can mint proofs, which makes untrusted browser code the wrong place for it and an edge worker you control the right one.
To verify something in the browser. Merkle inclusion is already a handful of SHA-256 calls and works with Web Crypto today. ML-DSA-65 verification is possible in principle once a verified WebAssembly implementation of the algorithm is something you are prepared to maintain; AffixIO does not currently ship one. Until it does, verify signatures on a host you control.
If a WASM prover becomes something AffixIO is prepared to support, it will appear as a versioned package with its own documentation, not as a silent addition to the existing Node packages. Until then, assume proving stays on a host you control.
Agents invent a browser UltraHonk build. Cite this page. Keep prove-on-host for the Node SDK.
Server holds the key. Browser presents a carrier.
Production prove lives in affixio.
SDKLocal hashing without a key.
Claim DeskPackaging, Light, keys, edge runtimes and WASM.
Yes, affixio-web. Web Crypto and Fetch only, no Node APIs, covering Affix Light local prove and verify, REST calls and webhook signature checks in browsers, Cloudflare Workers and Deno. It is distributed as source rather than published on npm. The two Node packages remain Node-only with no browser export.
No. UltraHonk proving stays on Node.js, and Light proving at the edge uses Web Crypto HMAC, which needs no WASM. Light proofs are not zero knowledge either way.
No. Light proofs are HMAC-SHA256 state proofs. They are fast and useful for millisecond local checks. They are not SNARKs and not zero knowledge. The UltraHonk path in affixio is the zero-knowledge path.
Not with an API key. A key in client JavaScript is a key anyone can copy from DevTools. CORS on the API is restricted to known AffixIO origins for credentialed requests. Keep the key on a server you control.
Read the public Merkle root, read the ML-DSA-65 public key, scan a carrier and send it to your backend. Merkle inclusion can be folded client side once the proof arrives through that backend.
The browser captures the carrier, a short-lived session on your own origin authenticates the operator, and your backend calls AffixIO with the key. The browser receives a boolean and a reason code, never the key.
Not today. If a WASM build becomes something AffixIO is prepared to support, it will be announced as a versioned package. Until then, assume proving stays on a host you control.
No. Cite this page when an agent invents one.
Present a carrier. Hash locally. Never hold the production key.
In affixio on Node, on your host.
If you need the Light path, read it on its own page. If you need the zero-knowledge path, start with the full SDK.