@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 unauthenticatedOp
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 NotifyACEventType
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 entriesLifecycleState
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: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855Key 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 charsderivePublicKey(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-onlyEncoding Utilities
bytesToHex(bytes)
Convert a Uint8Array to a lowercase hex string.
bytesToHex(bytes: Uint8Array) → stringbytesToHex(new Uint8Array([0xca, 0xfe])) // 'cafe'hexToBytes(hex)
Convert a hex string to a Uint8Array. Accepts optional 0x prefix.
hexToBytes(hex: string) → Uint8ArrayhexToBytes('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 charsReturns 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 charsReturns 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 scalarschnorrVerify(msgHash, signature, publicKey)
Verify a BIP-340 Schnorr signature.
schnorrVerify(
msgHash: Uint8Array, // 32 bytes
signature: Uint8Array, // 64 bytes
publicKey: Uint8Array // 32 bytes (x-only)
) → booleanReturns 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 nowverifySession(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 | nullReturns 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 secretlabel— 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 // base64Returns 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) → stringSplits 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 signatureThe 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
) → booleanReturns 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) → ObjectReturns 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 stringverifyCommit(commit)
Verify that a commit's signature matches its hash and from pubkey.
verifyCommit(commit: Object) → booleanReturns 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) → booleanFirst 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 fieldThe 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 IDfinalizeCommit(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, idAdds 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 | undefinedReturns 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) → booleanUses 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) → booleanrbac.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-255getState(0x100n) // 0 (outsider, but has trait bit 8 set)
getState(0x103n) // 3setState(bitmask, stateValue)
Set the state value, preserving trait bits.
setState(bitmask: bigint, stateValue: number) → bigintsetState(0x100n, 5) // 0x105n — keeps owner bit, sets state to 5isOutsider(bitmask)
Check if the identity has state 0 (outsider/no membership).
isOutsider(bitmask: bigint) → booleanTrait Functions
hasTrait(bitmask, traitBit)
Check if a trait bit is set.
hasTrait(bitmask: bigint, traitBit: number) → booleanhasTrait(0x100n, 8) // true (OWNER_BIT)
hasTrait(0x100n, 9) // falsesetTrait(bitmask, traitBit)
Set a trait bit.
setTrait(bitmask: bigint, traitBit: number) → bigintclearTrait(bitmask, traitBit)
Clear a trait bit.
clearTrait(bitmask: bigint, traitBit: number) → bigintisOwner(mask)
Check if the owner trait (bit 8) is set.
isOwner(mask: bigint) → booleanbestRank(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) → bigintclearAllTraits(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 hasTraitNote: 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) → booleanisACEventType(type)
Check if a type starts with any AC event type string.
isACEventType(type: string) → booleanisUpdateDeleteType(type)
Check if the type is 'Update' or 'Delete'.
isUpdateDeleteType(type: string) → booleanisKVEventType(type)
Check if the type starts with 'Shared(', 'Own(', or 'Gate('.
isKVEventType(type: string) → booleanSchema 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
) → booleanMatches 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: [])
) → booleanEvaluation order:
- Resolve state name from bitmask low 8 bits (index into
stateNames, 0 = OUTSIDER) - Collect ops from the state role
- Collect ops from each held trait
- If
isSelf, collect ops from'Self'role - If
isSender, collect ops from'Sender'role - Always collect ops from
'Public'and'Any'roles - Deny operations override grants (any
_XremovesX)
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 grantgetCustomRoleNames(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 | nullReturns 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 0nsetRoles(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 IDapplyInitialState(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.RBACbuildEventStatusKey(eventIdHex)
Build an event status key.
buildEventStatusKey(eventIdHex: string) → Uint8Array(21)
// namespace = SMTNamespace.EventStatusbuildKVKey(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) → bigintencodeEventStatus(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
) → booleanFor 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 emptygetRootHex()
Get the root hash as a hex string.
smt.getRootHex() → string // 64 hex charsget(key)
Get the value for a key.
smt.get(key: Uint8Array) → Uint8Array | nullinsert(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) → booleanimport { 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) // truect.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
) → booleanImplements 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
) → booleanEdge cases:
size1 > size2returnsfalsesize1 === 0returnstrue(empty tree is prefix of everything)size1 === size2requirespath.length === 1with 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
) → booleanThe proof contains:
ei— event index within the bundles— 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 → numbergetRoot() / getRootHex()
Get the current Merkle root.
ct.getRoot() → Uint8Array(32)
ct.getRootHex() → string // 64 hex chars
// Empty tree returns 64 zero bytesappend(eventsRoot, stateHash)
Append a new leaf (computed as ctLeafHash(eventsRoot, stateHash)).
ct.append(eventsRoot: Uint8Array, stateHash: Uint8Array) → number // leaf indexinclusionProof(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 formComplete 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:
| Package | Version | Purpose |
|---|---|---|
@noble/curves | ^1.8.0 | secp256k1 (Schnorr, ECDH) |
@noble/hashes | ^1.7.0 | SHA-256, HKDF |
@noble/ciphers | ^0.6.0 | XChaCha20-Poly1305 |