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

@boundbook/sdk

v0.1.0

Published

TypeScript SDK for the BoundBook API — ATF-compliant bound book management with on-chain anchoring and zero-knowledge proofs. Dual-format (ESM + CJS), fully typed.

Downloads

186

Readme

@boundbook/sdk

TypeScript SDK for the BoundBook API — ATF-compliant bound book management with blockchain anchoring and zero-knowledge proofs.

Dual-format (ESM + CommonJS), zero dependencies, fully typed.

Install

npm install @boundbook/sdk

Quick Start

import { BoundBookClient } from '@boundbook/sdk';

const client = new BoundBookClient({
  baseUrl: 'https://api.boundbook.example.com/api/v1',
  getToken: () => getAuthToken(), // async or sync
});

// Create an organization
const org = await client.organizations.create({
  name: 'My FFL',
});

// Add a bound book entry
const entry = await client.boundBook.create({
  locationId: 1,
  manufacturer: 'Glock',
  model: '19',
  serialNumber: 'ABC123',
  type: 'pistol',
  caliberOrGauge: '9mm',
});

Configuration

interface ClientConfig {
  baseUrl: string;                          // API base URL (trailing slashes stripped)
  getToken?: () => string | Promise<string>; // Auth token provider
  fetch?: typeof globalThis.fetch;           // Custom fetch implementation
}

The fetch option is useful for testing (mock fetch) or environments without a global fetch.

API Reference

Health

await client.health.check();
// => { status: 'ok' | 'degraded' }

Auth

await client.auth.sync();   // POST /auth/sync — sync user from identity provider
await client.auth.me();     // GET  /auth/me  — current user with tenant/location

Organizations

await client.organizations.create({ name, ein? });
await client.organizations.get();           // current tenant
await client.organizations.getById(id);     // by ID

Locations

await client.locations.create({ name, address1, city, state, zipCode, fflNumber, fflType, licenseExpiry, address2?, phone? });
await client.locations.list();
await client.locations.get(id);

Users

// Same-org scoped profile lookup. Returns the users row + membership
// link for a member of the caller's org; a cross-org or missing
// target returns 404. Role / permission edits go through the
// members API, not this endpoint.
await client.users.get(userId);

Bound Book (Entries)

// CRUD
await client.boundBook.create({ locationId, manufacturer, model, serialNumber, type, caliberOrGauge, ... });
await client.boundBook.get(entryId);
await client.boundBook.list(locationId);
await client.boundBook.search(locationId, query);

// Status & compliance
await client.boundBook.updateStatus(entryId, { status: 'acquired' });
await client.boundBook.validate(entryId);
// => { valid: boolean, missingFields: string[], warnings: string[] }

await client.boundBook.compliance(locationId);
// => { totalEntries, inInventory, disposed, draft, incompleteEntries, overdueAcquisitions, overdueDispositions }

await client.boundBook.overdue(locationId);
// => OverdueEntry[]

Acquisitions

await client.acquisitions.create({ entryId, acquisitionType, dateAcquired, sourceName, ... });
await client.acquisitions.list(entryId);

Dispositions

await client.dispositions.create({ entryId, dispositionType, dateDisposed, transfereeName, ... });
await client.dispositions.list(entryId);

Documents

// Upload (multipart form data)
const file = new File([buffer], 'form.pdf', { type: 'application/pdf' });
await client.documents.upload(file, { documentType: 'form_4473', entryId: 1, notes: 'Signed copy' });

await client.documents.list(entryId);
await client.documents.get(documentId);
await client.documents.download(documentId);  // => { url, expiresIn }
await client.documents.delete(documentId);

Manufacturers

await client.manufacturers.create({ name: 'Glock' });
await client.manufacturers.list();
await client.manufacturers.createModel(manufacturerId, { name: 'G19' });
await client.manufacturers.listModels(manufacturerId);

Audit

await client.audit.query({ action?, start?, end?, limit?, offset? });
await client.audit.getByEntry(entryId);
await client.audit.exportJson({ action?, start?, end?, limit?, offset? });
await client.audit.exportCsv({ action?, start?, end?, limit?, offset? });

Export

client.export.entriesCsvUrl(locationId);
// => '/export/entries/<locationId>/csv'  (path you fetch with the auth header)

await client.export.exportOutOfBusinessRecords(locationId);
// => { location, entries, generatedAt }

// A&D PDF export history (the dashboard's /print page consumes these).
await client.export.listAdbookExports({ locationId?, limit?, offset? });
client.export.adbookPdfRedownloadUrl(exportId);

Per-entry chain-of-custody data is reachable via boundBook.get(id), audit.getByEntry(id), acquisitions.list(id), dispositions.list(id). The earlier export.trace(entryId) bundle resource was removed — nothing in the dashboard consumed it and the response shape had drifted from the API.

Chain (Blockchain Anchoring)

await client.chain.anchor({ entryId, locationHash });
await client.chain.anchorBatch({ entryIds: [1, 2, 3], locationHash });
// => { stateRoot, batchId, txHash, anchorCount }
await client.chain.attest({ locationHash, dataHash });
await client.chain.verify(dataHash);
// => { committed: boolean, anchor: ChainAnchor | null }

FastBound

FB uses HTTP Basic auth with a per-account API key (FAQ at fastbound.com/faq/authentication/). FastBound's API model is one FFL per account, so multi-location orgs run one FB account per location/FFL. BoundBook's connection table follows that model — every connection is keyed on locationId, and orgs with N locations hold N rows.

// Connect a FastBound account to a specific location.
const status = await client.fastbound.connect(locationId, {
  accountNumber: 'FB-9999',
  apiKey: 'fb-...',           // from FastBound's Settings → API page
  auditUserEmail: '[email protected]',
});
// => { status: 'active', locationId, fbAccountNumber, fbUserEmail,
//      connectedAt, fbOwner: { fflNumber, licenseName, premiseAddress* } }
//
// On connect, the api hits FB's /Account, caches /Account.owner on
// the connection row, and seeds empty FFL fields on the linked
// location from owner data. Refuses with 409 if the location's
// existing fflNumber doesn't match the FB account's fflNumber.

// List every FB connection across this org's locations.
await client.fastbound.listConnections();
// => Array<{ status, locationId, fbAccountNumber, fbOwner, ... }>

// Read the redacted single-location status (never returns the apiKey).
await client.fastbound.status(locationId);

// Disconnect a single location — zeros every secret column on the
// connection row and clears locations.fbStoreId so a re-connect to a
// different FB account is unambiguous.
await client.fastbound.disconnect(locationId);

// Rotate the inbound webhook HMAC secret.
await client.fastbound.rotateWebhookSecret();
// => { newSecret, previousExpiresAt }

Webhook URL: each FB account's webhook is configured to /fastbound/webhook/<locationId> so the verifier can look up the single right secret without a fan-out check across rows.

Inbound sync (FB → BoundBook) is webhook-driven; outbound push-back of on-chain receipts goes through PUT /Items/{id} with a fresh BoundBook anchor tag in the note field. The mirror covers full FB item parity (operational + FB system metadata + theft-loss + destruction + pricing + Lightspeed IDs + notes), plus per-event acquisition / disposition tables that link to N entries via the new acquisition_items / disposition_items join tables. The only FB-side fields not mirrored are attachments[] and multipleSaleReports[] (file/PDF arrays, future plan). See docs/fastbound-integration.md for the full flow.

Locations

Operator-managed CRUD; FB connections attach per location. Read access needs locations:read; create/edit/deactivate need locations:write.

await client.locations.list();
await client.locations.get(locationId);
await client.locations.create({
  name: 'Branch B',
  address1: '500 Oak St', city: 'Springfield', state: 'IL', zipCode: '62702',
  fflNumber: '1-23-456-78-9A-99999', fflType: 'type_01',
  licenseExpiry: '2030-01-01T00:00:00Z',
});
await client.locations.update(locationId, { name: 'Renamed' });
// Soft-delete (sets isActive=false) — chain anchors that reference
// this location stay valid.
await client.locations.deactivate(locationId);

ZK (Zero-Knowledge Proofs)

Each proof type has prove() and verify() methods, plus a proveAndVerify() convenience method that chains both.

// Prove then verify in one call
const result = await client.zk.completeness.proveAndVerify({ entryId: 1 });
// => { valid: boolean, proofType, message, verifiedAt }

// Or separately
const proof = await client.zk.completeness.prove({ entryId: 1 });
const result = await client.zk.completeness.verify({
  proof: proof.proof,
  entryHash: proof.publicInputs[0],
  requiredFieldCount: Number(proof.publicInputs[1]),
  nonce: proof.nonce,
  expiresAt: proof.expiresAt,
});

Proof types with prove + verify + proveAndVerify auto-mapping:

| Sub-resource | Prove input | Purpose | |---|---|---| | zk.completeness | { entryId } | Entry has all required fields | | zk.inclusion | { serialNumber, locationId } | Serial exists in location inventory | | zk.nonTamper | { entryId } | Entry has not been tampered with |

Selective disclosure uses an explicit { prove, verify } pair (no proveAndVerify helper — the verify endpoint requires verifier-typed values that the server cannot synthesize from publicInputs):

// Prove: any combination of fields across the entry + N acquisitions + M dispositions
const bundle = await client.zk.selectiveDisclosure.prove({
  entryId: 1,
  entryFields: ['manufacturer', 'serialNumber'],
  acquisitions: [{ acquisitionId: 5, fields: ['sourceName'] }],
  dispositions: [{ dispositionId: 3, fields: ['transfereeName', 'transfereeDateOfBirth'] }],
});

// Verify: forward the bundle plus values the verifier typed
const result = await client.zk.selectiveDisclosure.verify({
  bundle,
  entryValues: { manufacturer: 'GLOCK', serialNumber: 'ABC123' },
  acquisitionValues: [{ acquisitionId: 5, values: { sourceName: 'Acme Guns LLC' } }],
  dispositionValues: [{ dispositionId: 3, values: { transfereeName: 'Jane Doe', transfereeDateOfBirth: '1990-06-15' } }],
});

result.valid;             // true if every sub-proof verified AND every value matched
result.entryResult;       // per-entity result with fieldMatches[]
result.acquisitionResults;
result.dispositionResults;

publicVerify.checkValues(token, payload) is the unauthenticated companion for verifiers consuming a share token — same payload shape, but the response is intentionally coarse (no per-field detail) to defend PII against enumeration.

Verify-only proof types (no prove or proveAndVerify):

| Sub-resource | Purpose | |---|---| | zk.ownership | Verify ownership proof with public key | | zk.trust | Verify trust chain with license hash |

Verify-only proof types (no prove or proveAndVerify):

| Sub-resource | Purpose | |---|---| | zk.ownership | Verify ownership proof with public key | | zk.trust | Verify trust chain with license hash |

Cross-organization serial trace

Not on this SDK. Cross-org tracer users (LE / regulator) authenticate through Clerk against the dashboard's (tracer) route group at /api/trace/search and /api/trace/disclose/[entryId], not via the per-org API. See docs/platform-admin-architecture.md for the end-to-end picture.

Error Handling

All API errors throw BoundBookError with status helpers:

import { BoundBookError, isBoundBookError } from '@boundbook/sdk';

try {
  await client.boundBook.get(999);
} catch (err) {
  if (isBoundBookError(err)) {
    err.status;            // 404
    err.statusText;        // 'Not Found'
    err.body;              // raw response body

    // Status helpers
    err.isNotFound;        // true
    err.isUnauthorized;    // status === 401
    err.isForbidden;       // status === 403
    err.isValidationError; // status === 400
    err.isRateLimited;     // status === 429

    // Validation errors (400) may have multiple messages
    err.validationMessages; // string[]
  }
}

Types

All types are exported from the package root:

import type {
  // Core models
  Organization, User, UserMemberProfile, Location, Entry,
  Acquisition, Disposition, Document, Manufacturer, Model,

  // DTOs
  CreateOrganizationDto, CreateLocationDto, CreateEntryDto,
  CreateAcquisitionDto, CreateDispositionDto, CreateDocumentDto,
  CreateManufacturerDto, CreateModelDto, UpdateStatusDto,

  // Enums (const arrays + union types)
  FirearmType, EntryStatus, AcquisitionType, DispositionType,
  FflType, DocumentType, AnchorType, UserRole, AuditAction,
  ProofType,
  EntryDisclosedField, AcquisitionDisclosedField, DispositionDisclosedField,

  // Other
  HealthStatus, ComplianceSummary, ValidationResult, OverdueEntry,
  AuditEvent, ChainAnchor, VerifyResult,
  OutOfBusinessRecordsExport, BatchAnchorResult,
  ZkProof, VerificationResult,
  ClientConfig, ApiErrorBody,
} from '@boundbook/sdk';

// Enum arrays are also available as values for runtime validation
import { FIREARM_TYPES, ENTRY_STATUSES, USER_ROLES } from '@boundbook/sdk';

Development

npm -w @boundbook/sdk run build        # Build ESM + CJS + .d.ts
npm -w @boundbook/sdk run dev          # Build with --watch
npm -w @boundbook/sdk run test         # Run tests
npm -w @boundbook/sdk run test:watch   # Run tests with --watch
npm -w @boundbook/sdk run test:coverage # Run tests with coverage