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@0.2.0, the protocol primitives package for the ENC Protocol. All code is generated from the Lean 4 DSL and formally verified (282 theorems). Do not edit the source files directly.

Registry: https://npm-registry.ocrybit.workers.dev/

Installation

npm install @enc-protocol/core --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)

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://enc-node.ocrybit.workers.dev/${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: The source code contains a bug where these aliases reference traitBit instead of bit. Use setTrait/clearTrait/hasTrait directly instead.

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

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://enc-node.ocrybit.workers.dev'
 
// 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