Skip to content

TypeScript: @dnsid-ai/log-c2sp-tlog

DNSid binding for C2SP tiled logs, including verified reads and split-signature prepared writes.

This implementation follows DNSid method revision d5a65d06f76eff4db81e50f8767a600d2ca7fc2a (exported as DNSID_C2SP_METHOD_REVISION). The method, envelope v:1, and bundle @v1 labels are unchanged. Every scope requires signed binding context and a logical predecessor chain. C2spChain.previousEventId replaces previousIndex and previousLeafHash; the corresponding wire field is prev_event_id. prepared.eventId is SHA-256 of "dnsid-c2sp-event-v1", one zero byte, and canonical signed payload bytes excluding top-level sigs. State hashing is unchanged. Old index/leaf chain fields and signed event_id are prohibited. The old unchained option no longer bypasses verification.

Deploy corrected readers, writers, monitors and bundle producers together. Preserve conforming histories (including conforming public ISSUANCE-only streams) and terminal state. Inventory nonconforming histories and outstanding submissions; retain old evidence and use fresh streams where necessary. Never rewrite bytes that may already have been submitted, even when the logical event ID is unchanged. Historical cross-SDK matrix results are not corrected-contract release evidence.

The package root contains the Node.js checkpoint, Merkle, and signed-note verification implementation. Browser-safe consumers that only prepare, inspect, sign, or serialize registry-provided events can import the portable @dnsid-ai/log-c2sp-tlog/writer subpath. Immutable binding-version metadata is available from @dnsid-ai/log-c2sp-tlog/version.

import { createC2spTlogVerificationRegistry } from '@dnsid-ai/log-c2sp-tlog';
const registry = await createC2spTlogVerificationRegistry({
// This URL is explicit, independently trusted application configuration.
policyUrl: 'https://policy.example/dnsid-policy',
// Local freshness policy used by verifyNonRevocation.
checkpointMaxAge: 5 * 60 * 1000,
allowedClockSkew: 30 * 1000,
});

The factory fetches the policy with SSRF-safe destination checks, rejects redirects, requires HTTP 200, bounds decoded responses during reading, and uses the same bounded resource fetcher for log evidence. The example above supplies no bundle keys, so it uses a complete-log scan. With an independently distributed trustProfile, or policyDocument/policyUrl plus direct bundleVerifierKeys, set maxBundleLifetimeMs to enable bundle verification (and checkpointMaxAge for non-revocation); the default reader then prefers {lr log-prefix}/streams/{fqdn}?format=bundle and shares its verified lifecycle snapshot across binding, continuity, and key-age checks. A missing or temporarily unavailable endpoint falls back to the bounded complete-log scan. A valid newer bundle also falls back when no consistency-proof source is available, allowing the complete scan to verify both roots independently. Malformed, expired, invalid, rollback, or conflicting bundle evidence never falls back. Set requireStreamBundle to disable fallback.

For an explicit application decision to trust DNSid-managed DNSid logs, use the separately named managed factory:

import { createDnsidManagedVerificationRegistry } from '@dnsid-ai/log-c2sp-tlog';
const registry = await createDnsidManagedVerificationRegistry();

It selects reviewed trust bundled with the SDK only for exact canonical public references to https://log.dev.dnsid.ai or https://log.dnsid.ai. Both development and production use bundle-first verification with bounded raw-scan fallback when bundle evidence is unavailable or consistency evidence requires a complete scan. Unknown scopes and prefixes fail closed. The generic factory never selects managed roots when trust is omitted.

Set checkpointMaxAge when using verifyNonRevocation; omitting it makes that operation fail closed. Non-revocation always refreshes evidence instead of relying on the lifecycle snapshot retained by VerifiedDomain. allowedClockSkew defaults to zero. The factory installs an in-memory trusted-checkpoint store by default; that protects against rollback only for the process lifetime. Inject a durable trustedCheckpointStore when protection must survive restarts. A custom resourceFetcher must implement the bounded fetch contract and explicitly report all required security guarantees.

Checkpoint-store load/compareAndSwap and consistency-source fetchConsistencyProof receive the verification signal as their last argument. Custom stores must check cancellation at the atomic commit boundary and prevent pending writes after cancellation; racing an uncancelable write is insufficient. Checkpoint advancement shares the caller’s cancellation and has a 30-second standalone default (timeoutMs/signal options).

Never derive policyUrl from an unverified identity record, its lr, or a log prefix. Advanced deployments can compose parseC2spPolicyFile, ScanStreamSource, registerC2spTlog, and a custom checkpoint store directly for private transports, mirrors, archives, or portable bundles.

Public verification needs a trusted local policy and complete stream evidence. A single inclusion proof proves historical inclusion only; it is not current lifecycle state or non-revocation evidence.

For self-managed new identity instances, generate and persist a stream ID with generateC2spTlogStreamId() (128 random bits, 22 unpadded base64url characters). For registry-managed issuance, use the registry-provided bound log reference; do not generate a second ID or reuse a bare FQDN as the stream ID.

For writes, construct C2spTlogBinding with a bound reference, call prepareEvent, and pass the prepared event between signer processes. Each process calls parsePreparedEvent before signPreparedEvent; existing signatures and the provider’s public key are checked before another signature is added. The operational side of split ISSUANCE supplies its locally expected FQDN, governance ID, entity key, and operational key as verification context; the SDK rejects missing or mismatched expectations before adding the operational countersignature. entryBytes requires the same ISSUANCE context and verifies every role signature.

writePreparedEvent passes the exact canonical entry bytes and optional idempotency key to the deployment adapter. Events after genesis in every scope also require a validateChain callback backed by authoritative prior stream state; the SDK will not append them based only on caller-supplied chain hashes.

Draft-01 ISSUANCE entries contain the recorded entity and operational public keys and carry sigs.ae and sigs.op. KEY_ROTATION entries carry both the previous-key authorization in sigs.prev_op and the new-key proof of possession in sigs.new_op. Other supported lifecycle entries carry sigs.ae. The draft-01 behavior supports ISSUANCE, KEY_ROTATION, REVOCATION, RETIREMENT, MIGRATION, and DELEGATION.

Candidate selection deduplicates exact signed payloads, not complete entry bytes. It accepts valid ES256 high-S/low-S variants and independently re-signed copies without changing first-applied order, sequence or key age. Every role signature must verify before a new payload can create a contradiction. Authority comes from its verified predecessor, including superseded keys; signed forks and new post-terminal events fail closed. Bundle summaries count logical events, but every supplied inclusion proof verifies exact complete bytes, even for ignored copies. readEvent independently authenticates every role of the requested occurrence; an invalid-signature replay is not valid standalone evidence.

Inbound migration requires verifyMigration to return the verified prior-log history through finalEntryRef, plus the entity and active operational keys it establishes. To return LoggedStateEvidence, it also supplies the aligned priorHistoryReferences; these preserve prior-log bounds without deriving them from destination indexes. rebuildHistory validates imported state and returns one stitched history with the migration event exactly once. A missing, terminal, key-inconsistent, or boundary-incomplete prior history fails closed. Migration callbacks receive a second { signal } argument and must propagate that same budget through recursive predecessor verification. The callback owns exact-cutoff proof verification and cumulative recursion/history/response limits.

Read operations and standalone stream verifiers have a 30-second overall default. Factory/reader signal settings and per-call stream-verifier options propagate cancellation; injected sources must honor it. Raw scans default to one million entries and 256 MiB aggregate entry bytes; lifecycle replay additionally caps retained history at 10,000 logical events. Copies still consume input limits. Reuse registries/transports/checkpoint stores, monitor fallback cost, and lower scan limits or require bundles for predictable cost.

Lifecycle and non-revocation verification require a timestamped witness quorum. checkpointMaxAge is required for non-revocation; bundle reads also enforce checkpoint freshness (using checkpointMaxAge when set, otherwise maxBundleLifetimeMs). Raw-scan historical binding and key-continuity checks accept older authenticated checkpoints. Text policies in the C2SP tlog-policy format, including nested groups, can be loaded with parseC2spPolicyFile.

This package currently supports C2SP signed-note Ed25519 log signatures (type 0x01) and timestamped Ed25519 witness cosignatures (type 0x04).

verifyC2spStreamBundle verifies canonical dnsid-c2sp-stream-bundle@v1 bytes against exact independently accepted policy bytes, a trusted bundle-signing key, C2SP inclusion proofs, lifecycle signatures, checkpoint freshness, trusted-index completeness, and the origin-scoped checkpoint store. It does not resolve DNS or validate the identity record/status; callers must independently supply trusted policy, bundle keys, and entity key, plus a checkpoint store and positive bundle byte, event-count, and lifetime limits. The exported C2SP_TLOG_SPECIFICATIONS object identifies the exact external C2SP revisions implemented by this package.

C2SP tlog-backed lifecycle log support for DNSid.

Implements the c2sp-tlog log method: reading, writing, and verifying DNSid lifecycle transparency logs built on the C2SP specifications (tlog-checkpoint, tlog-tiles, signed-note, tlog-cosignature, and related documents; see C2SP_TLOG_SPECIFICATIONS). A stream in such a log records an agent’s identity-record lifecycle events (ISSUANCE, KEY_ROTATION, REVOCATION, …) as canonical JSON entries in a Merkle tree, authenticated by witnessed checkpoints under a local trust policy.

Key entry points: createC2spTlogVerificationRegistry for generic caller-supplied trust, createDnsidManagedVerificationRegistry for the reviewed DNSid-managed trust catalog, registerC2spTlog / C2spTlogReader for lower-level composition, C2spTlogBinding and the prepared-event writer API for appending events, and verifyC2spStreamBundle for offline bundles.

type C2spJsonEvent = Record<string, unknown>;

Defined in: packages/log-c2sp-tlog/src/event-codec.ts:20

Raw JSON envelope of a DNSid lifecycle event as stored in a c2sp-tlog entry.


type C2spSignerRole =
| "Entity"
| "OperationalCountersignature"
| "PreviousOperational"
| "NewOperational";

Defined in: packages/log-c2sp-tlog/src/writer.ts:29

Role a signature is produced under when signing a prepared lifecycle event.


type C2spTlogQuorumRule =
| {
kind: "none";
}
| {
key: SignedNoteKey | string;
kind: "witness";
}
| {
kind: "threshold";
members: C2spTlogQuorumRule[];
threshold: number;
};

Defined in: packages/log-c2sp-tlog/src/policy.ts:9

Witness-quorum requirement for accepting a checkpoint: no witnesses, a single named witness, or a threshold over nested member rules.


type C2spTlogScope = "public" | "testnet" | `private-${string}`;

Defined in: packages/log-c2sp-tlog/src/lr.ts:5

Deployment scope of a c2sp-tlog reference; public additionally requires HTTPS and a non-zero witness quorum. All scopes require chaining; lifecycle reads require witnessed checkpoints.

const C2SP_ENTRIES_PER_BUNDLE: 256 = 256;

Defined in: packages/log-c2sp-tlog/src/stream-source.ts:57

C2SP tlog-tiles fixes full entry bundles at 256 entries.


const C2SP_TLOG_PROFILE_VERSION: 1;

Defined in: packages/log-c2sp-tlog/src/version.ts:2

Exact external specifications implemented by this c2sp-tlog package.


const C2SP_TLOG_SPECIFICATIONS: Readonly<{
signed-note: "https://c2sp.org/signed-note@v1.0.0";
tlog-checkpoint: "https://c2sp.org/tlog-checkpoint@v1.0.0";
tlog-cosignature: "https://c2sp.org/tlog-cosignature@v1.0.1";
tlog-mirror: "d0fe789122c75b903bfc1680b0b8b8dc570f0db3";
tlog-policy: "1896a5aea5559b3203d275d0206d872f59348cf5";
tlog-proof: "ab17a74116563005f908b9167e6421cc929a5c2b";
tlog-tiles: "https://c2sp.org/tlog-tiles@v0.1.0";
tlog-witness: "https://c2sp.org/tlog-witness@v1.0.0";
}>;

Defined in: packages/log-c2sp-tlog/src/version.ts:8

Pinned versions (URL or commit) of each C2SP specification this package implements.


const DEFAULT_C2SP_MAX_CHECKPOINT_BYTES: 1048576 = 1_048_576;

Defined in: packages/log-c2sp-tlog/src/stream-source.ts:59


const DEFAULT_C2SP_MAX_ENTRY_BUNDLE_BYTES: 16777472 = 16_777_472;

Defined in: packages/log-c2sp-tlog/src/stream-source.ts:61

256 * (2-byte prefix + 65,535-byte entry).


const DEFAULT_C2SP_MAX_STREAM_BUNDLE_BYTES: number;

Defined in: packages/log-c2sp-tlog/src/stream-bundle.ts:21


const DEFAULT_C2SP_MAX_STREAM_BUNDLE_EVENTS: 10000 = 10_000;

Defined in: packages/log-c2sp-tlog/src/stream-bundle.ts:22


const DEFAULT_C2SP_MAX_TOTAL_ENTRY_BYTES: 268435456 = 268_435_456;

Defined in: packages/log-c2sp-tlog/src/stream-source.ts:62


const DEFAULT_C2SP_MAX_TREE_SIZE: 1000000 = 1_000_000;

Defined in: packages/log-c2sp-tlog/src/stream-source.ts:58


const DEFAULT_C2SP_REQUEST_TIMEOUT_MS: 10000 = 10_000;

Defined in: packages/log-c2sp-tlog/src/stream-source.ts:63


const DNSID_C2SP_METHOD_REVISION: "d5a65d06f76eff4db81e50f8767a600d2ca7fc2a";

Defined in: packages/log-c2sp-tlog/src/version.ts:5

DNSid method contract with signature-independent logical event identity.

Documented on Transparency log: classes:

Documented on Transparency log: interfaces:

Documented on Transparency log: functions: