npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@lekinox/cero

v1.1.0

Published

P2P made easy

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 cero

Quick 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 path
  • spec - Compiled schema spec
  • opts.seed - 12-word seed for restoration
  • opts.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 key
  • cero.publicKey - Identity public key buffer
  • cero.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 device

await 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 device

cero.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 incremented

await 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 decremented

await 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 completes

await 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 ends

await 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 peers

await 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 mode

cero.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 instance
  • spec - Identity spec (spec.identity)
  • opts.seed - Optional 12-word mnemonic seed
  • opts.local - Local instance for persistence
  • opts.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 - true if 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 string

await 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 null

identity.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 access

Identity.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:

  1. Derives discovery key from seed
  2. Finds an existing device via P2P discovery
  3. Requests to join as a new device
  4. 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 instance
  • spec - Room spec (spec.room)
  • identity - Identity instance
  • opts.key - Room key to join existing room
  • opts.encryptionKey - Room encryption key (auto-generated for new rooms)
  • opts.local - Local instance for persistence
  • opts.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) // 1

Methods

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 name

room.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 - true if 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 string

await 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 - Pass null for standalone
  • spec - 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_CANCELLED

Flows

Multi-Device Flow

  1. 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()
  1. Create invite:
const { invite } = await cero.invites.create()
// Share invite with second device (QR code, etc)
  1. 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()
  1. 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.

  1. 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...')
})
  1. 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')
  }
}
  1. 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

  1. Create room:
const cero = new Cero('./storage', spec)
await cero.ready()

const room = await cero.rooms.create()
await room.profile.set({ name: 'Project Alpha' })
  1. Create invite:
const { invite } = await room.invites.create({ role: 'member' })
// Share invite with new member
  1. 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)
  1. 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