burnledger
v0.10.0
Published
TypeScript SDK for the BurnLedger API — cryptographic deletion certificates
Maintainers
Readme
BurnLedger TypeScript SDK
TypeScript/JavaScript client for the BurnLedger API — cryptographic deletion certificates for regulatory compliance.
Scope
The SDK covers the data plane: systems, attestations, certificates, webhooks,
API keys, /v1/me, and the transparency log — everything involved in
measuring a datastore and verifying what came back.
Account management (teams, TOTP enrolment, subscriptions, invoices, the audit
log, billing) is deliberately not part of the SDK. Those are administrative
actions people take once, in the dashboard at
https://dashboard.burnledger.io, and they are available on the raw /v1 API
for anyone who needs to automate them.
Install
npm install burnledgerRequirements: Node.js 18+
Quick Start
import { BurnLedger } from "burnledger";
const dp = new BurnLedger({ apiKey: "dp_..." });
// 1. Attest — snapshot systems before deletion
const att = await dp.attest("[email protected]", {
systemIds: ["sys_abc", "sys_def"],
});
// 2. Delete data (your code, your tools)
// 3. Verify — confirm deletion and get certificate
const result = await dp.verify(att.id, "[email protected]", { timeout: 60 });
// 4. Download certificate PDF
await dp.savePdf(result.certificate!.id, "./deletion-cert.pdf");Browser Support
For browser environments (verification only, no API client):
import { verifyCertificate, publicKeyFromHex } from "burnledger/browser";Uses WebCrypto (crypto.subtle) instead of node:crypto. Requires Chrome 113+, Firefox 128+, or Safari 17+.
Offline Verification
Verify certificates without network access using Ed25519 signatures:
import {
verifyCertificate,
verifyTransparency,
publicKeyFromHex,
} from "burnledger";
const key = await publicKeyFromHex("abcdef...", { revoked: false });
const keys = new Map([[key.keyId, key]]);
const certResult = await verifyCertificate(certificate, keys);
// "VALID" or a soft key verdict; throws VerificationError on failure
const logResult = await verifyTransparency(certificate, keys);
// "INCLUDED" | "NOT_AVAILABLE"; throws VerificationError on an invalid proofMalformed input — a missing field, a value of the wrong JSON type, bytes that
are neither hex nor base64, a hash or signature of the wrong length — throws
VerificationError too, with a message naming the field
(systems[0].query_hash is 31 bytes, expected 32). A record in a format this
version cannot read throws it with code === UNSUPPORTED_FORMAT_VERSION.
Requiring a registration
From format 9.0 every system in a record says whether the enclave checked it
against a registration (authorization: certified, with the customer_key_id
it was registered under) or not (authorization: none). A none record is
still genuine, and verifies by default: whoever relays requests to the enclave
can always leave a registration out, and the enclave cannot refuse that. What
nothing but you can do is refuse the record, so ask for it, naming your key id:
import { AUTHORIZATION_POLICY_FAILED, verifyCertificate } from "burnledger";
await verifyCertificate(certificate, keys, {
requireAuthorization: { customerKeyIds: ["cust_k_..."] }, // every system, your key
});What a pass means: every required system is certified under a key id you
named, in a record from an enclave image of wire protocol 12 or later — the
first that registers a system only with the signatures of the key group that id
names. So that key group signed the registration. Earlier images, which include
every image that has issued format 9.0 so far, could register a system with no
customer signature, so a certified record from one of them fails the policy,
and so does a record that names no image. The key id is required for the same
reason: certified under a key you did not name could be under a key group the
relay enrolled itself.
systems narrows the requirement to the system ids you name. A record that does
not meet the policy throws VerificationError with
code === AUTHORIZATION_POLICY_FAILED, naming the reason. A record older than
9.0 always fails: nothing signs its authorization field.
verifyCertificateWithStatus takes the same options as its last argument, and
the CLI takes --require-certified or --require-certified-system <system_id>,
each with --customer-key-id <id>.
Registering a system with your own key
ADR-025 lets a system's Verification Records say authorization: certified
with your customer_key_id: before measuring it, the enclave checked that the
system's config and query are the ones a key group you hold registered. That
holds for records from an enclave image of protocol 12 or later; the verifiers
say when a record comes from an earlier one, whose certified is not evidence.
All of it happens on your machine, and the SDK never holds or sends a private key.
Node only: ADR-025 makes a client you run locally the only place enrolment can
happen, so these calls are not part of the browser entry point.
import { BurnLedger, generateKeyGroup } from "burnledger";
// 1. A key group: n Ed25519 members, any t of which can act.
const { group, signers } = await generateKeyGroup({ members: 2, threshold: 1 });
const seeds = signers.map((s) => s.exportSeed()); // store these offline, now
const dp = new BurnLedger({ apiKey: "dp_..." });
// 2. Verify the enclave against the PCR0 you pinned out of band. Configs are
// sealed to it, and everything below must come back signed by its key.
const enclave = await dp.attestEnclaveIdentity({ expectedPcr0: PINNED_PCR0 });
// 3. Enrol the group for your team. Every member signs, over the latest the
// authorization may end (`notAfter`, by default the enclave's 90-day maximum).
await dp.enrollKeyGroup({ teamId: TEAM_ID, group, signers, enclave });
// 4. Seal the config to the enclave, create the system with only the sealed
// bytes, and register it, signed at the group's threshold.
const { system, registration } = await dp.createRegisteredSystem({
teamId: TEAM_ID,
group,
signers: signers.slice(0, 1),
enclave,
name: "users-db",
connectorType: "postgresql",
dsn: "postgres://...",
subjectQuery: "SELECT id FROM users WHERE email = $1",
});
// 5. Rotate when you must. The OUTGOING group signs at its threshold and every
// incoming member signs; nothing is re-registered.
const next = await generateKeyGroup({ members: 2, threshold: 1 });
await dp.rotateKeyGroup({
teamId: TEAM_ID,
previousGroup: group,
previousSigners: signers.slice(0, 1),
nextGroup: next.group,
nextSigners: next.signers,
enclave,
});What each call checks. The API relays every document, so each one that
comes back — the enrolment statement, the authorization, the registration — must
verify under the signing key of the enclave you attested, or the call throws
VerificationError. A relay that forged an enrolment would otherwise have you
believe you were registered. The key comes from your attestation, not from
/.well-known/burnledger-keys, which the same API serves. Then the enrolment
must name the group you hold and the team you asked for, its authorization must
end exactly at the notAfter you signed and not start in the future, so an older
genuine answer or another team's is refused, and the registration must name the
system, config and connector you signed. verifyKeyEnrollmentStatement,
verifyTeamAuthorization and verifySystemRegistration re-check a stored
document against the enclave key.
Two of the registration's signed fields are deliberately not compared. Its
query_template_hash is HashQuery over the template after the server
normalizes it, while the request signs sha256 of the raw template, so the two
are not byte-equal even for an honest registration and the SDK has no normalizer
to reconcile them. registered_at is not freshness-checked either, as there is
no signed bound to hold it to. So a relay could hand back an older genuine
registration of this system, under this key and config, over a template you
signed for it earlier; the customer path calls the check right after its own
POST, which narrows that window but does not close it.
A signer is anything with a publicKey and an async sign(payload)
(KeyGroupSigner), so a member can live in an HSM, a cloud KMS or another
process; InMemorySigner is the convenience for keys on this machine. A system
that already exists is registered with registerSystemWithKey, given the exact
config bytes it was created with.
What the records then say. Every Verification Record of a registered system
carries authorization: certified and your customer_key_id for it, and the CLI
prints that key id on the system's Registration line, with what the record's
enclave image makes of it. A system never registered
says authorization: none. A record naming a key id that is not yours means
someone else authorized that verification. The authorization behind your key
expires; enrol the same group again before authorization.notAfter to renew it.
Custody. The private keys are yours alone: BurnLedger never sees them and
cannot reissue them. Lose enough members that the group can no longer reach its
threshold and it can neither register a system nor authorize its own rotation,
and there is no recovery, because an operator who could re-enrol on your behalf
is an operator who could mint (ADR-025 §3). The way back is a new group and
re-registering every system, and records before and after will name different key
ids, visibly. So enrol more members than the threshold and keep one offline — two
members, threshold one, is the simplest. A recovery-phrase format is not defined
yet; until it is, back up each member's 32-byte seed (exportSeed()) as you
would any credential that cannot be reissued.
Webhook Verification
Verify incoming webhook signatures (HMAC-SHA256):
import { verifyWebhookSignature } from "burnledger";
const valid = verifyWebhookSignature(
webhookSecret, // from dp.registerWebhook()
request.body, // raw request body
request.headers["x-burnledger-signature"],
);API Reference
Client
new BurnLedger({
apiKey: string,
baseUrl?: string, // default: "https://api.burnledger.io"
timeout?: number, // seconds, default: 30
maxRetries?: number, // default: 2
})Systems
| Method | Returns |
|--------|---------|
| registerSystem(opts) | Promise<System> |
| getSystem(id) | Promise<System> |
| listSystems(opts?) | Paginator<System> |
| deregisterSystem(id) | Promise<void> |
| healthCheck(id) | Promise<System> |
Attestations
| Method | Returns |
|--------|---------|
| attest(subject, opts?) | Promise<Attestation> |
| batchAttest(subjects, opts?) | Promise<BatchAttestationResponse> |
| getAttestation(id) | Promise<Attestation> |
| waitFor(id, opts?) | Promise<Attestation> |
| verify(id, subject, opts?) | Promise<VerifyResult> |
Certificates
| Method | Returns |
|--------|---------|
| getCertificate(id) | Promise<CertificateResponse> |
| listCertificates(opts?) | Paginator<CertificateResponse> |
| getCertificateStats() | Promise<CertificateStats> |
| exportCertificates(opts?) | Promise<Uint8Array> |
| downloadPdf(id) | Promise<Uint8Array> |
| savePdf(id, path) | Promise<void> |
| getRevocationStatus(id) | Promise<RevocationStatus> |
| revokeCertificate(id, opts) | Promise<CertificateResponse> |
| batchRevokeCertificates(ids, opts) | Promise<BatchRevokeResponse> |
batchRevokeCertificates takes up to MAX_BATCH_REVOKE (100) ids under one
reason and resolves whether all, some or none were revoked: the failures are
in errors, each naming the request index and certificateId, so a
partially failed batch is inspected, not caught. More than 100 ids throws
before any request is made; chunk larger sets by MAX_BATCH_REVOKE.
Webhooks
| Method | Returns |
|--------|---------|
| registerWebhook(opts) | Promise<Webhook> |
| listWebhooks(opts?) | Paginator<Webhook> |
| deleteWebhook(id) | Promise<void> |
| rotateWebhookSecret(id) | Promise<WebhookRotateResponse> |
| commitWebhookRotation(id) | Promise<void> |
| listFailedDeliveries(opts?) | Paginator<WebhookDelivery> |
| retryDelivery(deliveryId) | Promise<void> |
| resolveDelivery(deliveryId) | Promise<void> |
API Keys
| Method | Returns |
|--------|---------|
| listApiKeys() | Promise<ApiKeyListItem[]> |
| createApiKey({ teamId? }) | Promise<ApiKeyResponse> |
| revokeApiKey(id) | Promise<void> |
Transparency Log
These methods do not require authentication.
| Method | Returns |
|--------|---------|
| getLogHead() | Promise<SignedTreeHead> |
| getLogEntry(index) | Promise<LogEntry> |
| getLogEntries(opts) | Promise<LogEntry[]> |
| getInclusionProof(opts) | Promise<InclusionProof> |
| getConsistencyProof(opts) | Promise<ConsistencyProof> |
Pagination
All list* methods return a Paginator that auto-fetches pages:
for await (const cert of dp.listCertificates()) {
console.log(cert.id);
}Dual ESM/CJS
The package ships both ESM and CommonJS builds:
// ESM
import { BurnLedger } from "burnledger";
// CommonJS
const { BurnLedger } = require("burnledger");License
MIT
