Skip to content

Use from JavaScript

The noqtis npm package is the C library compiled to WebAssembly: one package for Node, browsers, Deno and Bun, with nothing to compile and no dependencies. It needs Node 22 or later.

Terminal window
npm install noqtis

Every call after await init() is synchronous. Bytes go in and come out as Uint8Array; messages, contexts and hash input may also be strings (UTF-8).

noqtis/node reads a folder from disk and writes the seal into it. Save this as prov.mjs and run node prov.mjs:

import { mkdirSync, mkdtempSync, writeFileSync, appendFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { init, sign, kem, hash } from 'noqtis';
import { sealDir, checkDir } from 'noqtis/node';
await init(); // once: loads noqtis.wasm
// a folder with some results
const dir = join(mkdtempSync(join(tmpdir(), 'nq-')), 'results');
mkdirSync(join(dir, 'data'), { recursive: true });
writeFileSync(join(dir, 'data', 'in.csv'), 'a,b\n1,2\n');
writeFileSync(join(dir, 'report.txt'), 'r\n');
// seal it, then check it with the public key pinned
const { publicKey, secretKey } = sign.keygen('ML-DSA-65');
sealDir(dir, 'ML-DSA-65', secretKey, publicKey, { meta: { project: 'tutorial' } });
console.log('check:', checkDir(dir, { trust: publicKey }).ok);
// change a file: the check fails and says why
appendFileSync(join(dir, 'report.txt'), 'edited\n');
const r = checkDir(dir, { trust: publicKey });
console.log('after edit:', r.ok);
for (const c of r.checks.filter((c) => !c.ok)) console.log('FAIL', c.check, c.detail);
// a detached signature with a context string
const msg = 'the results in table 2';
const sig = sign.sign('ML-DSA-65', secretKey, msg, { ctx: 'app-v1' });
console.log('verify:', sign.verify('ML-DSA-65', publicKey, msg, sig, { ctx: 'app-v1' }));
console.log('other context:', sign.verify('ML-DSA-65', publicKey, msg, sig));
secretKey.fill(0); // wipe the secret key when done
// key encapsulation and hashes
const kp = kem.keygen('ML-KEM-768');
const { ciphertext, sharedSecret } = kem.encap('ML-KEM-768', kp.publicKey);
const back = kem.decap('ML-KEM-768', kp.secretKey, ciphertext);
console.log('decap matches:', back.every((b, i) => b === sharedSecret[i]));
console.log('sha3-256:', Buffer.from(hash('sha3-256', 'abc')).toString('hex'));

It prints:

check: true
after edit: false
FAIL file report.txt: size differs
verify: true
other context: false
decap matches: true
sha3-256: 3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532

The first check passes. After the edit it fails, and the failing row names the file and why (report.txt: size differs); the signature verifies only under its own context.

The folder now holds noqtis.json and noqtis.sig, the same capsule the noqtis command and the C library check. checkDir holds the files in memory; for very large capsules use the command-line tool, which streams. CommonJS works too: const nq = require('noqtis'), and the require('noqtis/node') functions return promises.

An identity card ties your signing key to who you are, signed by the key itself; noqtis check --card reads it too. Save this as card.mjs:

import { init, sign, idcard, hash } from 'noqtis';
await init();
const { publicKey, secretKey } = sign.keygen('ML-DSA-65');
const card = idcard.make('ML-DSA-65', secretKey, publicKey, {
name: 'Ada Lovelace',
orcid: 'https://orcid.org/0000-0002-1825-0097',
url: 'https://example.org/ada',
});
const c = idcard.verify(card, { trust: publicKey }); // null unless it is
console.log('name:', c.name);
console.log('orcid:', c.orcid);
const fp = Buffer.from(hash('sha256', publicKey)).toString('hex');
console.log('key:', c.keySha256 === fp);
// a changed card does not verify; a wrong check digit is refused
const text = new TextDecoder().decode(card);
console.log('changed:', idcard.verify(text.replace('Ada', 'Eve')));
try {
idcard.make('ML-DSA-65', secretKey, publicKey,
{ name: 'Ada', orcid: '0000-0002-1825-0098' });
} catch (e) {
console.log('refused:', e.code);
}
secretKey.fill(0); // wipe the secret key when done

It prints:

name: Ada Lovelace
orcid: 0000-0002-1825-0097
key: true
changed: null
refused: -1

The card holds the bare ORCID iD. It is self-signed: it shows that the holder of the key wrote it, not that the name is true, so give the key’s SHA-256 (keySha256) where people already know you, or let readers pin your public key with trust.

In a browser a capsule is a Map of /-separated relative paths to file contents, so sealing and checking happen on the page and nothing is uploaded:

import { init, sign, capsule } from 'noqtis';
/** Seal the files of an <input type="file" webkitdirectory> and check them. */
export async function sealPicked(/** @type {FileList} */ list) {
await init();
const files = new Map();
for (const f of Array.from(list)) {
// webkitRelativePath is "picked-folder/sub/file"; drop the first part
const path = f.webkitRelativePath.split('/').slice(1).join('/');
files.set(path, new Uint8Array(await f.arrayBuffer()));
}
const { publicKey, secretKey } = sign.keygen('ML-DSA-65');
const seal = capsule.seal(files, 'ML-DSA-65', secretKey, publicKey,
{ meta: { project: 'tutorial' } });
secretKey.fill(0);
for (const [name, bytes] of Object.entries(seal)) files.set(name, bytes);
const report = capsule.check(files, { trust: publicKey });
return { seal, ok: report.ok, checks: report.checks };
}
<input type="file" webkitdirectory id="pick">
<script type="module">
import { sealPicked } from './seal.js';
document.getElementById('pick').addEventListener('change', async (e) => {
const r = await sealPicked(e.target.files);
console.log(r.ok, r.checks.length);
});
</script>

To check a capsule someone gave you, put their noqtis.json and noqtis.sig in the map with the other files and call capsule.check(files, { trust: theirPublicKey }). Without trust the check accepts the key the seal carries, and its report says so.

The package loads noqtis.wasm with new URL('noqtis.wasm', import.meta.url), which Vite, webpack 5, esbuild and Rollup handle. Where that is not available, fetch the bytes yourself and pass them: await init({ wasm: bytes }).

import { init, sign } from 'npm:noqtis'; // Deno

Bun imports noqtis as Node does.

  • Randomness comes only from crypto.getRandomValues; if it is missing the call throws, with no fallback.
  • Buffers the package places in WebAssembly memory are wiped before they are freed, but the Uint8Array results are ordinary JavaScript memory: wipe secret keys yourself (secretKey.fill(0)). Strings cannot be wiped, so pass secrets as bytes.
  • A signature that does not verify returns false; bad arguments throw NoqtisError with the library’s code in .code.

The full API is in the package’s README, with TypeScript types in the package.