Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

@enc-protocol/core — API Reference

Complete API reference for @enc-protocol/core, the protocol primitives package for the ENC Protocol. All code is generated from the Lean 4 DSL and formally verified. Do not edit the source files directly.

Installation

npm install @enc-protocol/core --registry https://npm-registry.ocrybit.workers.dev/

Published to the ENC registry at https://npm-registry.ocrybit.workers.dev/. To skip the --registry flag, set the scope once: npm config set @enc-protocol:registry https://npm-registry.ocrybit.workers.dev/.

Package Structure

@enc-protocol/core
  index.js          Re-exports everything from all modules
  types.js          Protocol constants and enumerations
  crypto.js         Cryptographic operations (secp256k1, SHA-256, XChaCha20, ECDH)
  event.js          Commit construction, signing, verification
  rbac.js           Role-based access control bitmask operations
  smt.js            Sparse Merkle Tree (168-bit depth)
  ct.js             Certificate Transparency tree (RFC 9162)
  manifest-validator.js   Manifest RBAC structure validation
  snapshot.js             .enc portable snapshot container format
  dm-ratchet.js           Confidentiality plugin — DM ratchet (+ NIP-44)
  group-ratchet.js        Confidentiality plugin — group ratchet
  mls-lazy.js             Confidentiality plugin — lazy-MLS group tree
  ecdh-envelope.js        Confidentiality plugin — notice / invite handoff
  identity-aead.js        Confidentiality plugin — owner-only private content
  aggregator.js           WebSocket hub routing core
  *-wasm.js               WASM-kernel-backed twins (byte-identical output)

Each module is importable individually:

import { generateKeypair } from '@enc-protocol/core/crypto.js'
import { mkCommit } from '@enc-protocol/core/event.js'

Or import everything from the barrel:

import { generateKeypair, mkCommit, Context, SparseMerkleTree } from '@enc-protocol/core'

types.js

Protocol constants and enumerations. All exports are Object.freeze-d.

Context

RBAC context roles used in schema permission evaluation.

import { Context } from '@enc-protocol/core/types.js'
 
Context.Self    // 'Self'    — the event author is the identity being checked
Context.Sender  // 'Sender'  — the identity that submitted the commit
Context.Public  // 'Public'  — any identity, including unauthenticated

Op

RBAC operations. Prefix _ denotes explicit denial (overrides grant).

import { Op } from '@enc-protocol/core/types.js'
 
// Grant operations
Op.C   // 'C'   — Create
Op.R   // 'R'   — Read
Op.U   // 'U'   — Update
Op.D   // 'D'   — Delete
Op.P   // 'P'   — Push (append to collection)
Op.N   // 'N'   — Notify (receive real-time events)
 
// Deny operations (override grants)
Op._C  // '_C'  — Deny Create
Op._R  // '_R'  — Deny Read
Op._U  // '_U'  — Deny Update
Op._D  // '_D'  — Deny Delete
Op._P  // '_P'  — Deny Push
Op._N  // '_N'  — Deny Notify

ACEventType

Access control event types. Frozen array of 13 strings.

import { ACEventType } from '@enc-protocol/core/types.js'
 
ACEventType
// ['Manifest', 'Grant', 'Revoke', 'Move', 'Transfer',
//  'Gate', 'Shared', 'Own', 'AC_Bundle',
//  'Pause', 'Resume', 'Terminate', 'Migrate']

EventStatus

Possible statuses for events in the state tree.

import { EventStatus } from '@enc-protocol/core/types.js'
 
EventStatus.Active   // 'Active'
EventStatus.Deleted  // 'Deleted'
EventStatus.Updated  // 'Updated'

SMTNamespace

Namespace prefixes for Sparse Merkle Tree keys.

import { SMTNamespace } from '@enc-protocol/core/types.js'
 
SMTNamespace.RBAC        // 'RBAC'        — identity role bitmasks
SMTNamespace.EventStatus // 'EventStatus' — event deletion/update status
SMTNamespace.KVState     // 'KVState'     — key-value state entries

LifecycleState

Enclave lifecycle states.

import { LifecycleState } from '@enc-protocol/core/types.js'
 
LifecycleState.Active      // 'Active'
LifecycleState.Paused      // 'Paused'
LifecycleState.Terminated  // 'Terminated'
LifecycleState.Migrating   // 'Migrating'

crypto.js

Cryptographic operations built on @noble/curves (secp256k1), @noble/hashes (SHA-256), and @noble/ciphers (XChaCha20-Poly1305). All functions are pure and deterministic except generateKeypair(), generateSession(), and encrypt() which use randomBytes.

Domain Separation Constants

Used as single-byte prefixes in domain-separated hashes.

import {
  DOMAIN_CT_LEAF,   // 0   — Certificate Transparency leaf hash prefix
  DOMAIN_CT_NODE,   // 1   — Certificate Transparency node hash prefix
  DOMAIN_COMMIT,    // 16  — Commit hash prefix
  DOMAIN_EVENT,     // 17  — Event hash prefix
  DOMAIN_ENCLAVE,   // 18  — Enclave ID hash prefix
  DOMAIN_SMT_LEAF,  // 32  — SMT leaf hash prefix
  DOMAIN_SMT_NODE,  // 33  — SMT node hash prefix
} from '@enc-protocol/core/crypto.js'

SMT_EMPTY_HASH

The SHA-256 hash of empty input. Used as the default hash for empty SMT nodes and empty events roots.

import { SMT_EMPTY_HASH } from '@enc-protocol/core/crypto.js'
 
// Uint8Array(32) — equals sha256(new Uint8Array(0))
// Hex: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

Key Generation & Derivation

generateKeypair()

Generate a random secp256k1 keypair.

generateKeypair() → { privateKey: Uint8Array(32), publicKey: Uint8Array(32) }
  • privateKey — 32 random bytes (valid secp256k1 scalar)
  • publicKey — x-only public key (32 bytes, no parity prefix)
import { generateKeypair, bytesToHex } from '@enc-protocol/core/crypto.js'
 
const kp = generateKeypair()
console.log(bytesToHex(kp.publicKey))  // 64 hex chars

derivePublicKey(privateKey)

Derive the x-only public key from a private key.

derivePublicKey(privateKey: Uint8Array) → Uint8Array(32)

Internally computes the compressed public key (33 bytes) and strips the parity prefix byte, returning the 32-byte x coordinate.

import { derivePublicKey, hexToBytes } from '@enc-protocol/core/crypto.js'
 
const priv = hexToBytes('deadbeef...')  // 32 bytes
const pub = derivePublicKey(priv)       // 32 bytes, x-only

Encoding Utilities

bytesToHex(bytes)

Convert a Uint8Array to a lowercase hex string.

bytesToHex(bytes: Uint8Array) → string
bytesToHex(new Uint8Array([0xca, 0xfe])) // 'cafe'

hexToBytes(hex)

Convert a hex string to a Uint8Array. Accepts optional 0x prefix.

hexToBytes(hex: string) → Uint8Array
hexToBytes('cafe')   // Uint8Array [0xca, 0xfe]
hexToBytes('0xcafe') // Uint8Array [0xca, 0xfe]

Hash Functions

sha256Hash(data)

Raw SHA-256 hash.

sha256Hash(data: Uint8Array) → Uint8Array(32)

sha256Str(str)

SHA-256 of a UTF-8 encoded string.

sha256Str(str: string) → Uint8Array(32)
const hash = sha256Str('hello world')  // Uint8Array(32)

domainHash(prefix, data)

Domain-separated hash: sha256(prefix_byte || data).

domainHash(prefix: number, data: Uint8Array) → Uint8Array(32)

Used internally by all tree hash functions. The single-byte prefix provides collision resistance between different hash domains.

taggedHash(tag, data)

BIP-340 tagged hash: sha256(sha256(tag) || sha256(tag) || data).

taggedHash(tag: string, data: Uint8Array) → Uint8Array(32)

Used in session token generation for challenge computation.


Tree Hash Functions

ctLeafHash(eventsRoot, stateHash)

Certificate Transparency leaf hash.

ctLeafHash(eventsRoot: Uint8Array, stateHash: Uint8Array) → Uint8Array(32)

Computes sha256(0x00 || eventsRoot || stateHash).

ctNodeHash(left, right)

Certificate Transparency internal node hash.

ctNodeHash(left: Uint8Array, right: Uint8Array) → Uint8Array(32)

Computes sha256(0x01 || left || right).

smtLeafHash(key, value)

Sparse Merkle Tree leaf hash.

smtLeafHash(key: Uint8Array, value: Uint8Array) → Uint8Array(32)

Computes sha256(0x20 || key || value).

smtNodeHash(left, right)

Sparse Merkle Tree internal node hash.

smtNodeHash(left: Uint8Array, right: Uint8Array) → Uint8Array(32)

Computes sha256(0x21 || left || right).


Protocol Hash Functions

computeContentHash(content)

Hash event content (UTF-8 string).

computeContentHash(content: string) → Uint8Array(32)

Equivalent to sha256Hash(new TextEncoder().encode(content)).

computeCommitHash(contentHash, enclave, from, type, exp, tags)

Compute the commit hash that gets signed.

computeCommitHash(
  contentHash: string,   // hex string of content hash
  enclave: string,       // enclave ID (64 hex)
  from: string,          // author pubkey (64 hex)
  type: string,          // event type
  exp: number,           // expiration timestamp (ms)
  tags: string[][]       // tag array
) → Uint8Array(32)

Internally computes sha256(JSON.stringify([16, enclave, from, type, contentHash, exp, encodedTags])).

computeEventHash(seq, sequencer, sig1, timestamp)

Compute the event hash (signed by the sequencer).

computeEventHash(
  seq: number,           // sequence number
  sequencer: string,     // sequencer pubkey (64 hex)
  sig1: string,          // author signature (128 hex)
  timestamp: number      // event timestamp (ms)
) → Uint8Array(32)

Internally computes sha256(JSON.stringify([1, seq, sequencer, sig1, timestamp])).

Note: The domain prefix 1 in the JSON array serves the same role as DOMAIN_EVENT but is embedded in the serialization format rather than prepended as a byte.

computeEnclaveId(from, type, contentHash, tags)

Derive a deterministic enclave ID from a manifest commit.

computeEnclaveId(
  from: string,          // creator pubkey (64 hex)
  type: string,          // 'Manifest'
  contentHash: string,   // hex content hash
  tags: string[][]       // tags
) → string              // 64 hex chars

Returns a hex string (not bytes). The enclave ID is deterministic given the same inputs.

computeEventId(sig2Hex)

Compute event ID from the sequencer's signature.

computeEventId(sig2Hex: string) → string  // 64 hex chars

Returns sha256(hexToBytes(sig2Hex)) as a hex string. The event ID is the hash of the sequencer signature, making it globally unique.

computeEventsRoot(eventIds)

Compute Merkle root from an array of event IDs.

computeEventsRoot(eventIds: string[]) → Uint8Array(32)
  • Empty array returns SMT_EMPTY_HASH
  • Single event returns the event ID bytes
  • Multiple events are combined into a binary Merkle tree using ctNodeHash

Schnorr Signatures (BIP-340)

schnorrSign(msgHash, privateKey)

Create a BIP-340 Schnorr signature with deterministic nonce (zero auxiliary bytes).

schnorrSign(msgHash: Uint8Array, privateKey: Uint8Array) → Uint8Array(64)

The zero aux bytes (new Uint8Array(32)) ensure cross-implementation reproducibility. This is a deliberate deviation from standard BIP-340 which uses random aux bytes.

const sig = schnorrSign(sha256Hash(message), privateKey)
// sig is 64 bytes: 32-byte R x-coordinate || 32-byte s scalar

schnorrVerify(msgHash, signature, publicKey)

Verify a BIP-340 Schnorr signature.

schnorrVerify(
  msgHash: Uint8Array,    // 32 bytes
  signature: Uint8Array,  // 64 bytes
  publicKey: Uint8Array   // 32 bytes (x-only)
) → boolean

Returns false on any error (never throws).


Session Management

generateSession(idPriv, duration?)

Generate a session token and derived session keypair.

generateSession(
  idPriv: Uint8Array,  // 32-byte identity private key
  duration?: number    // session duration in seconds (default: 7200, max: 7200)
) → {
  session: string,         // 136 hex chars: r(64) + sessionPub(64) + expires(8)
  sessionPriv: Uint8Array, // 32-byte session private key
  expires: number          // Unix timestamp (seconds) when session expires
}

The session token encodes:

  • Bytes 0-31 (hex 0-63): r — random point x-coordinate
  • Bytes 32-63 (hex 64-127): sessionPub — derived session public key
  • Bytes 64-67 (hex 128-135): expires — big-endian uint32 expiration timestamp

The session private key is derived via EC point arithmetic: sessionPriv = k + e * d where k is random, e is a BIP-340 tagged challenge hash, and d is the identity private key.

import { generateKeypair, generateSession, bytesToHex } from '@enc-protocol/core/crypto.js'
 
const kp = generateKeypair()
const { session, sessionPriv, expires } = generateSession(kp.privateKey, 3600)
console.log(session.length)  // 136
console.log(expires)         // Unix seconds, ~1h from now

verifySession(session, fromPubHex)

Verify a session token was created by the given identity.

verifySession(
  session: string,       // 136 hex char session token
  fromPubHex: string     // 64 hex char identity public key
) → string | null

Returns null on success. Returns an error string on failure:

  • 'INVALID_SESSION: token must be 136 hex chars'
  • 'SESSION_EXPIRED: token expired'
  • 'INVALID_SESSION: expires too far in future (max 2h)'
  • 'INVALID_SESSION: session_pub verification failed'

Clock skew tolerance: 60 seconds.

import { generateKeypair, generateSession, verifySession, bytesToHex } from '@enc-protocol/core/crypto.js'
 
const kp = generateKeypair()
const { session } = generateSession(kp.privateKey)
const pubHex = bytesToHex(kp.publicKey)
 
const err = verifySession(session, pubHex)
console.log(err) // null (valid)

ECDH Encryption

ecdh(privKey, pubKey)

Compute an ECDH shared secret.

ecdh(privKey: Uint8Array, pubKey: Uint8Array) → Uint8Array(32)

Uses secp256k1 point multiplication. The shared secret is the x-coordinate of the resulting point (32 bytes).

deriveKey(shared, label)

Derive an encryption key from a shared secret using HKDF-SHA256.

deriveKey(shared: Uint8Array, label: string) → Uint8Array(32)
  • shared — the ECDH shared secret
  • label — HKDF info string (e.g. 'enc:query', 'enc:response')

No salt is used (undefined). Output is 32 bytes.

encrypt(key, plaintext)

Encrypt a string with XChaCha20-Poly1305.

encrypt(key: Uint8Array, plaintext: string) → string  // base64

Returns base64-encoded nonce(24) || ciphertext || tag(16). The 24-byte nonce is randomly generated.

decrypt(key, ciphertextB64)

Decrypt a base64-encoded XChaCha20-Poly1305 ciphertext.

decrypt(key: Uint8Array, ciphertextB64: string) → string

Splits the decoded bytes into 24-byte nonce and remaining ciphertext, decrypts, and returns the UTF-8 string.

import { ecdh, deriveKey, encrypt, decrypt, generateKeypair } from '@enc-protocol/core/crypto.js'
 
const alice = generateKeypair()
const bob = generateKeypair()
 
const sharedA = ecdh(alice.privateKey, bob.publicKey)
const sharedB = ecdh(bob.privateKey, alice.publicKey)
// sharedA === sharedB (same shared secret)
 
const key = deriveKey(sharedA, 'my-app:messages')
const ct = encrypt(key, 'hello bob')
const pt = decrypt(key, ct)
console.log(pt) // 'hello bob'

Signer Derivation

Used for ECDH-encrypted communication with the node. Derives per-session, per-enclave signer keys.

deriveSignerPriv(sessionPriv, sessionPub, seqPub, enclaveId)

Derive a signer private key from session credentials.

deriveSignerPriv(
  sessionPriv: Uint8Array,  // 32-byte session private key
  sessionPub: Uint8Array,   // 32-byte session public key (x-only)
  seqPub: Uint8Array,       // 32-byte sequencer public key (x-only)
  enclaveId: string          // 64 hex char enclave ID
) → Uint8Array(32)

Computes t = sha256(sessionPub || seqPub || enclaveBytes) mod n, then signerPriv = adjustedSessionPriv + t mod n. The y-parity of the session public key point determines whether sessionPriv is negated.

deriveSignerPub(sessionPub, seqPub, enclaveId)

Derive the corresponding signer public key (without needing the private key).

deriveSignerPub(
  sessionPub: Uint8Array,   // 32-byte session public key (x-only)
  seqPub: Uint8Array,       // 32-byte sequencer public key (x-only)
  enclaveId: string          // 64 hex char enclave ID
) → Uint8Array(32)

Computes the same t value and returns sessionPubPoint + t*G as an x-only public key.


Signed Tree Head (STH)

signSTH(t, ts, rootHash, seqPriv)

Sign a tree head.

signSTH(
  t: number,               // tree size (number of leaves)
  ts: number,              // timestamp
  rootHash: Uint8Array,    // 32-byte Merkle root
  seqPriv: Uint8Array      // 32-byte sequencer private key
) → string                // 128 hex char Schnorr signature

The signed message is: "enc:sth:" || bigEndian64(t) || bigEndian64(ts) || rootHash.

verifySTH(t, ts, rootHash, sigHex, seqPub)

Verify a signed tree head.

verifySTH(
  t: number,              // tree size
  ts: number,             // timestamp
  rootHash: Uint8Array,   // 32-byte Merkle root
  sigHex: string,         // 128 hex char signature
  seqPub: Uint8Array      // 32-byte sequencer public key
) → boolean

Returns false on any error (never throws).

import { verifySTH, hexToBytes } from '@enc-protocol/core/crypto.js'
 
const sth = await (await fetch(`https://your-node.example.com/${enclaveId}/sth`)).json()
const valid = verifySTH(sth.t, sth.ts, hexToBytes(sth.r), sth.sig, hexToBytes(seqPubHex))

event.js

Commit construction, signing, and verification. Commits are the unit of write in the ENC Protocol. A commit becomes an event after the sequencer co-signs it.

Constants

import {
  MAX_EXP_WINDOW,       // 3600000 (1 hour in ms) — maximum expiration window
  CLOCK_SKEW_TOLERANCE, // 60000 (1 minute in ms) — clock skew tolerance
  acEventTypes,         // same as ACEventType from types.js
  lifecycleEventTypes,  // ['Pause', 'Resume', 'Terminate', 'Migrate']
} from '@enc-protocol/core/event.js'

mkCommit(enclave, from, type, content, exp, tags)

Create an unsigned commit object.

mkCommit(
  enclave: string,   // enclave ID (64 hex)
  from: string,      // author public key (64 hex)
  type: string,      // event type (e.g. 'post', 'Grant', 'Manifest')
  content: string,   // event content (JSON string)
  exp: number,       // expiration timestamp in ms (epoch)
  tags: string[][]   // tag array (e.g. [['t', 'post'], ['p', '<pubkey>']])
) → {
  enclave: string,
  from: string,
  type: string,
  content: string,
  content_hash: string,   // hex SHA-256 of content
  hash: string,           // hex commit hash (to be signed)
  exp: number,
  tags: string[][]
}

The hash field is the value that gets signed by the author. It is computed as: sha256(JSON.stringify([16, enclave, from, type, contentHash, exp, encodedTags])).

import { mkCommit, signCommit } from '@enc-protocol/core/event.js'
import { generateKeypair, bytesToHex } from '@enc-protocol/core/crypto.js'
 
const kp = generateKeypair()
const pub = bytesToHex(kp.publicKey)
 
const commit = mkCommit(
  enclaveId,
  pub,
  'post',
  JSON.stringify({ body: 'hello world' }),
  Date.now() + 300000,  // expires in 5 minutes
  []
)

signCommit(commit, privateKey)

Sign a commit, adding the sig field.

signCommit(commit: Object, privateKey: Uint8Array) → Object

Returns a new object with all commit fields plus sig (128 hex char Schnorr signature over the commit hash).

const signed = signCommit(commit, kp.privateKey)
// signed.sig is a 128 hex char string

verifyCommit(commit)

Verify that a commit's signature matches its hash and from pubkey.

verifyCommit(commit: Object) → boolean

Returns false on any error (never throws). Requires commit.hash, commit.sig, and commit.from.

verifyEvent(event)

Verify both the author signature (sig) and sequencer co-signature (seq_sig) on a finalized event.

verifyEvent(event: Object) → boolean

First verifies the commit signature, then verifies the sequencer signature over the event hash.

mkManifestCommit(from, manifestContent, exp, tags)

Create a manifest commit with a deterministically derived enclave ID.

mkManifestCommit(
  from: string,              // creator pubkey (64 hex)
  manifestContent: string,   // manifest JSON string
  exp: number,               // expiration timestamp (ms)
  tags: string[][]           // tags
) → Object                  // commit with derived enclave field

The enclave field is set to computeEnclaveId(from, 'Manifest', contentHash, tags). This means the enclave ID is deterministic: the same creator, manifest content, and tags always produce the same enclave ID.

import { mkManifestCommit, signCommit } from '@enc-protocol/core/event.js'
 
const manifest = JSON.stringify({
  enc_v: 2,
  nonce: Date.now(),
  RBAC: {
    use_temp: 'none',
    schema: [
      { event: 'post', role: 'owner', ops: ['C', 'U', 'D'] },
      { event: '*', role: 'Public', ops: ['R'] },
    ],
    states: [],
    traits: ['owner(0)'],
    initial_state: { owner: [myPubHex] },
  },
})
 
const commit = mkManifestCommit(myPubHex, manifest, Date.now() + 300000, [])
const signed = signCommit(commit, myPrivateKey)
// signed.enclave is the derived enclave ID

finalizeCommit(commit, timestamp, seq, sequencerPubHex, sequencerKey)

Finalize a commit into a full event (sequencer-side operation).

finalizeCommit(
  commit: Object,          // signed commit
  timestamp: number,       // event timestamp (ms)
  seq: number,             // sequence number
  sequencerPubHex: string, // sequencer public key (64 hex)
  sequencerKey: Uint8Array // sequencer private key
) → Object                // event with seq, timestamp, sequencer, seq_sig, id

Adds the sequencer co-signature (seq_sig) and computes the event ID from it.

mkReceipt(event)

Extract a receipt from a finalized event.

mkReceipt(event: Object) → {
  seq: number,
  id: string,
  hash: string,
  timestamp: number,
  sig: string,
  seq_sig: string,
  sequencer: string
}

validateCommitStructure(commit)

Validate a commit's hash matches its fields.

validateCommitStructure(commit: Object) → string | undefined

Returns an error string ('missing required fields' or 'hash mismatch') or undefined on success.

Type Checking Functions

isACEvent(type)

Check if an event type is an access control event.

isACEvent(type: string) → boolean

Uses startsWith matching, so 'Grant' and 'Grant(admin)' both return true.

isLifecycleEvent(type)

Check if an event type is a lifecycle event.

isLifecycleEvent(type: string) → boolean
// true for: 'Pause', 'Resume', 'Terminate', 'Migrate'

canUpdateDelete(type)

Check if the type is 'Update' or 'Delete'.

canUpdateDelete(type: string) → boolean

rbac.js

Role-based access control using bitmask operations. Each identity has a single bigint bitmask encoding both a state (low 8 bits) and trait flags (bits 8+).

Bitmask Layout

Bit:  255 ... 10  9  8  7  6  5  4  3  2  1  0
      [--- traits ---]  [------ state (0-255) ------]
                     ^
                     |
                OWNER_BIT (bit 8, = FIRST_TRAIT_BIT)
  • Bits 0-7 (STATE_MASK = 0xFF): State value (0 = outsider, 1-255 = named states from schema)
  • Bit 8 (OWNER_BIT): Owner trait (always the first trait)
  • Bits 9+: Custom traits defined in the manifest schema

Constants

import {
  STATE_MASK,       // 0xFFn — masks low 8 bits
  FIRST_TRAIT_BIT,  // 8     — first trait bit position
  OUTSIDER_STATE,   // 0n    — outsider state value
  EMPTY_ROLES,      // 0n    — no roles assigned
  OWNER_BIT,        // 8     — same as FIRST_TRAIT_BIT
} from '@enc-protocol/core/rbac.js'

Additional internal constants:

acEventTypesWithState   // same 13 AC event types
lifecycleOnlyACEvents   // ['Pause', 'Resume', 'Terminate', 'Migrate']
updateDeleteTypes       // ['Update', 'Delete']
kvEventTypes            // ['Shared', 'Own', 'Gate']

State Functions

getState(bitmask)

Extract the state value from the low 8 bits.

getState(bitmask: bigint) → number  // 0-255
getState(0x100n)  // 0 (outsider, but has trait bit 8 set)
getState(0x103n)  // 3

setState(bitmask, stateValue)

Set the state value, preserving trait bits.

setState(bitmask: bigint, stateValue: number) → bigint
setState(0x100n, 5)  // 0x105n — keeps owner bit, sets state to 5

isOutsider(bitmask)

Check if the identity has state 0 (outsider/no membership).

isOutsider(bitmask: bigint) → boolean

Trait Functions

hasTrait(bitmask, traitBit)

Check if a trait bit is set.

hasTrait(bitmask: bigint, traitBit: number) → boolean
hasTrait(0x100n, 8)  // true (OWNER_BIT)
hasTrait(0x100n, 9)  // false

setTrait(bitmask, traitBit)

Set a trait bit.

setTrait(bitmask: bigint, traitBit: number) → bigint

clearTrait(bitmask, traitBit)

Clear a trait bit.

clearTrait(bitmask: bigint, traitBit: number) → bigint

isOwner(mask)

Check if the owner trait (bit 8) is set.

isOwner(mask: bigint) → boolean

bestRank(mask, traitRanks)

Find the lowest (best) rank among all traits the identity holds.

bestRank(mask: bigint, traitRanks: [number, number][]) → number | 'Infinity'

Each entry in traitRanks is [traitBit, rank]. Returns the minimum rank for traits that are set, or 'Infinity' if none match.

clearAllTraits(bitmask)

Clear all trait bits, keeping only the state.

clearAllTraits(bitmask: bigint) → bigint
clearAllTraits(0x1FFn)  // 0xFFn (all traits cleared, state preserved)

Alias Functions

These are aliases for the trait functions, using bit parameter name:

setRoleBit(bitmask, bit)    // alias for setTrait
clearRoleBit(bitmask, bit)  // alias for clearTrait
hasBit(bitmask, bit)        // alias for hasTrait

Note: Prefer setTrait / clearTrait / hasTrait directly; the *Bit aliases are retained only for backwards compatibility.

Type Checking Functions

isContext(role)

Check if a role name is a context role (Self, Sender, or Public).

isContext(role: string) → boolean

isACEventType(type)

Check if a type starts with any AC event type string.

isACEventType(type: string) → boolean

isUpdateDeleteType(type)

Check if the type is 'Update' or 'Delete'.

isUpdateDeleteType(type: string) → boolean

isKVEventType(type)

Check if the type starts with 'Shared(', 'Own(', or 'Gate('.

isKVEventType(type: string) → boolean

Schema Functions

schemaPermits(schema, roleName, eventType, op)

Check if a schema grants an operation to a role for an event type.

schemaPermits(
  schema: { event: string, role: string, ops: string[] }[],
  roleName: string,
  eventType: string,
  op: string
) → boolean

Matches event === eventType or event === '*' (wildcard).

isAuthorized(schema, bitmask, eventType, op, isSelf?, isSender?, stateNames?, traitNames?)

The main authorization function. Evaluates all applicable roles and returns whether the operation is permitted.

isAuthorized(
  schema: { event: string, role: string, ops: string[] }[],
  bitmask: bigint,          // identity's role bitmask
  eventType: string,        // event type being checked
  op: string,               // operation (e.g. 'C', 'R')
  isSelf?: boolean,         // is the identity the event author? (default: false)
  isSender?: boolean,       // is the identity the commit sender? (default: false)
  stateNames?: string[],    // ordered state names from manifest (default: [])
  traitNames?: string[]     // ordered trait names from manifest (default: [])
) → boolean

Evaluation order:

  1. Resolve state name from bitmask low 8 bits (index into stateNames, 0 = OUTSIDER)
  2. Collect ops from the state role
  3. Collect ops from each held trait
  4. If isSelf, collect ops from 'Self' role
  5. If isSender, collect ops from 'Sender' role
  6. Always collect ops from 'Public' and 'Any' roles
  7. Deny operations override grants (any _X removes X)
import { isAuthorized, OWNER_BIT, setTrait, setState } from '@enc-protocol/core/rbac.js'
 
const schema = [
  { event: 'post', role: 'owner', ops: ['C', 'U', 'D'] },
  { event: '*', role: 'Public', ops: ['R'] },
]
 
// Owner with state 1 ('member')
const ownerMask = setTrait(setState(0n, 1), OWNER_BIT)
 
isAuthorized(schema, ownerMask, 'post', 'C', false, false, ['member'], ['owner'])
// true — owner role grants C on 'post'
 
isAuthorized(schema, 0n, 'post', 'R', false, false, [], [])
// true — Public grants R on '*'
 
isAuthorized(schema, 0n, 'post', 'C', false, false, [], [])
// false — outsider has no C grant

getCustomRoleNames(schema)

Extract custom (non-context) role names from a schema.

getCustomRoleNames(schema: Object[]) → string[]

Filters out 'Self', 'Sender', 'Public', and 'Any'.

roleBitFromName(name, customRoles)

Get the trait bit position for a custom role name.

roleBitFromName(name: string, customRoles: string[]) → number | null

Returns FIRST_TRAIT_BIT + indexOf(name) or null if not found.

RBACState Class

Manages per-identity role bitmasks and event status.

import { RBACState } from '@enc-protocol/core/rbac.js'
 
const state = new RBACState()

getRoles(identity)

Get the role bitmask for an identity.

state.getRoles(identity: string) → bigint  // defaults to 0n

setRoles(identity, mask)

Set the entire role bitmask for an identity.

state.setRoles(identity: string, mask: bigint)

grantRole(identity, bit)

Set a single trait bit for an identity.

state.grantRole(identity: string, bit: number)

revokeRole(identity, bit)

Clear a single trait bit for an identity.

state.revokeRole(identity: string, bit: number)

revokeAll(identity)

Remove all roles for an identity (delete from map).

state.revokeAll(identity: string)

markDeleted(eventId)

Mark an event as deleted in event status tracking.

state.markDeleted(eventId: string)

markUpdated(targetId, updateId)

Mark an event as updated, storing the update event ID.

state.markUpdated(targetId: string, updateId: string)

getEventStatus(eventId)

Get an event's status.

state.getEventStatus(eventId: string) → 'Active' | 'Deleted' | string
// Returns 'Active' if not tracked, 'Deleted' if deleted, or the update event ID

applyInitialState(initialState, schema, stateNames, traitNames)

Apply initial state from a manifest's initial_state field.

state.applyInitialState(
  initialState: { [roleName: string]: (string | { identity: string })[] },
  schema: Object[],
  stateNames: string[],
  traitNames: string[]
)

smt.js

Sparse Merkle Tree with 168-bit depth (21-byte keys). Provides authenticated state proofs for RBAC roles, event status, and key-value state.

Constants

import {
  DEPTH,                 // 168 — tree depth in bits
  KEY_BYTES,             // 21  — key size in bytes
  EVENT_STATUS_DELETED,  // Uint8Array([0]) — sentinel value for deleted events
} from '@enc-protocol/core/smt.js'

Key Building Functions

All keys are 21 bytes: 1 namespace byte + 20 bytes from sha256(rawKey).

buildSMTKey(namespace, rawKey)

Build a generic SMT key.

buildSMTKey(namespace: number, rawKey: Uint8Array) → Uint8Array(21)

buildRBACKey(identityHex)

Build an RBAC state key for an identity.

buildRBACKey(identityHex: string) → Uint8Array(21)
// namespace = SMTNamespace.RBAC

buildEventStatusKey(eventIdHex)

Build an event status key.

buildEventStatusKey(eventIdHex: string) → Uint8Array(21)
// namespace = SMTNamespace.EventStatus

buildKVKey(kvKey, identity)

Build a key-value state key.

buildKVKey(kvKey: string, identity?: string) → Uint8Array(21)
// namespace = SMTNamespace.KV
// If identity provided: sha256(kvKey + identity); otherwise sha256(kvKey)

Wire Format Conversion

proofToWire(proof)

Convert a proof object to wire format (hex strings).

proofToWire(proof: { key, value, bitmap, siblings }) → {
  k: string,           // hex key
  v: string | null,    // hex value (null for non-membership)
  b: string,           // hex bitmap
  s: string[]          // hex siblings
}

wireToProof(wire)

Convert wire format back to a proof object (Uint8Arrays).

wireToProof(wire: { k, v, b, s }) → {
  key: Uint8Array,
  value: Uint8Array | null,
  bitmap: Uint8Array,
  siblings: Uint8Array[]
}

Encoding Functions

encodeRoleBitmask(bitmask)

Encode a bigint bitmask as 32 big-endian bytes for SMT storage.

encodeRoleBitmask(bitmask: bigint) → Uint8Array(32)

decodeRoleBitmask(bytes)

Decode 32 big-endian bytes back to a bigint bitmask.

decodeRoleBitmask(bytes: Uint8Array) → bigint

encodeEventStatus(status)

Encode an event status for SMT storage.

encodeEventStatus(status: string) → Uint8Array
// 'Deleted' → Uint8Array([0])
// Otherwise → hexToBytes(status) (the update event ID)

verify(proof, expectedRoot)

Verify an SMT membership or non-membership proof against a root hash.

verify(
  proof: { key: Uint8Array, value: Uint8Array | null, bitmap: Uint8Array, siblings: Uint8Array[] },
  expectedRoot: Uint8Array
) → boolean

For membership proofs, value is non-null and the proof demonstrates the key-value pair exists in the tree. For non-membership proofs, value is null and the proof demonstrates the key is absent.

The bitmap is a 21-byte array where each bit indicates whether a sibling exists at that depth. The siblings array contains only the non-empty siblings, in order from leaf to root.

import { verify, wireToProof } from '@enc-protocol/core/smt.js'
import { hexToBytes } from '@enc-protocol/core/crypto.js'
 
// From a node API response
const proof = wireToProof(apiResponse.proof)
const root = hexToBytes(apiResponse.smt_root)
const valid = verify(proof, root)

SparseMerkleTree Class

Full in-memory SMT implementation.

import { SparseMerkleTree } from '@enc-protocol/core/smt.js'
 
const smt = new SparseMerkleTree()

getRoot()

Get the current root hash.

smt.getRoot() → Uint8Array(32)
// Returns SMT_EMPTY_HASH when tree is empty

getRootHex()

Get the root hash as a hex string.

smt.getRootHex() → string  // 64 hex chars

get(key)

Get the value for a key.

smt.get(key: Uint8Array) → Uint8Array | null

insert(key, value)

Insert or update a key-value pair. Recomputes the root.

smt.insert(key: Uint8Array, value: Uint8Array)

remove(key)

Remove a key. Recomputes the root.

smt.remove(key: Uint8Array)

prove(key)

Generate a membership or non-membership proof.

smt.prove(key: Uint8Array) → {
  key: Uint8Array,
  value: Uint8Array | null,
  bitmap: Uint8Array(21),
  siblings: Uint8Array[]
}

serialize() / deserialize(data)

Serialize the tree to/from a JSON-compatible format.

smt.serialize() → { leaves: [string, string][] }  // [keyHex, valueHex] pairs
smt.deserialize(data: { leaves: [string, string][] })

Note: deserialize is an instance method (not static) that clears and repopulates the tree.

SparseMerkleTree.verify

Static reference to the module-level verify function.

SparseMerkleTree.verify(proof, expectedRoot) → boolean
import { SparseMerkleTree, buildRBACKey, encodeRoleBitmask } from '@enc-protocol/core/smt.js'
import { OWNER_BIT, setTrait, setState } from '@enc-protocol/core/rbac.js'
 
const smt = new SparseMerkleTree()
 
// Insert an owner role
const key = buildRBACKey('abcd1234...')  // 64 hex pubkey
const mask = setTrait(setState(0n, 1), OWNER_BIT)
smt.insert(key, encodeRoleBitmask(mask))
 
// Generate and verify proof
const proof = smt.prove(key)
const valid = SparseMerkleTree.verify(proof, smt.getRoot())
console.log(valid) // true

ct.js

Certificate Transparency tree following RFC 9162. Provides append-only event log verification with inclusion and consistency proofs.

verifyInclusionProof(leafHash, leafIndex, treeSize, path, expectedRoot)

Verify that a leaf is included in a tree of a given size.

verifyInclusionProof(
  leafHash: Uint8Array,     // 32-byte leaf hash
  leafIndex: number,        // 0-based leaf index
  treeSize: number,         // total number of leaves
  path: Uint8Array[],       // proof path (array of 32-byte hashes)
  expectedRoot: Uint8Array  // 32-byte expected root
) → boolean

Implements the RFC 9162 inclusion proof verification algorithm.

import { verifyInclusionProof } from '@enc-protocol/core/ct.js'
import { hexToBytes } from '@enc-protocol/core/crypto.js'
 
const sth = await (await fetch(`${nodeUrl}/${enclaveId}/sth`)).json()
// Get inclusion proof from node API
const inclProof = await (await fetch(`${nodeUrl}/inclusion`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ seq: 0 }),
})).json()
 
const path = inclProof.ct_proof.p.map(h => hexToBytes(h))
const leafHash = hexToBytes(inclProof.leaf_hash)
const valid = verifyInclusionProof(leafHash, 0, sth.t, path, hexToBytes(sth.r))

verifyConsistencyProof(size1, size2, path, firstRoot, secondRoot)

Verify that a smaller tree is a prefix of a larger tree (append-only property).

verifyConsistencyProof(
  size1: number,            // earlier tree size
  size2: number,            // later tree size
  path: Uint8Array[],       // consistency proof path
  firstRoot: Uint8Array,    // 32-byte root at size1
  secondRoot: Uint8Array    // 32-byte root at size2
) → boolean

Edge cases:

  • size1 > size2 returns false
  • size1 === 0 returns true (empty tree is prefix of everything)
  • size1 === size2 requires path.length === 1 with matching roots

verifyBundleMembership(eventIdHex, proof, expectedRootHex)

Verify that an event ID is part of a bundle's events root.

verifyBundleMembership(
  eventIdHex: string,         // 64 hex char event ID
  proof: { ei: number, s: string[] },  // bundle membership proof
  expectedRootHex: string     // 64 hex char expected events root
) → boolean

The proof contains:

  • ei — event index within the bundle
  • s — array of sibling hashes (hex strings)

bundleMembershipProof(eventIds, eventIndex)

Generate a bundle membership proof.

bundleMembershipProof(
  eventIds: string[],    // array of event ID hex strings
  eventIndex: number     // index of the event to prove
) → { ei: number, s: string[] }

Throws if eventIndex >= eventIds.length.

CTTree Class

Full in-memory Certificate Transparency tree.

import { CTTree } from '@enc-protocol/core/ct.js'
 
const ct = new CTTree()

size (getter)

Number of leaves in the tree.

ct.size → number

getRoot() / getRootHex()

Get the current Merkle root.

ct.getRoot() → Uint8Array(32)
ct.getRootHex() → string  // 64 hex chars
// Empty tree returns 64 zero bytes

append(eventsRoot, stateHash)

Append a new leaf (computed as ctLeafHash(eventsRoot, stateHash)).

ct.append(eventsRoot: Uint8Array, stateHash: Uint8Array) → number  // leaf index

inclusionProof(leafIndex)

Generate an inclusion proof.

ct.inclusionProof(leafIndex: number) → {
  ts: number,       // tree size at time of proof
  li: number,       // leaf index
  p: string[]       // proof path as hex strings
}

Throws if leafIndex >= tree size.

consistencyProof(size1, size2?)

Generate a consistency proof between two tree sizes.

ct.consistencyProof(size1: number, size2?: number) → {
  ts1: number,      // first tree size
  ts2: number,      // second tree size
  p: string[]       // proof path as hex strings
}

size2 defaults to current tree size. Throws if sizes are out of range.

serialize() / deserialize(data)

ct.serialize() → string[]          // array of hex leaf hashes
ct.deserialize(data: string[])     // restore from serialized form

manifest-validator.js

Static validation of an enclave manifest's RBAC structure against the protocol's naming and completeness rules. Generated from the Lean DSL and proven equivalent to the in-kernel validator.

validateManifest(manifestContent)

validateManifest(manifestContent: Object) → string | null

Returns null if the manifest is valid, or a human-readable error string identifying the first violated rule. Accepts either a full manifest object or its RBAC sub-object, normalizing the wire forms (string-or-object states, name(rank) traits, v1 schema event entries) before checking.

Rules enforced:

  • Rule 5 (Reserved Keys) — slot keys may not use the reserved gate: prefix or the key lifecycle.
  • Rule 8 (Complete States) — every state referenced by a move / grant / transfer scope must be declared (plus the implicit OUTSIDER).
  • Rule 9 (Naming) — states are UPPER_CASE, traits and slot keys are lower_case, and custom event names are lowercase or a known protocol event.
import { validateManifest } from '@enc-protocol/core/manifest-validator.js'
 
validateManifest({ RBAC: { states: ['member'], traits: ['owner(0)'], schema: [] } })
// 'Rule 9 (Naming): State "member" must be UPPER_CASE'

snapshot.js

The .enc portable snapshot container format — a header, payload, and SHA-256 footer — with kernel-version compatibility checks. A snapshot round-trips an enclave's full state byte-identically across hosts.

Constants

SNAPSHOT_MAGIC          // Uint8Array — 'ENC\x01' (0x454e4301)
SNAPSHOT_HEADER_SIZE    // 32
SNAPSHOT_FOOTER_SIZE    // 32
SNAPSHOT_LAYOUT_VERSION // 1

pack(kernelVer, payload) / unpack(bytes)

pack(kernelVer: number, payload: Uint8Array) → Uint8Array
unpack(bytes: Uint8Array) → { ok: true, header, payload } | { ok: false, code }

pack frames a payload into a .enc container: a 32-byte header (magic, layout version, packed kernel version, flags, payload size), the payload, then a 32-byte SHA-256 footer over header || payload. unpack validates magic, layout version, flags, size, and footer, returning the parsed header and payload — or an error code (TOO_SHORT, BAD_MAGIC, UNKNOWN_LAYOUT_VERSION, UNKNOWN_FLAGS, SIZE_MISMATCH, FOOTER_MISMATCH).

Kernel version helpers

packSemver(major, minor, patch) → number
majorOf(v) / minorOf(v) / patchOf(v) → number
kernelVerCompatible(producer, restorer)
'same' | 'patchDiff' | 'minorDiff' | 'majorDiff' | 'preOneAnyDiff'
isAcceptable(compat) → boolean   // true for same / patchDiff / minorDiff

kernelVerCompatible classifies whether a snapshot produced by one kernel version can be restored by another; isAcceptable is the restore gate. Lower-level header readers (checkMagic, readLayoutVer, readKernelVer, readFlags, readPayloadSize) operate directly on the raw bytes.

import { pack, unpack, packSemver } from '@enc-protocol/core/snapshot.js'
 
const kernelVer = packSemver(0, 11, 0)
const blob = pack(kernelVer, payloadBytes)   // .enc container bytes
 
const r = unpack(blob)
// r.ok === true, r.header.kernelVer === kernelVer, r.payload matches payloadBytes

Confidentiality plugins

The core package ships the JS implementations of the protocol's confidentiality plugins — the same algorithms specified in the plugin profiles. Each composes only the verified primitives in crypto.js (no raw crypto), so the wire bytes match the spec's known-answer vectors. Each plugin imports from its own module. Full per-plugin reference: Plugin SDKs.

ratchet-pair — dm-ratchet.js

Per-message symmetric ratchet for 1:1 (DM) confidentiality (ratchet-pair), plus a NIP-44 v2-compatible code path.

ratchetMessageKey(epochSecret: Uint8Array, senderSeq: number) → Uint8Array(32)
dmRatchetEncrypt(epochSecret, plaintext: string, senderSeq: number, epochN: number)
  → { epoch, sender_seq, ciphertext }
dmRatchetDecrypt(epochSecret, ciphertextB64: string, senderSeq: number) → string

The ratchet derives a fresh message key per senderSeq by chaining HKDF advances from the epoch secret, so every message uses a unique key. NIP-44 helpers (nip44Encrypt, nip44Decrypt, and the lower-level nip44ConversationKey / nip44MessageKeys / pad / HMAC functions) provide interop with the Nostr NIP-44 v2 envelope.

group-ratchet — group-ratchet.js

Per-sender symmetric ratchet for group messages.

groupRatchetMessageKey(epochSecret, senderPub: string, senderSeq: number) → Uint8Array(32)
groupEncrypt(groupSecret, senderPub, seq, plaintext) → { ciphertext, nonce }
groupDecrypt(groupSecret, senderPub, seq, ciphertextHex, nonceHex) → string

Each sender gets an independent ratchet chain seeded from the group's epoch secret and the sender's public key, so message keys never collide across senders.

mls-lazy — mls-lazy.js

Lazy-MLS group key agreement (mls-lazy): a binary ratchet tree giving O(log N) epoch-key distribution for large groups.

prepareCommit(opts) → { newEpochSecret, newTreeState, envelope }
consumeCommit(opts) → { newEpochSecret, newTreeState }
wireEnvelope(prepareResult) / parseWireEnvelope(content)
encryptMessage(opts) → { epoch_n, sender_pub, sender_seq, ciphertext, nonce }
decryptMessage(opts) → string | Uint8Array
deriveSenderMessageKey(epochSecret, senderPubHex, senderSeq) → Uint8Array(32)

prepareCommit (run by the committer) generates a new epoch root and wraps it to each member along the tree copath; consumeCommit (run by each member) unwraps the one path entry decryptable with that member's identity key. Once a member holds the epoch secret, encryptMessage / decryptMessage run the per-sender ratchet. The module also exports the pure tree-math helpers (paddedLeafCount, directPath, copath, lca, subtreeLeafIndices, …).

ecdh-envelope — ecdh-envelope.js

One-shot sender→recipient confidentiality for Personal notices and group-invite handoff (ecdh-envelope).

ecdhEnvelopeSeal(senderOpPriv, recipientOpPubHex, payload)
  → { ciphertext, nonce, sender_pub, scheme, encrypted }
ecdhEnvelopeOpen(myOpPriv, envelope) → payload
ecdhEnvelopeWrapHandoff(committerPriv, recipientOpPubHex, rootSecret) → handoff
ecdhEnvelopeUnwrapHandoff(myOpPriv, myOpPubHex, handoff) → Uint8Array(32) | null

Seal / Open carry an invite payload between operator keys via ECDH + HKDF (enc:personal:notice); the handoff pair distributes a 32-byte group root secret to a new member. The *WithNonce variants are deterministic, for the spec's KAT vectors.

identity-aead — identity-aead.js

Single-owner deterministic confidentiality for Personal private content (identity-aead) — no ECDH, no rotation.

identityAeadEncrypt(identityPriv, enclaveIdHex, plaintext) → { ciphertext, nonce }
identityAeadDecrypt(identityPriv, enclaveIdHex, envelope) → string
identityAeadContentKey(identityPriv, enclaveIdHex) → Uint8Array(32)

The content key is HKDF(identity_priv, "enc-personal-private:" + lowercase(enclaveId)) — derived from the owner's identity alone, so only the owner can read their own private content, with no key exchange.


aggregator.js — Hub

In-process routing core for the WebSocket aggregation hub — the same sub_id multiplexing logic the hub Worker runs, exposed as a pure state machine.

import { Hub } from '@enc-protocol/core/aggregator.js'
 
const hub = new Hub()
hub.processClientQuery(client, clientSubId, enclave, queryBody) → outFrames
hub.processClientClose(client, clientSubId) → outFrames
hub.processUpstreamFrame(frame) → outFrames
hub.processUpstreamDeath(enclave) → notifications
hub.upstreamRefcount(enclave) → number
hub.hasRoute(client, clientSubId) → boolean

Each method folds an event into the hub's route table and returns the frames to emit. The underlying transitions are also exported individually from aggregator-core.js (hubInit, processClientQuery, …) as a pure (state, input) → { state, out } reducer.


WASM-accelerated modules

Every core module has a WASM-backed twin — crypto-wasm.js, event-wasm.js, rbac-wasm.js, smt-wasm.js, ct-wasm.js, aggregator-wasm.js, snapshot-wasm.js — exposing the same functions backed by the enc-core.wasm kernel instead of pure JS. They produce byte-identical results to their JS counterparts (this is the cross-implementation parity property) and are used where the WASM kernel is already loaded. The pure-JS modules above are the portable default and need no WASM to run.


Complete Example: Create Enclave and Submit Events

import {
  generateKeypair, derivePublicKey, bytesToHex, hexToBytes,
  verifySTH, computeEventsRoot,
} from '@enc-protocol/core/crypto.js'
import {
  mkCommit, signCommit, mkManifestCommit, verifyEvent,
} from '@enc-protocol/core/event.js'
import { verifyInclusionProof } from '@enc-protocol/core/ct.js'
import { verify, wireToProof, buildRBACKey, decodeRoleBitmask } from '@enc-protocol/core/smt.js'
import { isOwner } from '@enc-protocol/core/rbac.js'
 
const NODE = 'https://your-node.example.com'
 
// 1. Generate identity
const kp = generateKeypair()
const pub = bytesToHex(kp.publicKey)
 
// 2. Create manifest
const manifest = JSON.stringify({
  enc_v: 2,
  nonce: Date.now(),
  RBAC: {
    use_temp: 'none',
    schema: [
      { event: 'post', role: 'owner', ops: ['C', 'U', 'D'] },
      { event: '*', role: 'Public', ops: ['R'] },
    ],
    states: [],
    traits: ['owner(0)'],
    initial_state: { owner: [pub] },
  },
})
 
// 3. Submit manifest commit
const manifestCommit = signCommit(
  mkManifestCommit(pub, manifest, Date.now() + 300000, []),
  kp.privateKey
)
const createRes = await (await fetch(NODE, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(manifestCommit),
})).json()
 
const enclaveId = manifestCommit.enclave
const seqPub = createRes.sequencer
 
// 4. Submit a post
const postCommit = signCommit(
  mkCommit(enclaveId, pub, 'post', JSON.stringify({ body: 'hello' }), Date.now() + 300000, []),
  kp.privateKey
)
const receipt = await (await fetch(NODE, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(postCommit),
})).json()
 
console.log('Event ID:', receipt.id)
 
// 5. Verify signed tree head
const sth = await (await fetch(`${NODE}/${enclaveId}/sth`)).json()
const sthValid = verifySTH(sth.t, sth.ts, hexToBytes(sth.r), sth.sig, hexToBytes(seqPub))
console.log('STH valid:', sthValid)
 
// 6. Pull and verify events
const pullRes = await (await fetch(NODE, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ type: 'Pull', enclave: enclaveId, after_seq: -1, limit: 100 }),
})).json()
 
for (const event of pullRes.events) {
  console.log(`Event ${event.seq}: ${event.type} — verified: ${verifyEvent(event)}`)
}

Dependencies

All cryptographic operations use audited @noble libraries:

PackageVersionPurpose
@noble/curves^1.8.0secp256k1 (Schnorr, ECDH)
@noble/hashes^1.7.0SHA-256, HKDF
@noble/ciphers^0.6.0XChaCha20-Poly1305