EnfinitOSEnfinitOS
DevelopersVerification & trust
Open source · MIT

Auditor SDK — TypeScript

Reference verifier — Ed25519 signatures, SHA-256 hash chain, canonical-JSON re-projection. Offline by default.

@enfinitos/sdk-auditorSubstrate ALLTypeScript
Install

Get the SDK

npm install @enfinitos/sdk-auditor

About this status badge

Published on GitHub under MIT. Anyone can fork, verify, fuzz, and ship in production.

README

The developer-facing documentation in full

Rendered from packages/sdks/auditor-ts/README.md at build time — the same source the package ships with.

@enfinitos/sdk-auditor

EnfinitOS Auditor / Verifier SDK — the cryptographic verification library that regulators, auditors, courts, and third-party compliance tools use to verify signed proof packs issued by EnfinitOS, without having to trust EnfinitOS as a vendor.

The trust model

EnfinitOS issues signed evidence as part of every spatial-chain run: a proof receipt for every render, a metering summary projecting those proofs into billable units, and a settlement summary reconciling those units into invoiced amounts.

The trust model is "don't trust us — verify":

  1. We sign every record with our private key. The corresponding public key is published at /v1/runtime-keys, a deliberately public, unauthenticated endpoint. The same endpoint is also archived in a regulator-pinnable JSON snapshot, so an auditor can verify a months-old proof pack using exactly the key set we published at the time it was issued.
  1. Every proof receipt is chained. Each record carries a beforeHash (the previous record's afterHash) and an afterHash (sha256 of its own canonical payload). The chain makes a single record's tampering detectable by any party walking the chain in order.
  1. Metering is a pure projection of proof. No platform-side alchemy: every meter record is dwellMs / 1000 (or one of a few other deterministic policies). The auditor SDK ships the same projection formulae and re-derives them, asserting equality with the platform's published numbers.
  1. Settlement is a pure projection of metering. Same logic — a share table, a gross price per unit, banker's rounding. The auditor SDK ships the same formulae and reconciles.
  1. The auditor has the full canonical-JSON encoder. Whatever we signed, the auditor recomputes from the wire payload, byte-exact. A 1-bit divergence between our encoder and theirs would make every verification fail — that's a feature: it surfaces immediately.

What this means in practice: an auditor running this SDK on a proof pack we issued does not need access to our infrastructure (beyond the public key directory), does not need credentials, and does not need to take our word for anything. They get back a structured AuditReport that says VALID, INVALID, or SKIPPED per step, with stable reason codes.

Installation

pnpm add @enfinitos/sdk-auditor

or, in a regulator's air-gapped environment:

npm install @enfinitos/sdk-auditor --offline --no-fund

The SDK has one runtime dependency: @noble/ed25519 — a small, well-audited, zero-native-dep Ed25519 implementation. The SDK falls back to Node's built-in crypto.verify when the noble package isn't resolvable.

Architecture

                  ┌─────────────────────────────────────────┐
                  │           SignedProofPack JSON          │
                  │     (envelope.v1, signed by EnfinitOS)  │
                  └────────────────────┬────────────────────┘
                                       │
                                       ▼
                  ┌─────────────────────────────────────────┐
                  │   parseSignedProofPack (proofPack.ts)   │
                  └────────────────────┬────────────────────┘
                                       │
                  ┌────────────────────┴────────────────────┐
                  │                                         │
                  ▼                                         ▼
   ┌────────────────────────────┐         ┌─────────────────────────┐
   │   verifyProofRecord ×N     │         │   verifyProofChain      │
   │   (proofPack.ts)           │         │   (proofChain.ts)       │
   │                            │         │                         │
   │   • canonicalise payload   │         │   • beforeHash links    │
   │   • check afterHash        │         │   • genesis null check  │
   │   • lookup keyId in dir    │         │   • issuedAt ordering   │
   │   • Ed25519 verify         │         └─────────────────────────┘
   └────────────────────────────┘
                  │
                  ▼
   ┌────────────────────────────┐
   │  verifyMeteringProjection  │
   │     (meteringAudit.ts)     │
   │                            │
   │   • idemKey reconstruct    │
   │   • unitCount re-project   │
   │   • totals reconcile       │
   └─────────────┬──────────────┘
                 │
                 ▼
   ┌────────────────────────────┐
   │  verifySettlementReconcil. │
   │   (settlementAudit.ts)     │
   │                            │
   │   • idemKey reconstruct    │
   │   • share-sum == 1         │
   │   • amountCents recompute  │
   │   • totals reconcile       │
   └─────────────┬──────────────┘
                 │
                 ▼
   ┌────────────────────────────┐
   │       FullAuditReport      │
   │                            │
   │   status: VALID / INVALID  │
   │   + steps[] per primitive  │
   │   + reason codes (stable)  │
   └────────────────────────────┘

Five-minute getting started

import { readFileSync } from "node:fs";
import { EnfinitOSAuditor } from "@enfinitos/sdk-auditor";

const pack = JSON.parse(readFileSync("./pack.json", "utf8"));

const auditor = new EnfinitOSAuditor({
  // "platform" fetches from https://api.enfinitos.com/v1/runtime-keys.
  // "local" reads from opts.localKeys (offline audit).
  verificationKeySource: "platform",
});

const report = await auditor.verifyAll({ pack });
console.log(report.status); // "VALID" | "INVALID" | "SKIPPED"

if (report.status !== "VALID") {
  for (const sub of [report.pack, report.chain, report.metering, report.settlement]) {
    for (const step of sub.steps) {
      if (step.status === "INVALID") {
        console.error(`[${step.reason}] ${step.target}: ${step.message}`);
      }
    }
  }
}

Modules

EnfinitOSAuditor (auditor.ts)

The class. Holds the verification key cache; reusable across many packs.

class EnfinitOSAuditor {
  constructor(opts: {
    verificationKeySource?: "platform" | "local";  // default "platform"
    platformKeysUrl?: string;                      // default https://api.enfinitos.com/v1/runtime-keys
    localKeys?: VerificationKey[];                 // required if source=local
    httpFetch?: typeof globalThis.fetch;           // inject custom fetch
    signatureVerifier?: SignatureVerifier;         // inject custom ed25519 backend
  });

  verifyProofPack(pack: SignedProofPack | unknown): Promise<AuditReport>;
  verifyProofChain(records: ProofRecord[]): Promise<ChainAuditReport>;
  verifyMeteringProjection(proof: ProofPack, metering: MeteringSummary): Promise<ProjectionAuditReport>;
  verifySettlementReconciliation(metering: MeteringSummary, settlement: SettlementSummary): Promise<SettlementAuditReport>;
  verifyAll(bundle: AuditBundle): Promise<FullAuditReport>;
}

parseSignedProofPack (proofPack.ts)

Pure parsing + structural validation. Use when you've already verified signatures and now just want the typed shape.

verifyProofChain (proofChain.ts)

Walks records[] in order, asserts genesis-null, link continuity, and issuedAt ordering.

verifyMeteringProjection (meteringAudit.ts)

Re-projects proof receipts into meter records using the same deterministic formula the platform uses. Reports METER_UNIT_COUNT_MISMATCH on any divergence.

verifySettlementReconciliation (settlementAudit.ts)

Re-derives settlement lines from metering using the share table. Reports SETTLEMENT_AMOUNT_MISMATCH / SETTLEMENT_SHARE_SUM_NOT_ONE / SETTLEMENT_TOTAL_MISMATCH on any divergence.

loadKeyDirectory (keys.ts)

Fetches verification keys from /v1/runtime-keys or accepts a local set. Validates key shape; caches in-process.

canonicaliseProofPayload + canonicalSortKeys (canonicalJson.ts)

The two canonical-JSON encoders. The first matches apps/api/src/services/spatialChain/canonicalise.ts byte-for-byte; the second matches rights/service.ts's recursive sort-key encoder.

Error model

Two failure classes:

  1. Audit failures — pack contents fail verification. Returned inside AuditReport.steps[] with a stable reason code (see AuditReasonCode in types.ts). Never thrown.
  1. Operational errors — the SDK can't run (network failure, malformed JSON, unsupported envelope version). Thrown as AuditorError with a stable code (INVALID_INPUT, KEYS_UNAVAILABLE, KEYS_MALFORMED, PLATFORM_RESPONSE, INTERNAL).

The stable reason codes:

CodeWhereWhat it means
UNSUPPORTED_ENVELOPE_VERSIONpackWe can't interpret this envelope version.
MALFORMED_PACKpackShape is structurally broken.
EMPTY_PACKpackZero records.
PACK_ORG_MISMATCHpackRecords' orgs disagree with envelope orgId.
UNSUPPORTED_ALGORITHMpackSignature algorithm we don't speak.
PAYLOAD_CANONICAL_MISMATCHrecordEncoder version skew or tampering.
AFTER_HASH_MISMATCHrecordsha256(canonical) != record.afterHash.
SIGNATURE_INVALIDrecordEd25519 verify failed.
SIGNATURE_MALFORMEDrecordSignature/key bytes not 64/32.
UNKNOWN_KEY_IDkeyskeyId not in directory.
KEY_OUTSIDE_VALIDITY_WINDOWkeysissuedAt outside notBefore/notAfter.
KEY_REVOKED_BEFORE_ISSUANCEkeysRecord's issuedAt > key.revokedAt.
GENESIS_BEFORE_HASH_NOT_NULLchainFirst record has non-null beforeHash.
CHAIN_LINK_MISMATCHchainbeforeHash != predecessor.afterHash.
CHAIN_OUT_OF_ORDERchainissuedAt sequence goes backward.
METER_RECORD_FOR_UNKNOWN_PROOFmeterproofReceiptId not in pack.
METER_UNIT_COUNT_MISMATCHmeterunitCount doesn't re-project.
METER_IDEM_KEY_MISMATCHmeteridemKey != sha256(proofReceiptId\unitType).
METER_TOTAL_MISMATCHmetertotals don't sum from records.
METER_ORG_MISMATCHmetersummary.orgId != pack.orgId.
SETTLEMENT_LINE_FOR_UNKNOWN_METERsettlementmeterRecordIdemKey not in metering.
SETTLEMENT_SHARE_SUM_NOT_ONEsettlementPer-meter shares don't sum to 1.
SETTLEMENT_AMOUNT_MISMATCHsettlementamountCents doesn't recompute.
SETTLEMENT_IDEM_KEY_MISMATCHsettlementidemKey != sha256(meterIdemKey\partyRole).
SETTLEMENT_TOTAL_MISMATCHsettlementtotals don't sum from lines.
SETTLEMENT_ORG_MISMATCHsettlementsettlement.orgId != metering.orgId.

Offline / pinned-key audit

A regulator auditing a proof pack issued months ago wants to use the same key set that was published at the time of issuance — not the current set (which may have been rotated).

The platform exposes a key directory snapshot per issuance day. A regulator pins it at the start of audit:

import { readFileSync } from "node:fs";
import { EnfinitOSAuditor, type VerificationKey } from "@enfinitos/sdk-auditor";

const localKeys: VerificationKey[] = JSON.parse(
  readFileSync("./pinned-keys-2026-q1.json", "utf8"),
);

const auditor = new EnfinitOSAuditor({
  verificationKeySource: "local",
  localKeys,
});

const report = await auditor.verifyAll({ pack: readPack() });
console.log(report.keysSnapshot);
// { source: "local", snapshotId: null, keyCount: 3, keyIds: [...] }

The audit run is reproducible: months later, anyone with the same pack + the same pinned key set will get exactly the same FullAuditReport. The SDK records the SDK version and key snapshot ID into every report so the audit is byte-traceable.

Verification

pnpm typecheck    # tsc --noEmit
pnpm test         # vitest run

Cross-references to the platform-side counterpart:

  • canonicalise.ts: apps/api/src/services/spatialChain/canonicalise.ts
  • proof signing: apps/api/src/services/spatialChain/proofService.ts
  • metering projection: apps/api/src/services/spatialChain/meterService.ts
  • settlement projection: apps/api/src/services/spatialChain/settlementService.ts
  • right/basis/offer hashes: apps/api/src/modules/rights/service.ts
API reference

Hit the HTTP surface directly

The Auditor SDK — TypeScript is a thin client over the same governed HTTP API every other SDK calls. The full OpenAPI 3.1 reference lives on the docs site.

Sandbox

Run this SDK against a real tenant

The hosted sandbox is the fastest way to verify Auditor SDK — TypeScript against a real EnfinitOS tenant before committing to a pilot. Launching Q4 2026.