@deepidv/chain
Node.js / TypeScript SDK for the deepidv chain layer.
Typed access to the public registry API at
api.proof.deepidv.com, plus partial offline verification of
.dpiv-bundle proof archives — five of the six checks defined in
the chain-layer architecture spec, performed in pure Node with zero
runtime dependencies.
The deepidv chain layer is a public, append-only transparency log of identity-verification attestations: every verified session produces a signed, timestamped envelope, every envelope hashes into a Merkle tree, and every tree's root is signed by a chain-master key on a regular schedule and (optionally) anchored to Base. Read proofs anywhere; verify them anywhere.
Install
npm install @deepidv/chain
# or
pnpm add @deepidv/chain
# or
yarn add @deepidv/chain
Requires Node ≥ 20 (uses the global fetch, AbortSignal.timeout,
ReadableStream, and node:crypto.createPublicKey).
Quickstart
import { createClient, verifyBundle } from "@deepidv/chain";
// 1. Look up an attestation in the public registry.
const client = createClient({
apiUrl: "https://api.proof.deepidv.com",
});
const attestation = await client.getAttestation("attest_01JTEST…");
// 2. Download the proof bundle.
const zipBytes = await client.downloadBundle(attestation.id);
// 3. Verify it offline (5 of 6 checks; see "Verification scope").
const result = await verifyBundle(zipBytes);
if (result.ok) {
console.log("✓ verified", attestation.id);
} else {
console.error("✗ failed:", result.reason);
}
Subpath imports (recommended)
The package exposes four subpaths so you can pull in only what you need:
import { createClient } from "@deepidv/chain/client";
import { verifyBundle } from "@deepidv/chain/verify";
import { jcs, envelopeHash, sthHash } from "@deepidv/chain/crypto";
import type { AttestationDetail, RegistryPage } from "@deepidv/chain/types";
Each subpath ships dual ESM + CJS with .d.ts declarations.
API surface
createClient(options) → DeepidvChainClient
| Method | Path | Returns |
|---|---|---|
getAttestation(id) |
GET /v1/attestation/:id |
AttestationDetail |
listRegistry(filters) |
GET /v1/registry |
RegistryPage |
getIssuer(id) |
GET /v1/issuer/:id |
IssuerDetail |
getSegment(n) |
GET /v1/segment/:n |
SegmentDetail |
listSths(segment) |
GET /v1/sth?segment= |
SthListResponse |
getConsistencyProof(from, to, segment) |
GET /v1/consistency |
ConsistencyProofResponse |
getLog() |
GET /v1/log |
{ segments: SegmentDetail[] } |
downloadBundle(id) |
GET /v1/bundle/:id |
ArrayBuffer |
streamAttestations({signal}) |
GET /v1/stream (SSE) |
AsyncIterable<StreamEvent> |
All methods accept an optional { signal, timeoutMs } and throw
typed errors:
DeepidvAuthError— 401 / 403DeepidvNotFoundError— 404DeepidvRateLimitError— 429 (parsesRetry-AfterintoretryAfterSeconds)DeepidvServerError— 5xxDeepidvNetworkError— fetch / DNS / abortDeepidvApiError— common base
import { DeepidvRateLimitError } from "@deepidv/chain";
try {
await client.listRegistry();
} catch (err) {
if (err instanceof DeepidvRateLimitError && err.retryAfterSeconds) {
await new Promise((r) => setTimeout(r, err.retryAfterSeconds * 1000));
}
throw err;
}
Live attestation stream
const ctrl = new AbortController();
for await (const ev of client.streamAttestations({ signal: ctrl.signal })) {
if (ev.type === "attestation.minted") {
console.log(ev.payload.id, ev.payload.recordType);
}
}
The iterator manages reconnection internally — exponential backoff,
jittered, capped at 30 s, with Last-Event-ID resumption.
Aborting the signal stops iteration cleanly.
Crypto primitives
import {
jcs,
sha256Hex,
envelopeHash,
sthHash,
serializeManifest,
parseManifest,
leafHash,
verifyInclusion,
} from "@deepidv/chain/crypto";
const h = envelopeHash({ v: 1, t: "IDV", id: "attest_01", /* … */ });
// ^- byte-identical with the Python SDK and the backend
JCS (RFC 8785) canonicalization, RFC 6962 leaf/node hashing, and sha256sum-compatible MANIFEST.txt serialization. Cross-language parity is locked down by fixture tests against the same constants the Python SDK and the backend assert.
Verification scope
verifyBundle() performs 5 of 6 checks defined in the
ARCHITECTURE.md §8 D.5 spec:
| # | Check | Result type |
|---|---|---|
| 1 | envelope_hash |
boolean |
| 2 | issuer_signature |
boolean |
| 3 | tsa_tokens |
"skipped" |
| 4 | merkle_inclusion |
boolean |
| 5 | master_sth_signature |
boolean |
| 6 | onchain_anchor |
boolean | "absent" |
TSA token verification (RFC 3161 against the DigiCert and Sectigo CA chains) is deliberately skipped. Pulling a full ASN.1 + RFC 3161 verifier into a zero-dependency SDK would add ~200 KB of transitive deps. The status is reported as the literal string
"skipped"— never a boolean — so callers can't mistake it for a passing check.If you need full six-of-six verification, run the bundle's own
verify.shcompanion. It usesopenssl ts -verifyand is the canonical TSA verifier by design.
const r = await verifyBundle(zipBytes);
// ↳ { ok, checks, skipped: ["tsa_tokens"], reason?, onchainReference? }
if (r.ok && r.checks.tsa_tokens === "skipped") {
// 5 of 6 checks passed. Run verify.sh from the bundle for the
// canonical TSA check if your use case requires it.
}
The on-chain anchor check (#6) is informational only — it
verifies the structural fields in onchain.json against
sth.json and exposes the on-chain reference, but does not call an
RPC. Live transaction confirmation is a verify.sh concern (and
even there is informational per ARCHITECTURE.md §8 D.7).
Privacy
- Claim bodies are never on the wire — the envelope carries only
claim_hash. Tenants store the canonical claim themselves. - Label salts are stored only when an attestation is built with an explicit reveal-set. A salt-bearing label is always the result of the subject opting to reveal it. The SDK never logs salt values. Consumers building UIs MUST gate any salt rendering behind an explicit reveal flow.
- The SDK does not send authentication headers. The public registry is unauthenticated by design — every attestation is globally readable, and the privacy model lives at the envelope level (subject pseudonym, claim hash) and the label layer (salted commitments).
Versioning
Semver. The SDK is at 1.0.0 and tracks the chain layer's v1 wire
formats. Forward-compatibility is built in:
- New
recordTypevalues (e.g.BIO,DOC,ADDRin Phase 2) are semver-minor. - New
StreamEvent.typevalues are semver-minor — consumers switch ontypeand ignore unknowns. - A new envelope version (v2, TripleLock v3) is semver-major.
- Drift in JCS canonicalization, MANIFEST.txt format, or the envelope/STH hash preimage is a breaking change requiring a coordinated update with the Python SDK and the backend's shared-deps. The fixture tests will fail loudly if you forget.
Examples
See examples/ for runnable scripts:
examples/registry-search.ts— paginate the registry with filtersexamples/verify-bundle.ts— download + verify an attestationexamples/sse-stream.ts— subscribe to live attestations
Run any example with:
npx tsx examples/verify-bundle.ts <attestation_id>
Contributing
Issues and PRs welcome at
github.com/Deep-Identity-Inc/deepidv-chain-node.
Wire-format changes (envelope, STH, manifest, bundle layout)
require coordinated PRs against the Python SDK and the backend's
shared-deps; see the parity fixtures under test/fixtures/.
License
Apache-2.0 Deep Identity Inc.
The SDK and the chain layer it talks to are part of deepidv, a verification engine and agentic compliance suite operated out of San Francisco.