@lekinox/cero
v1.1.0
Published
P2P made easy
Maintainers
Readme
cero
Simple p2p lib.
Contents
- Builder - Spec building and extension
- Cero - Main entry point
- Identity - Cryptographic identity with multi-device support
- Room - Encrypted collaborative spaces
- Local - Local-only storage
- Pairing - Device and room pairing
- Flows - Common usage patterns
- Events - Reactive state tracking
- Errors - Error codes
Install
npm install ceroQuick Start
import { Cero } from 'cero'
const cero = new Cero('./storage', spec)
await cero.ready()
// Identity
await cero.profile.set({ name: 'Alice' })
const seed = cero.seed.get()
// Create a room
const room = await cero.rooms.create({ name: 'My Room' })
const { invite } = await room.invites.create({ role: 'member' })
// Join a room
const joined = await cero.rooms.pair(invite)
// Read and write data
await room.settings.set('topic', 'General')
const topic = await room.settings.get('topic')
const members = await room.members.list()
// Subscribe to live updates
for await (const members of room.members.subscribe()) {
console.log('Members:', members.length)
}
// Pair another device (seedless)
// Device A: create a device invite
const { invite: deviceInvite } = await cero.invites.create()
// share `deviceInvite` string with device B (QR code, paste, etc.)
// Device B: join with the invite
const ceroB = new Cero('./storage-b', spec)
await ceroB.ready()
await ceroB.pair(deviceInvite)
// Device B now shares identity, profile, and rooms with device A
// Recover from seed phrase (alternative)
const ceroR = new Cero('./storage-r', spec)
await ceroR.ready()
await ceroR.recover(seed)Schema
Cero uses hyperschema for data definitions. See hyperschema docs.
Builder
Build and extend specs with cero's base types for each data scope.
Scopes
SCOPE_LOCAL— Local-only data (not replicated)SCOPE_IDENTITY— Identity-scoped data (synced across user's devices)SCOPE_ROOM— Room-scoped data (synced across room members)SCOPE_RPC— RPC type definitions (no database or dispatch)
build(dir, ns?, fn?)
Build specs for all scopes. Auto-creates defaults — user only provides custom scopes.
import { build } from 'cero'
// Defaults only (no custom types):
build('./spec')
// Custom room types:
build('./spec', 'chat', ({ register }) => {
register.type({
name: 'message',
compact: false,
fields: [
{ name: 'id', type: 'string', required: true },
{ name: 'text', type: 'string', required: true }
]
})
register.collection({ name: 'messages', schema: '@chat/message', key: ['id'] })
register.dispatch({ name: 'message', requestType: '@chat/message' })
})
// Multiple custom scopes:
build('./spec', 'myapp', {
room({ register }) {
/* ... */
},
identity({ register }) {
/* ... */
},
rpc({ register }) {
/* custom RPC types and methods */
}
})extend(scope, { schema, database, dispatch })
Extend existing builder instances with cero base types for a given scope. Idempotent — safe to call multiple times.
import { extend, SCOPE_ROOM } from 'cero'
extend(SCOPE_ROOM, { schema, database, dispatch })
extend(SCOPE_LOCAL, { schema, database })
extend(SCOPE_IDENTITY, { schema, database, dispatch })Cero
new Cero(dir, spec, opts)
dir- Storage pathspec- Compiled schema specopts.seed- 12-word seed for restorationopts.replicate- Enable P2P (default: true)opts.bootstrap- DHT bootstrap nodes
const cero = new Cero('./storage', spec)
await cero.ready()Properties
cero.id- Z32-encoded identity public keycero.publicKey- Identity public key buffercero.deviceId- Z32-encoded device ID
cero.seed
const seed = cero.seed.get() // Get 12-word seed
cero.seed.clear() // Clear from memory
cero.seed.validate(seed) // { valid: true }cero.profile
await cero.profile.get() // { name, alias, meta, avatar }
await cero.profile.set({ name: 'Alice' })cero.profile.subscribe()
Subscribe to profile updates.
for await (const profile of cero.profile.subscribe()) {
console.log('Profile updated:', profile)
}cero.devices
await cero.devices.list()
List all devices linked to this identity. Each device includes isCurrent: true if it's the current device.
await cero.devices.get(id)
Get a device by ID. Includes isCurrent flag.
await cero.devices.set(id, updates)
Update device name, role, or meta.
await cero.devices.delete(id)
Remove a device from the identity.
await cero.devices.count()
Get the number of devices linked to this identity.
const count = await cero.devices.count()cero.devices.subscribe(id?)
Subscribe to device updates.
cero.invites
await cero.invites.create(opts?)
Create an invite for pairing a new device. Returns { id, invite, expires }.
const { invite } = await cero.invites.create({
role: 'write',
expires: Date.now() + 3600000
})await cero.invites.list()
List all invites.
await cero.invites.delete(id)
Delete an invite.
await cero.invites.count()
Get the number of active invites.
const count = await cero.invites.count()cero.pair / cero.recover
await cero.pair(invite, opts?)
Pair this device to an existing identity via invite (seedless pairing). The current empty identity is replaced with the linked identity.
const cero = new Cero('./new-device', spec)
await cero.ready()
// Pair using invite from existing device (no seed required)
await cero.pair(invite, { signal: AbortSignal.timeout(30000) })
// Now linked to the same identity
console.log(cero.id) // Same as original deviceawait cero.recover(seed, opts?)
Recover identity from seed phrase. Requires at least one existing device online.
const cero = new Cero('./new-device', spec)
await cero.ready()
await cero.recover(seed, { signal: AbortSignal.timeout(60000) })
// Now recovered with the same identity
console.log(cero.id) // Same as original devicecero.settings
Device-specific settings stored locally. NOT synced across devices. Use for preferences like theme, language, color.
await cero.settings.get(key)
Get a setting value by key. Returns the value or null.
const theme = await cero.settings.get('theme')
// 'dark'await cero.settings.set(key, value)
Set a setting value.
await cero.settings.set('theme', 'dark')
await cero.settings.set('language', 'en')
await cero.settings.set('color', 'teal')await cero.settings.delete(key)
Delete a setting.
await cero.settings.delete('theme')await cero.settings.list()
List all settings.
const settings = await cero.settings.list()
// [{ key: 'theme', value: 'dark' }, { key: 'language', value: 'en' }, ...]cero.rooms
await cero.rooms.create(opts?)
Create a new room. Sets room.refs = 1.
const room = await cero.rooms.create()
await room.profile.set({ name: 'My Room' })await cero.rooms.open(id)
Open a room by ID. Increments room.refs if already cached, otherwise opens and sets refs = 1.
const room = await cero.rooms.open(roomId)
// room.refs is now incrementedawait cero.rooms.pair(invite, opts?)
Join a room via invite. Sets room.refs = 1.
const room = await cero.rooms.pair(invite, {
signal: AbortSignal.timeout(30000)
})cero.rooms.release(room)
Decrement a room's reference count. Does not close the room.
cero.rooms.release(room)
// room.refs is decrementedawait cero.rooms.with(id, fn)
Open a room, execute a function, then release. Useful for per-request patterns.
const members = await cero.rooms.with(roomId, async (room) => {
return room.members.list()
})
// room.refs is automatically decremented after fn completesawait cero.rooms.close(room)
Force close a room and remove from cache. Ignores ref count.
await cero.rooms.close(room)await cero.rooms.list()
List all rooms.
await cero.rooms.get(id)
Get room metadata by ID.
await cero.rooms.search(query?)
Search rooms by name prefix (case-insensitive).
const rooms = await cero.rooms.search({ name: 'Alice' })await cero.rooms.delete(id)
Remove a room from the list.
await cero.rooms.count()
Get the number of rooms.
const count = await cero.rooms.count()cero.rooms.subscribe(id?)
Subscribe to room updates.
cero.rooms.subscribeWith(id, fn)
Open a room, create a subscription stream, and auto-cleanup when stream ends. Useful for per-request subscriptions.
const stream = await cero.rooms.subscribeWith(roomId, (room) => {
return room.members.subscribe()
})
for await (const members of stream) {
console.log('Members:', members.length)
}
// room.refs automatically decremented when stream endsawait cero.rooms.activate(id)
Open a room with full P2P sync enabled. Use this when the user navigates to a room.
const room = await cero.rooms.activate(roomId)
// room is open and actively syncing with peersawait cero.rooms.deactivate(id)
Switch a room to background sync and release it. Use this when the user navigates away from a room.
await cero.rooms.deactivate(roomId)
// room continues syncing in background modecero.rooms.isActive(id)
Check if a room has full sync enabled.
if (cero.rooms.isActive(roomId)) {
// room is actively syncing
}cero.rooms.on('update', ({ roomId }) => {})
Listen for updates to background rooms. Use this to show notification badges.
cero.rooms.on('update', ({ roomId }) => {
// Show notification badge for this room
setUnreadCounts((prev) => ({ ...prev, [roomId]: (prev[roomId] || 0) + 1 }))
})Identity
Manage cryptographic identity with multi-device support.
new Identity(store, spec, opts)
store- Corestore instancespec- Identity spec (spec.identity)opts.seed- Optional 12-word mnemonic seedopts.local- Local instance for persistenceopts.replicate- Enable replication (default: true)opts.bootstrap- DHT bootstrap nodes
import Corestore from 'corestore'
import { Identity, Local } from 'cero'
const localStore = new Corestore('./local', { manifestVersion: 2 })
const local = new Local(null, spec.local, { store: localStore })
const store = new Corestore('./storage', { manifestVersion: 2 })
const identity = new Identity(store, spec.identity, { local })
await identity.ready()Properties
identity.id
Z32-encoded identity public key.
identity.publicKey
Identity public key buffer.
identity.deviceId
Z32-encoded device ID (derived from writer key).
identity.writerKey
This device's writer public key buffer.
Methods
await identity.ready()
Wait for identity to be ready.
await identity.close()
Close identity and clean up resources.
identity.seed
identity.seed.get()
Get the 12-word BIP39 seed phrase. Throws if clear() was called.
const seed = identity.seed.get()
// 'witch collapse practice feed shame open despair creek road again ice least'identity.seed.clear()
Clear seed phrase from memory. Call after user confirms backup.
identity.profile
await identity.profile.get()
Get profile. Returns { name, alias, meta, avatar } or null.
const profile = await identity.profile.get()
// { name: 'Alice', alias: 'alice', meta: { bio: 'Hello' }, avatar: null }await identity.profile.set(updates)
Update profile fields. Partial updates preserve existing fields.
await identity.profile.set({
name: 'Alice',
alias: 'alice',
meta: { bio: 'Hello' },
avatar: avatarBuffer
})identity.profile.subscribe()
Subscribe to profile updates.
for await (const profile of identity.profile.subscribe()) {
console.log('Profile updated:', profile)
}identity.devices
await identity.devices.list(query?)
List all devices linked to this identity.
const devices = await identity.devices.list()
// [{ id, key, name, role, meta, isCurrent, createdAt, attestation }, ...]isCurrent-trueif this is the device running the current instance
await identity.devices.get(id)
Get a device by ID.
const device = await identity.devices.get(deviceId)
// { id, key, name, role, meta, isCurrent, createdAt, attestation }await identity.devices.set(id, updates)
Update device name, role, or meta.
await identity.devices.set(deviceId, { name: 'Work Laptop' })await identity.devices.delete(id)
Remove a device from the identity.
await identity.devices.delete(deviceId)await identity.devices.count()
Get the number of devices.
const count = await identity.devices.count()identity.devices.subscribe(id?)
Subscribe to device updates. Pass ID to watch single device, omit for all.
// All devices
for await (const devices of identity.devices.subscribe()) {
console.log('Devices:', devices.length)
}
// Single device
for await (const device of identity.devices.subscribe(deviceId)) {
console.log('Device updated:', device)
}identity.invites
await identity.invites.create(opts?)
Create an invite for pairing a new device. Returns { id, invite, expires }.
const { invite } = await identity.invites.create({
role: 'write', // optional, default: 'member'
expires: Date.now() + 3600000 // optional, 1 hour
})
// invite is a z32-encoded stringawait identity.invites.get(id)
Get invite by ID.
const invite = await identity.invites.get(inviteId)
// { id, publicKey, role, expires, createdBy, createdAt }await identity.invites.list()
List all invites.
const invites = await identity.invites.list()
// [{ id, publicKey, role, expires, createdBy, createdAt }, ...]await identity.invites.delete(id)
Delete an invite.
await identity.invites.delete(inviteId)await identity.invites.count()
Get the number of invites.
const count = await identity.invites.count()identity.invites.subscribe(id?)
Subscribe to invite updates.
for await (const invites of identity.invites.subscribe()) {
console.log('Invites:', invites.length)
}identity.rooms
Room list synced across all devices of this identity.
await identity.rooms.list()
List all rooms this identity is part of.
const rooms = await identity.rooms.list()
// [{ id, key, encryptionKey, name, createdAt }, ...]await identity.rooms.get(id)
Get a room by ID.
await identity.rooms.delete(id)
Remove a room from the list.
await identity.rooms.count()
Get the number of rooms.
const count = await identity.rooms.count()identity.rooms.subscribe(id?)
Subscribe to room list updates.
identity.settings
Key-value settings synced across all devices of this identity.
await identity.settings.get(key)
Get a setting value by key. Returns the value or undefined.
const theme = await identity.settings.get('theme')
// 'dark'await identity.settings.set(key, value)
Set a setting value. Accepts any JSON-serializable value.
await identity.settings.set('theme', 'dark')
await identity.settings.set('fontSize', 16)
await identity.settings.set('layout', { sidebar: true, compact: false })await identity.settings.list()
List all settings.
const settings = await identity.settings.list()
// [{ key: 'theme', value: 'dark' }, { key: 'fontSize', value: 16 }, ...]await identity.settings.delete(key)
Delete a setting.
await identity.settings.delete('theme')identity.settings.subscribe(key?)
Subscribe to setting updates. Pass key to watch single setting, omit for all.
// Single setting
for await (const theme of identity.settings.subscribe('theme')) {
console.log('Theme changed:', theme)
}
// All settings
for await (const settings of identity.settings.subscribe()) {
console.log('Settings:', settings.length)
}Counters
Generic counter methods for custom application use.
await identity.getCounter(name)
Get a counter value by name. Returns 0 if not set.
const count = await identity.getCounter('messages')await identity.setCounter(name, value)
Set a counter value.
await identity.setCounter('messages', 10)await identity.incCounter(name)
Increment a counter by 1. Returns the new value.
const count = await identity.incCounter('messages')await identity.decCounter(name)
Decrement a counter by 1 (floors at 0). Returns the new value.
const count = await identity.decCounter('messages')identity.attestation
await identity.attestation.create(data)
Create an attestation proof for data. Accepts buffers or JSON objects.
const { proof, receipt } = await identity.attestation.create(deviceKey)identity.attestation.verify(proof, data, opts?)
Verify an attestation proof.
const result = identity.attestation.verify(proof, data, {
maxAge: 5 * 60 * 1000, // optional, max age in ms
expectedIdentity: publicKey, // optional
receipt: previousReceipt // optional, for replay protection
})
// Returns { receipt, identityPublicKey, devicePublicKey } or nullidentity.signature
identity.signature.sign(data)
Sign data with identity key. Accepts buffers or JSON objects.
import b4a from 'b4a'
const sig1 = identity.signature.sign(b4a.from('message'))
const sig2 = identity.signature.sign({ action: 'transfer', amount: 100 })identity.signature.verify(data, signature, publicKey?)
Verify a signature. Uses identity public key by default.
const valid = identity.signature.verify(message, signature)
const valid2 = identity.signature.verify(message, signature, otherPublicKey)identity.getEncryptionKey(key)
Derive a symmetric encryption key from the identity's secret key chain.
const encryptionKey = identity.getEncryptionKey(identity.publicKey)
const roomKey = identity.getEncryptionKey(roomPublicKey)Static Methods
Identity.getSeedPhrase()
Generate a new 12-word BIP39 seed phrase.
const seed = Identity.getSeedPhrase()
// 'witch collapse practice feed shame open despair creek road again ice least'Identity.validateSeed(mnemonic, opts?)
Validate a mnemonic seed phrase. Returns { valid: boolean }.
const { valid } = Identity.validateSeed(
'witch collapse practice feed shame open despair creek road again ice least'
)
// { valid: true }Identity.pair(store, spec, invite, opts)
Pair a new device using an invite. Supports two modes:
Seed-based pairing - New device proves identity ownership via seed attestation:
const { invite } = await identity.invites.create()
const store = new Corestore('./new-device', { manifestVersion: 2 })
const pairer = Identity.pair(store, spec.identity, invite, {
seed: 'your 12 word seed phrase...'
})
const identity = await pairer.resolve()Seedless pairing - Host device attests for the new device (no seed required on joiner):
// On existing device, create invite
const { invite } = await identity.invites.create()
// On new device, pair WITHOUT seed - host will attest
const store = new Corestore('./new-device', { manifestVersion: 2 })
const pairer = Identity.pair(store, spec.identity, invite) // no seed
const identity = await pairer.resolve()
// This device is now a "linked device" without seed accessIdentity.recover(store, spec, seed, opts)
Recover an existing identity from seed phrase. Use when returning to an identity on a new device without an invite.
The recovery process:
- Derives discovery key from seed
- Finds an existing device via P2P discovery
- Requests to join as a new device
- Existing device adds the recoverer as a writer
import Corestore from 'corestore'
import { Identity } from 'cero'
const store = new Corestore('./new-device', { manifestVersion: 2 })
const recoverer = Identity.recover(store, spec.identity, 'your 12 word seed phrase...', {
replicate: true,
bootstrap: bootstrapNodes // optional
})
// Wait for recovery (requires an existing device online)
const identity = await recoverer.resolve({
signal: AbortSignal.timeout(60000) // optional timeout
})The recoverer emits a 'waiting' event while searching for peers:
recoverer.on('waiting', () => {
console.log('Looking for existing device...')
})Cancel recovery if needed:
await recoverer.cancel()Events
identity.on('update', () => {})
Emitted when the underlying data changes.
Room
Encrypted collaborative space for multiple identities.
new Room(store, spec, identity, opts)
store- Corestore instancespec- Room spec (spec.room)identity- Identity instanceopts.key- Room key to join existing roomopts.encryptionKey- Room encryption key (auto-generated for new rooms)opts.local- Local instance for persistenceopts.replicate- Enable replication (default: true)opts.bootstrap- DHT bootstrap nodes
import Corestore from 'corestore'
import { Identity, Room } from 'cero'
const identityStore = new Corestore('./identity', { manifestVersion: 2 })
const identity = new Identity(identityStore, spec.identity)
await identity.ready()
await identity.profile.set({ name: 'Alice' })
const roomStore = new Corestore('./room', { manifestVersion: 2 })
const room = new Room(roomStore, spec.room, identity)
await room.ready()Properties
room.key
Room's autobase key buffer.
room.encryptionKey
Room's 32-byte encryption key buffer.
room.discoveryKey
Room's discovery key for DHT.
room.writable
Whether current device can write to room.
room.identity
The identity instance associated with this room.
room.refs
Reference count for room lifecycle management. Incremented by rooms.open(), decremented by rooms.release().
const room = await cero.rooms.open(roomId)
console.log(room.refs) // 1
await cero.rooms.open(roomId)
console.log(room.refs) // 2
cero.rooms.release(room)
console.log(room.refs) // 1Methods
await room.ready()
Wait for room to be ready.
await room.close()
Close room and clean up resources.
await room.leave()
Leave the room (remove self as member).
await room.leave()room.profile
await room.profile.get()
Get room profile.
const profile = await room.profile.get()
// { name: 'My Room', description: 'A collaborative space', meta: null, avatar: null }await room.profile.set(updates)
Update room profile fields.
await room.profile.set({
name: 'My Room',
description: 'A collaborative space',
meta: { category: 'work' },
avatar: avatarBuffer
})room.profile.subscribe()
Subscribe to room profile updates.
for await (const profile of room.profile.subscribe()) {
console.log('Room profile updated:', profile)
}room.members
await room.members.list(query?)
List all room members.
const members = await room.members.list()
// [{ id, key, name, alias, avatar, role, meta, attestation, createdAt }, ...]await room.members.get(id)
Get a member by ID.
const member = await room.members.get(memberId)
// { id, key, name, alias, avatar, role, meta, attestation, createdAt }await room.members.delete(id)
Remove a member from the room.
await room.members.delete(memberId)await room.members.count()
Get the number of members.
const count = await room.members.count()Auto-sync from identity
When you update your identity profile, all rooms automatically sync the changes:
await identity.profile.set({ name: 'Alice Smith' })
// Other room members will see the updated nameroom.members.subscribe(id?)
Subscribe to member updates.
// All members
for await (const members of room.members.subscribe()) {
console.log('Members:', members.length)
}
// Single member
for await (const member of room.members.subscribe(memberId)) {
console.log('Member updated:', member)
}room.devices
await room.devices.list(query?)
List all devices in the room.
const devices = await room.devices.list()
// [{ id, key, memberId, memberKey, role, isCurrent, attestation, createdAt }, ...]isCurrent-trueif this is the device running the current instance
await room.devices.get(id)
Get a device by ID.
const device = await room.devices.get(deviceId)
// { id, key, memberId, memberKey, role, isCurrent, attestation, createdAt }await room.devices.count()
Get the number of devices.
const count = await room.devices.count()room.devices.subscribe(id?)
Subscribe to device updates.
for await (const devices of room.devices.subscribe()) {
console.log('Devices:', devices.length)
}room.invites
await room.invites.create(opts?)
Create an invite for a new member to join the room. Returns { id, invite, expires }.
const { invite } = await room.invites.create({
role: 'admin', // optional, default: 'member'
expires: Date.now() + 3600000 // optional, 1 hour
})
// invite is a z32-encoded stringawait room.invites.get(id)
Get invite by ID.
const invite = await room.invites.get(inviteId)
// { id, invite, publicKey, role, expires, createdBy, createdAt }await room.invites.list()
List all room invites.
const invites = await room.invites.list()
// [{ id, invite, publicKey, role, expires, createdBy, createdAt }, ...]await room.invites.delete(id)
Delete a room invite.
await room.invites.delete(inviteId)await room.invites.count()
Get the number of invites.
const count = await room.invites.count()room.invites.subscribe(id?)
Subscribe to invite updates.
for await (const invites of room.invites.subscribe()) {
console.log('Invites:', invites.length)
}room.files
await room.files.save(input, opts?)
Save a file to the room's blob storage. Accepts a buffer, file path, or array of [data, opts] tuples for batch saves.
// From buffer
const file = await room.files.save(buffer, { name: 'photo.png', mimetype: 'image/png' })
// From file path
const file = await room.files.save('/path/to/file.pdf')
// Batch save
const files = await room.files.save([
[buffer1, { name: 'a.png' }],
[buffer2, { name: 'b.png' }]
])Returns a file object with { id, key, name, mimetype, blob, size }.
await room.files.get(file)
Get file bytes from blob storage.
const bytes = await room.files.get(file)await room.files.list()
List all files stored locally for this room.
const files = await room.files.list()await room.files.clear(file)
Clear a file from local storage (frees disk space).
await room.files.clear(file)await room.files.clearAll(opts?)
Clear all files for this room. Returns bytes cleared.
const bytesCleared = await room.files.clearAll()
// When leaving a room, delete records instead of marking cleared
await room.files.clearAll({ leaving: true })room.settings
Key-value settings synced across all room members.
await room.settings.get(key)
Get a room setting value by key.
const theme = await room.settings.get('theme')await room.settings.set(key, value)
Set a room setting value.
await room.settings.set('notifications', true)
await room.settings.set('config', { autoSync: true })await room.settings.list()
List all room settings.
await room.settings.delete(key)
Delete a room setting.
room.settings.subscribe(key?)
Subscribe to room setting updates.
Counters
Generic counter methods for custom application use.
await room.getCounter(name)
Get a counter value by name. Returns 0 if not set.
const count = await room.getCounter('messages')await room.setCounter(name, value)
Set a counter value.
await room.setCounter('messages', 10)await room.incCounter(name)
Increment a counter by 1. Returns the new value.
const count = await room.incCounter('messages')await room.decCounter(name)
Decrement a counter by 1 (floors at 0). Returns the new value.
const count = await room.decCounter('messages')Static Methods
Room.pair(store, spec, identity, invite, opts)
Join a room using an invite.
const roomStore = new Corestore('./room', { manifestVersion: 2 })
const pairer = Room.pair(roomStore, spec.room, identity, invite, {
local: local,
bootstrap: bootstrapNodes
})
const room = await pairer.resolve()Events
room.on('update', () => {})
Emitted when the underlying data changes.
Local
Local-only storage for sensitive data that should not be replicated.
new Local(base, spec, opts)
base- Passnullfor standalonespec- Local spec (spec.local)opts.store- Corestore instance
import Corestore from 'corestore'
import { Local } from 'cero'
const store = new Corestore('./local', { manifestVersion: 2 })
const local = new Local(null, spec.local, { store })
await local.ready()Methods
await local.insert(collection, data)
Insert or update a record.
await local.insert('@local/identity', { key, entropy, device })await local.get(collection, query)
Get a single record.
const record = await local.get('@local/identity', {})await local.find(collection, query)
Find multiple records. Returns an async iterator.
const rooms = await local.find('@local/rooms', {}).toArray()await local.delete(collection, query)
Delete a record.
await local.delete('@local/rooms', { key: roomKey })await local.flush()
Flush pending writes to disk.
await local.flush()Counters
await local.getCounter(name)
Get a counter value. Returns 0 if not set.
const count = await local.getCounter('rooms')await local.setCounter(name, value)
Set a counter value.
await local.setCounter('rooms', 5)await local.incCounter(name)
Increment a counter by 1. Returns new value.
const count = await local.incCounter('rooms')await local.decCounter(name)
Decrement a counter by 1 (floors at 0). Returns new value.
const count = await local.decCounter('rooms')Pairing
Identity Pairing (Multi-Device)
Pair a new device to an existing identity:
import Corestore from 'corestore'
import { Identity } from 'cero'
// On existing device, create invite
const { invite } = await identity.invites.create()
// On new device, pair using invite and seed
const store = new Corestore('./new-device', { manifestVersion: 2 })
const pairer = Identity.pair(store, spec.identity, invite, {
seed: 'your 12 word seed phrase...'
})
const identity = await pairer.resolve()Room Pairing (Join Room)
Join a room via invite:
import Corestore from 'corestore'
import { Identity, Room } from 'cero'
// Create identity first
const identityStore = new Corestore('./identity', { manifestVersion: 2 })
const identity = new Identity(identityStore, spec.identity)
await identity.ready()
// Join room
const roomStore = new Corestore('./room', { manifestVersion: 2 })
const pairer = Room.pair(roomStore, spec.room, identity, invite)
const room = await pairer.resolve()Cancellation and Timeout
Use AbortController to cancel pairing or set a timeout:
import { CeroError } from 'cero'
// With timeout
try {
const room = await pairer.resolve({ signal: AbortSignal.timeout(30000) })
} catch (err) {
if (err.code === 'PAIRING_CANCELLED') {
console.log('Pairing timed out')
}
}
// Manual cancellation
const controller = new AbortController()
setTimeout(() => controller.abort(), 30000)
try {
const identity = await pairer.resolve({ signal: controller.signal })
} catch (err) {
if (err.code === 'PAIRING_CANCELLED') {
console.log('Pairing was cancelled')
}
}pairer.cancel()
Cancel an in-progress pairing operation:
const resolvePromise = pairer.resolve()
await pairer.cancel()
// resolvePromise will reject with PAIRING_CANCELLEDFlows
Multi-Device Flow
- Create identity on first device:
const cero = new Cero('./storage', spec)
await cero.ready()
await cero.profile.set({ name: 'Alice' })
// Save seed phrase securely
const seed = cero.seed.get()
cero.seed.clear()- Create invite:
const { invite } = await cero.invites.create()
// Share invite with second device (QR code, etc)- Pair second device:
import Corestore from 'corestore'
import { Identity } from 'cero'
const store = new Corestore('./storage', { manifestVersion: 2 })
const pairer = Identity.pair(store, spec.identity, invite, { seed })
const identity = await pairer.resolve()- Both devices stay in sync:
// On device 1
await identity.profile.set({ name: 'Alice Updated' })
// On device 2, after sync
const profile = await identity2.profile.get()
// { name: 'Alice Updated', ... }Recovery Flow
Recover an identity using only the seed phrase (no invite required). Useful when:
- User lost access to all devices
- User wants to add a new device but has no existing device available to create invite
Requirements: At least one existing device must be online for recovery to succeed.
- On new device, start recovery:
import Corestore from 'corestore'
import { Identity } from 'cero'
const store = new Corestore('./storage', { manifestVersion: 2 })
const recoverer = Identity.recover(store, spec.identity, seed, {
replicate: true
})
recoverer.on('waiting', () => {
console.log('Looking for existing device...')
})- Wait for recovery to complete:
try {
const identity = await recoverer.resolve({
signal: AbortSignal.timeout(60000)
})
console.log('Recovery successful!')
} catch (err) {
if (err.code === 'RECOVERY_CANCELLED') {
console.log('Recovery timed out or was cancelled')
}
}- On existing device, recovery is automatic:
Existing devices automatically monitor for recovery requests and add new devices as writers when valid seed-based attestation is verified.
Room Flow
- Create room:
const cero = new Cero('./storage', spec)
await cero.ready()
const room = await cero.rooms.create()
await room.profile.set({ name: 'Project Alpha' })- Create invite:
const { invite } = await room.invites.create({ role: 'member' })
// Share invite with new member- New member joins:
const joiner = new Cero('./joiner-storage', spec)
await joiner.ready()
await joiner.profile.set({ name: 'Bob' })
const room = await joiner.rooms.pair(invite)- Members collaborate:
// Both members can update room profile
await room.profile.set({ description: 'Updated by Alice' })
await bobRoom.profile.set({ description: 'Updated by Bob' })
// Both see all members
const members = await room.members.list()
// [{ name: 'Alice', role: 'owner' }, { name: 'Bob', role: 'member' }]Events
The Cero instance emits events for reactive state tracking. All event names use domain:past-verb convention.
Identity Events
identity:updated
Fires when identity state changes (profile, devices, rooms list).
cero.on('identity:updated', () => {
const profile = await cero.profile.get()
console.log('Identity updated:', profile.name)
})identity:recovery-requested
Fires when a recovery join request is sent and waiting for approval from an existing device.
cero.on('identity:recovery-requested', ({ device }) => {
console.log('Recovery pending for device:', device.name)
})identity:recovered
Fires when recovery completes and the device becomes a writer.
cero.on('identity:recovered', ({ device }) => {
console.log('Recovery complete for device:', device.name)
})Room Events
rooms:updated
Fires when an unfocused room receives new data in the background.
cero.on('rooms:updated', ({ roomId }) => {
console.log('Room has new data:', roomId)
})rooms:added
Fires when a room is added — created locally, joined via invite, or synced from another device.
cero.on('rooms:added', ({ roomId }) => {
console.log('New room:', roomId)
})rooms:removed
Fires when a room is deleted.
cero.on('rooms:removed', ({ roomId }) => {
console.log('Room removed:', roomId)
})Error Handling
CeroError
Typed errors for consistent error handling:
import { CeroError } from 'cero'
try {
await pairer.resolve({ signal: AbortSignal.timeout(5000) })
} catch (err) {
if (CeroError.isCeroError(err)) {
console.log('Error code:', err.code)
console.log('Details:', err.details)
console.log('Timestamp:', err.timestamp)
}
}Error Codes
| Code | Description |
| -------------------- | ------------------------------------------------ |
| PAIRING_CANCELLED | Pairing was cancelled or aborted |
| PAIRING_TIMEOUT | Pairing timed out |
| RECOVERY_CANCELLED | Recovery was cancelled or aborted |
| RECOVERY_TIMEOUT | Recovery timed out |
| ALREADY_PAIRED | Identity already paired/recovered on this device |
| PERMISSION_DENIED | Operation not permitted |
| NOT_FOUND | Resource not found |
| INVALID_INVITE | Invalid invite format |
| INVITE_EXPIRED | Invite has expired |
| ALREADY_MEMBER | Already a member |
| LAST_MEMBER | Cannot remove last member |
| ATTESTATION_FAILED | Attestation verification failed |
| ABORT | Contract operation aborted |
| CORRUPT | Data corruption detected |
| UNEXPECTED_ERROR | Unexpected error occurred |
Static Factory Methods
throw CeroError.NOT_FOUND('Member not found', { memberId: id })
throw CeroError.PERMISSION_DENIED('Only owners can delete')
throw CeroError.INVITE_EXPIRED('Invite has expired', { inviteId })License
Apache-2.0
