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

lorica-node

v0.1.5

Published

Node.js SDK for the Lorica biometric verification API — prove a real human authorized it

Readme

lorica-node

Node.js SDK for Attest — biometric attestation for high-stakes actions.

When someone releases a pharma batch, approves a wire, or deletes production data, Attest proves a real, identified human authorized it — and hands you a signed receipt anyone can verify later, even offline, without trusting Attest's database.

  • Node.js >= 20, TypeScript, ESM + CJS.
  • One runtime dependency: jose.

Install

npm install lorica-node

Quickstart

import { LoricaClient } from 'lorica-node';

const attest = new LoricaClient({
  apiKey: process.env.ATTEST_API_KEY!,          // "attest_sk_..."
  baseUrl: 'http://localhost:8000',             // or LORICA_API_URL
});

// 1. Consent first. Written informed consent is required before any face
//    is embedded (BIPA/GDPR) — the server rejects enrollment without it.
//    Fetch the exact disclosure text, show it, and record the "I agree":
const disclosure = await attest.getDisclosure();  // { version, text, sha256 }
showToUser(disclosure.text);                      // your UI — must be shown verbatim
await attest.recordConsent({                      // after an explicit "I agree"
  userId: 'op-1041',
  disclosureVersion: disclosure.version,          // omit to auto-fetch the current one
});

// 2. Enroll the operator once (face + verified identity).
await attest.enroll({
  userId: 'op-1041',
  image: LoricaClient.imageFromFile('./enrollment.jpg'), // Buffer | path | base64
  identity: { name: 'Zoë Müller', role: 'qa_lead', license_id: 'L-9', org: 'acme' },
});
// Or do both in one call — consent is recorded first, then enrollment:
//   await attest.enroll({ userId, image, identity, consent: {} });

// 3. At the moment of the action: attest it.
const { receipt, record } = await attest.attest({
  userId: 'op-1041',
  image: webcamFrame,                            // Buffer | path | base64
  actionType: 'batch_release',                   // letters/digits/_-.: only
  payload: { batch_id: 'BATCH-2210', quantity: 5000, unit: 'vials' },
  ref: 'BATCH-2210',
});
// Store `receipt` (an RS256 JWT) alongside the action it authorized.

// 4. Later — verify. Online:
const check = await attest.verify(receipt);     // { valid, reason, who, what, when, chain }

// ...or offline — verify without asking Attest for a verdict. Pass a JWKS for
// a truly zero-network check (fetch the keys once and pin/persist them, or take
// them from an evidence package's `jwks` block). Omit the arg and the JWKS is
// fetched once over the network and cached (~24h) instead.
const jwks = await attest.fetchJwks();          // GET /.well-known/jwks.json (once)
const offline = await attest.verifyOffline(receipt, jwks);   // zero network
if (offline.valid) {
  // Optionally bind the receipt to the exact payload you're about to act on:
  const bound = attest.verifyPayload(offline.claims!, {
    batch_id: 'BATCH-2210', quantity: 5000, unit: 'vials',
  });
  console.log('authorized by', offline.claims!.identity.name, '— payload bound:', bound);
}

Payload numbers must be JSON-stable. Use integers ({ amount: 50000 }) or decimal strings ({ amount: "50000.00" }) for numeric payload fields — never floats. A whole-number float like 50000.0 serializes as 50000.0 in Python but 50000 in JavaScript, so the SDKs and server reject it to guarantee your receipt verifies in any language. NaN and Infinity are rejected for the same reason. (In JavaScript 50000.0 is 50000, so there is nothing for this SDK to reject — the rule matters when a Python service mints or re-verifies the same payload.)

What a receipt proves

Each attest() call returns a signed JWT whose claims embed:

  • who — the enrolled identity (sub, identity) and a live face match (live, match_score),
  • what — the action type and a canonical sha256 of your payload (action, action_payload.{ref,hash}),
  • when / where in the logiat, seq, prev_hash, and a record_hash binding the whole record into a tamper-evident chain.

verifyOffline() checks the RS256 signature against a JWKS document, recomputes the canonical record hash from the claims, and compares it to record_hash. The server is never asked for a verdict, and the call is zero-network by default: pass a jwks (pinned from an evidence package's jwks block, or from a one-time fetchJwks()); omitting it throws JWKSRequiredError rather than silently fetching. If you want the SDK to fetch the JWKS for you on first use (cached ~24h — server-independent, but not network-free), opt in with allowNetwork: true on the client.

API surface

| Method | Endpoint | |---|---| | getDisclosure() | GET /consent/disclosure (public) | | recordConsent({userId, disclosureVersion?, agree?}) | POST /consent | | getConsent(userId) | GET /consent/{user_id} | | getRetention(userId) | GET /retention/{user_id} | | setRetention(userId, {mode, windowSeconds?}) | POST /retention/{user_id} | | enroll({userId, image, identity, consent?}) | POST /enroll | | attest({userId, image, actionType, payload?, ref?}) | POST /attest | | verify(receipt) | POST /verify | | fetchJwks() | GET /.well-known/jwks.json (cached 24h) | | verifyOffline(receipt, jwks?) | verify with no server verdict; zero-network by default (throws without jwks unless allowNetwork: true) | | verifyPayload(claims, payload) | local | | deleteUser(userId) | DELETE /users/{id} | | audit(filters?) | GET /audit | | exportEvidence(filters?) | GET /audit/export | | getReceipt(recordId) | GET /audit/receipt/{id} | | usage() | GET /usage | | health() | GET /health |

audit filters: { userId, actionType, fromTs, toTs, limit, offset }. exportEvidence filters: { userId, actionType, fromTs, toTs } only — the server ignores limit/offset on /audit/export and always returns up to a hard cap of 1000 records, so the SDK drops them rather than sending no-ops.

recordConsent with no disclosureVersion fetches the current one via getDisclosure() first — only appropriate when the disclosure you displayed IS the current one. Calling it at all asserts your app really showed the text and received an explicit affirmative action from the person.

Retention

Each enrolled user's biometric template has a retention mode (the org-wide schedule is public at GET /retention/policy): 'standing_credential' (default — kept until account closure or explicit deletion) or 'ephemeral' (destroyed once windowSeconds elapse after last use; 0 destroys it right after the action it was captured for).

await attest.getRetention('op-1041');   // { user_id, mode, window_seconds }
await attest.setRetention('op-1041', { mode: 'ephemeral', windowSeconds: 0 });

Errors

All HTTP errors are typed subclasses of LoricaError carrying statusCode, errorCode, and the raw envelope: AuthenticationError (401), MatchFailedError (403 match_failed — the action was not attested), ConsentRequiredError (403 consent_required — enroll attempted with no consent on file), NotFoundError (404), ValidationError (422 — no_face_detected, multiple_faces, image_too_large, invalid_base64, consent_not_affirmative, unknown_disclosure_version, invalid_retention_mode, ...), RateLimitError (429, with rateLimit.retryAfterMs), ServerError (5xx), plus NetworkError / TimeoutError for transport failures. verifyOffline() throws JWKSRequiredError when called without a JWKS document (the zero-network default); configure the client with allowNetwork: true to let it fetch the JWKS once instead.

Idempotent requests (GETs, DELETE, verify(), setRetention()) automatically retry on 429/5xx and transport drops with exponential backoff, honoring Retry-After. attest() also retries: each call mints one Idempotency-Key and reuses it across every attempt, so the server replays the original receipt instead of creating (and billing) a second attestation. (Reusing a key with a different body is rejected server-side with 409 idempotency_conflict; the SDK never does this because the key is minted with the body it is sent with.) enroll() and recordConsent() are never auto-retried — the server does not deduplicate them, so a transient failure there surfaces immediately for you to handle.

Canonical JSON & the float caveat

Payload hashing uses canonical JSON: keys sorted recursively by Unicode code point (matching Python's sort_keys=True, including keys that mix astral and upper-BMP characters), no whitespace, UTF-8, non-ASCII unescaped — byte-identical to Python's json.dumps(obj, sort_keys=True, separators=(",",":"), ensure_ascii=False).

Use integers and strings in payloads, not floats. JavaScript and Python disagree on rendering whole-number floats (1.0"1" vs "1.0"), which would break cross-language hash equality. The Python SDK and the Attest server therefore reject whole-number floats (and NaN/Infinity) in payloads at the boundary, so an ambiguous receipt is never minted; this SDK needs no such check because JavaScript cannot distinguish 1.0 from 1 in the first place. Represent money as integer cents or a decimal string. Fractional floats like 0.97 are fine.

Development

npm install
npm test        # vitest, fully offline (mocked fetch, in-test RSA keypairs)
npm run build   # dual ESM/CJS via esbuild + tsc declarations → dist/