Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.xandrlabs.ai/llms.txt

Use this file to discover all available pages before exploring further.

Every Knowledge Block in ALX Protocol is identified by a kbHash — a deterministic, domain-separated Keccak-256 digest computed from a canonical subset of the artifact’s fields. Because the hash is fully deterministic, any party holding the same artifact can independently reproduce and verify the same identifier without contacting a server or registry.

What gets hashed

The identity hash covers exactly seven fields, defined in the protocol as the hash scope:
FieldDescription
typeKB structural type (e.g. Practice, StateMachine)
domainNamespaced domain string (e.g. engineering.auth.jwt)
sourcesSorted array of parent kbHash references
artifactHashKeccak-256 digest of the canonical artifact payload
tierAccess tier (open, verified, premium, restricted)
payloadThe structured KB content object
derivationDerivation metadata: type, inputs, and recipe
The following fields are excluded from hashing and must not be present in the normalized input:
  • kbHash itself
  • metadata
  • curator
  • createdAt
  • signature
The kbHash field is excluded by design. Including it would make the hash self-referential, creating a circular dependency. Strip it before computing identity, then re-attach it to the serialized artifact.

The domain tag KB_V1

The hash is computed as keccak256("KB_V1" + canonicalJson) — not over the canonical JSON alone. The KB_V1 prefix is a domain separator that prevents collisions between KB hashes and hashes produced by other ALX Protocol object types (tasks, transitions, constraints, and so on). All on-chain kbId values are derived using this domain-tagged form. Do not use the undomain-tagged contentHashFromEnvelope function for new code — it is retained only for v1 conformance tests.

Source ordering and sortSources

The sources array contains the kbHash values of parent KBs. Array ordering is normalized before hashing: sortSources sorts the sources alphabetically so that two envelopes listing the same parents in different orders produce identical hashes. You do not need to sort sources manually before passing the envelope to kbHashFromEnvelope — normalization is applied internally.
import { sortSources } from "@alx/protocol";

// Both of these produce identical hashes after normalization
const envelopeA = { sources: ["0xabc...", "0xdef..."], /* ... */ };
const envelopeB = { sources: ["0xdef...", "0xabc..."], /* ... */ };

End-to-end example

The following example shows the complete identity derivation flow: stripping excluded fields, normalizing the envelope, calling canonicalize(), and calling kbHashFromEnvelope().
import {
  canonicalize,
  kbHashFromEnvelope,
  sortSources,
  DOMAIN_TAGS,
} from "@alx/protocol";

// 1. Your raw artifact, as retrieved from storage or constructed in memory.
//    It may contain kbHash and other excluded fields.
const artifact = {
  type: "Practice",
  domain: "engineering.auth.jwt",
  sources: [],
  artifactHash: "0xc3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2",
  tier: "open",
  payload: {
    type: "Practice",
    title: "JWT Rotation Policy",
    summary: "Rotate signing keys every 90 days.",
  },
  derivation: {
    type: "original",
    inputs: [],
    recipe: null,
  },
  // These fields are excluded from the hash:
  kbHash: "0x...",
  metadata: { version: 1 },
  curator: "0xAbCd...",
  createdAt: "2026-04-01T00:00:00.000Z",
};

// 2. Strip excluded fields before computing identity.
const {
  kbHash: _kbHash,
  metadata: _metadata,
  curator: _curator,
  createdAt: _createdAt,
  signature: _signature,
  ...envelope
} = artifact;

// 3. Normalize source ordering (also applied internally by kbHashFromEnvelope,
//    but calling it explicitly makes the intent clear).
const normalizedEnvelope = sortSources(envelope);

// 4. Compute the canonical JSON representation (RFC 8785 / JCS).
//    Keys are sorted alphabetically, no whitespace, deterministic number encoding.
const canonicalJson = canonicalize(normalizedEnvelope);
console.log(canonicalJson);
// {"artifactHash":"0xc3ab8ff1...","derivation":{...},"domain":"engineering.auth.jwt",...}

// 5. Compute the domain-tagged identity hash.
//    Internally: keccak256(DOMAIN_TAGS.KB + canonicalJson)
const kbHash = kbHashFromEnvelope(envelope);
console.log(kbHash);
// "0x" + 64 hex chars — this is the on-chain kbId

// 6. For reference: the domain tag used for KB identity.
console.log(DOMAIN_TAGS.KB); // "KB_V1"
kbHashFromEnvelope calls normalizeForHash internally, which picks only the seven hash-scope keys and sorts sources. You can pass the full artifact object (minus kbHash) safely — any extra fields are silently ignored during normalization.

Canonical JSON rules

canonicalize() follows RFC 8785 (JSON Canonicalization Scheme):
  • Object keys are sorted alphabetically at every nesting level.
  • No whitespace between tokens.
  • Numbers use the shortest representation (integers as 123, not 123.0).
  • null values, BigInt, and non-finite numbers are rejected with an error.
  • Arrays preserve insertion order — only object keys are sorted.

Artifact hash vs. KB identity

kbHash and artifactHash are separate, complementary values:
  • kbHash identifies the Knowledge Block. It is the on-chain kbId.
  • artifactHash is the Keccak-256 digest of the canonical payload object. It is stored in the registry for artifact integrity checks.
You can re-derive artifactHash at any time from payload alone using artifactHashFromPayload, without reconstructing the full envelope.
import { artifactHashFromPayload } from "@alx/protocol";

const artifactHash = artifactHashFromPayload(artifact.payload);