Guide for AI agents working in the @chelonia/lib codebase.
@chelonia/lib is a TypeScript library for building end-to-end encrypted (E2EE), federated applications using Shelter Protocol. It provides core functionality for decentralized applications with encryption, federation, and secure data synchronization:
- E2EE smart contracts with key management
- E2EE file storage and retrieval
- E2EE key-value store
- E2EE WebSocket-based encrypted pubsub messaging
- Zero-knowledge password proofs (ZKPP)
- Persistent action queue with retry logic
Every contract is represented as an append-only log of operations (op codes). Some of the opcodes are for managing keys, others are for updating contract state. Clients that are interested in the contracts will subscribe to them, load the chain of events, decrypt them locally on-device, and process them to update their local contract state. Each contract is referenced by its contractID, the hash of the first message in the contract. Contracts can be multi-writer and multi-reader, but a message can only be added on top of the previous one, with the server acting as the source of truth for message ordering. The structure of a message is defined by an object called SPMessage and contains the hash of the previous message in a field called previousHEAD.
npm install # Install dependencies
npm test # Run tests (node:test via ts-node with ESM loader)
npm run build # Build both ESM and CJS outputs
npm run build:esm # Build ESM only (.mjs files)
npm run build:cjs # Build CJS only (.cjs files)
npm run lint # Run ESLint
npm run clean # Remove dist/ artifactsDual ESM/CJS output with TypeScript declarations:
| Output | Directory | Extensions | Config |
|---|---|---|---|
| ESM | dist/esm/ |
.mjs, .d.mts |
tsconfig.json (target ES2022, module esnext) |
| CJS | dist/cjs/ |
.cjs, .d.cts |
tsconfig.cjs.json (module nodenext) |
- Temporarily changes
package.json"type"field (to"module"or"commonjs") - Runs
tscwith the appropriate tsconfig - Runs
renameFiles.mjsto rename.js→.mjs/.cjsand rewrite import paths within files - Restores
package.json
Note: CJS build excludes test files via tsconfig.cjs.json.
| Package | Purpose |
|---|---|
@sbp/sbp |
Selector-based programming core library for registering and calling selectors |
@chelonia/crypto |
Cryptographic operations |
@chelonia/serdes |
Serialization/deserialization |
turtledash |
Utility functions (lodash-style) |
tweetnacl |
NaCl cryptography |
scrypt-async |
Key derivation |
@sbp/okturtles.data |
In-memory key-value store |
@sbp/okturtles.events |
Event emitter system |
@sbp/okturtles.eventQueue |
Serialized event queue for async operations |
All source lives in src/. The largest files are internals.ts and chelonia.ts.
src/
├── index.ts # Entry point — re-exports all modules
├── chelonia.ts # Core framework — ~50 SBP selectors for contract lifecycle
├── internals.ts # Internal processing — message handling, sync, key ops
├── SPMessage.ts # Shelter Protocol message types and serialization
├── types.ts # All TypeScript type definitions
├── encryptedData.ts # EncryptedData<T> — encryption/decryption functions
├── signedData.ts # SignedData<T> — signing/verification functions
├── Secret.ts # Secret<T> — WeakMap-based secret wrapper
├── db.ts # Database abstraction (in-memory default)
├── files.ts # Encrypted file upload/download/delete
├── functions.ts # CID creation, blake32Hash, base64 utilities
├── utils.ts # Key lookup, permissions, event stream utilities
├── errors.ts # Custom error classes via ChelErrorGenerator factory
├── events.ts # Event name constants
├── constants.ts # INVITE_STATUS enum
├── persistent-actions.ts # PersistentAction queue with retry
├── presets.ts # Server preset for configuring Chelonia
├── time-sync.ts # Server time synchronization via monotonic offsets
├── chelonia-utils.ts # Optional utility selectors (e.g., chelonia/kv/queuedSet)
├── zkpp.ts # Zero-knowledge password proof primitives
├── zkppConstants.ts # ZKPP constants (AUTHSALT, CONTRACTSALT, etc.)
├── pubsub/
│ ├── index.ts # WebSocket PubSub client with reconnection logic
│ └── index.test.ts # Tests for reconnection delay
└── local-selectors/
└── index.ts # External state sync (e.g., Chelonia in service worker → Vuex in tab)
The codebase uses @sbp/sbp for dependency injection and module communication. Functions are registered as "selectors":
// Registering selectors
sbp('sbp/selectors/register', {
'chelonia/_init': function (this: CheloniaContext) { /* ... */ },
'chelonia/config': function (this: CheloniaContext) { /* ... */ },
// ...
})
// Selectors are called via `sbp('selector/name', ...args)`
await sbp('chelonia/contract/sync', contractID)Often the source file's default export is an array of the selector names it registered.
| Namespace | Location | Purpose |
|---|---|---|
chelonia/* |
chelonia.ts |
Public API — init, configure, connect, defineContract |
chelonia/out/* |
chelonia.ts |
Outgoing operations — actionEncrypted, keyAdd, keyDel, keyShare, keyRequest, atomic |
chelonia/contract/* |
chelonia.ts |
Contract lifecycle — sync, retain, release |
chelonia/private/* |
internals.ts |
Internal implementation — message processing, sync, key ops, side effects |
chelonia/private/in/* |
internals.ts |
Incoming message handlers — processMessage, syncContract, handleEvent |
chelonia/private/out/* |
internals.ts |
Outgoing internals — publishEvent |
chelonia.db/* |
db.ts |
Database primitives — get, set, delete, iterKeys, keyCount |
chelonia/db/* |
db.ts |
Higher-level DB — latestHEADinfo, getEntry, addEntry |
chelonia.persistentActions/* |
persistent-actions.ts |
Retry queue — configure, enqueue, cancel, status |
chelonia/kv/* |
chelonia-utils.ts |
Key-value store — queuedSet |
chelonia/externalStateSetup |
local-selectors/ |
External state synchronization |
chelonia/_init — Initialize Chelonia context (call automatically on registration)
chelonia/configure — Apply CheloniaConfig
chelonia/reset — Reset state
chelonia/connect — Connect to server
chelonia/defineContract — Register a contract definition
chelonia/contract/sync — Sync contract state from server
chelonia/contract/retain — Increment contract reference count
chelonia/contract/release — Decrement reference count (unsubscribe at 0)
chelonia/out/registerContract — Create a new contract on-chain
chelonia/out/actionEncrypted — Send an encrypted action
chelonia/out/actionUnencrypted — Send an unencrypted action
chelonia/out/keyAdd — Add a key to a contract
chelonia/out/keyDel — Remove a key from a contract
chelonia/out/keyUpdate — Rotate key and/or update key properties
chelonia/out/keyShare — Share a key with another contract
chelonia/out/keyRequest — Request keys from a contract
chelonia/out/atomic — Execute multiple operations atomically
- Outgoing: Create
SPMessage→ Sign/Encrypt → Publish viachelonia/private/out/publishEvent - Incoming: Receive via PubSub WebSocket or RESTful server endpoint → Deserialize → Validate signatures → Decrypt → Process → Update state → Run side effects
Types defined in SPMessage.ts (and implemented/processed in internals.ts):
| Operation | Type | Purpose |
|---|---|---|
OP_CONTRACT |
SPOpContract |
Create a new contract |
OP_ACTION_ENCRYPTED |
SPOpActionEncrypted |
Encrypted state mutation |
OP_ACTION_UNENCRYPTED |
SPOpActionUnencrypted |
Unencrypted state mutation |
OP_KEY_ADD |
SPOpKeyAdd |
Add authorized key |
OP_KEY_DEL |
SPOpKeyDel |
Remove authorized key |
OP_KEY_SHARE |
SPOpKeyShare |
Share key with another contract |
OP_KEY_REQUEST |
SPOpKeyRequest (V1/V2) |
Request keys |
OP_KEY_REQUEST_SEEN |
SPOpKeyRequestSeen |
Acknowledge key request |
OP_KEY_UPDATE |
SPOpKeyUpdate |
Rotate a key and/or update properties like "permissions"/"purpose" |
OP_PROP_SET |
SPOpPropSet |
Set contract properties |
OP_ATOMIC |
SPOpAtomic |
Atomic batch of operations |
Custom errors are generated using ChelErrorGenerator in src/errors.ts:
export const ChelErrorUnexpected = ChelErrorGenerator('ChelErrorUnexpected')
export const ChelErrorDecryptionError = ChelErrorGenerator('ChelErrorDecryptionError')Contracts update a state object as events/messages/actions come in by adding key/value parts to the state object.
This object also has internal keys managed by Chelonia that start with an underscore:
{
_vm: {
authorizedKeys: Record<string, ChelContractKey>,
type: string, // Contract type identifier
invites?: Record<string, {...}>, // Invite tracking
keyshares?: Record<string, {...}> // Key share tracking
},
// unlike _vm, _volatile is not sync'd between clients and is unique to each client
_volatile?: {
pendingKeyRequests?: [...], // Awaiting key responses
pendingKeyRevocations?: Record<string, ...>,
dirty?: boolean, // State needs re-sync
resyncing?: boolean // Currently re-syncing
}
}Uses @chelonia/crypto for cryptographic operations. The encryptedData.ts module provides:
encryptedOutgoingData()- Encrypt data for transmissionencryptedIncomingData()- Decrypt received dataencryptedOutgoingDataWithRawKey()- Encrypt with raw key
Always use .js extension for local imports even though source files are .ts:
import { something } from './utils.js' // ✅ Correct
import { something } from './utils' // ❌ Wrong
import { something } from './utils.ts' // ❌ WrongThis is required for ESM compatibility — TypeScript compiles .ts → .js and the import paths must match the output.
EncryptedData<T> and SignedData<T> use prototype-based type detection instead of instanceof:
// Checking type — uses a marker property on the prototype
function isEncryptedData(obj: unknown): obj is EncryptedData<unknown> {
return !!obj && (obj as any)._isEncryptedData === true
}Secret<T> stores values in a WeakMap keyed by the instance, preventing accidental exposure in logs or serialization. It implements @chelonia/serdes serialization symbols for controlled serialization.
const secret = new Secret(sensitiveValue)
secret.valueOf() // Returns the actual value
console.log(secret) // Does NOT reveal the valueAll event constants are in events.ts:
Events are emitted and listened to via sbp('okTurtles.events/emit', ...) and sbp('okTurtles.events/on', ...).
- Keys have
ringLevel(integer) — lower values have more authority and can rotate/revoke higher values purposeis an array of'enc' | 'sig' | 'sak'(encryption, signing, server accounting key)permissionscontrol what operations a key can authorize (e.g.,[OP_ACTION_ENCRYPTED])foreignKeyreferences keys in other contracts
ESLint with @typescript-eslint/parser and eslint-config-standard. Configuration is inline in package.json (not a separate file).
- Max line length: 100 characters (150 for comments)
- Template literals and strings are exempt from line length
- Uses
standardstyle (no semicolons, single quotes, 2-space indent)
Tests use Node.js built-in test runner (node:test):
import { describe, it } from 'node:test'
import assert from 'node:assert'
describe('Feature name', () => {
it('should do something', () => {
assert.strictEqual(actual, expected)
})
})Test files use .test.ts suffix and are imported in src/index.test.ts.
- Import extensions: Always use
.jsextension in imports, even for TypeScript files. The build process does not transform these. - SBP context: Many functions use
this: CheloniaContext- they must be called through SBP to have proper context binding. dist/under version control: Ignore the files indist/, don't review them or read them or update them.- Test isolation: Tests use SBP selectors which are globally registered. Mock functions carefully to avoid cross-test contamination.
- Secret keys: Secret keys are stored in
rootState.secretKeysas serialized strings and accessed viatransientSecretKeysproxy. - Reference counting for contracts — Use
chelonia/contract/retainandchelonia/contract/releasefor contract lifecycle. At refcount 0, the contract is unsubscribed. - Event queue serialization —
chelonia/db/addEntryand other operations useokTurtles.eventQueueto ensure serialized execution of asynchronous operations. Each contract has its own event queue under thecontractIDkey. Convention: to ensure an operation is run after a contract sync finishes, add it to this queue.