npm package: @lekinox/cero
Description: P2P made easy
Author: Lekinox
Latest version: 1.1.0

# cero

Simple p2p lib.

## Contents

- [Builder](#builder) - Spec building and extension
- [Cero](#cero) - Main entry point
- [Identity](#identity) - Cryptographic identity with multi-device support
- [Room](#room) - Encrypted collaborative spaces
- [Local](#local) - Local-only storage
- [Pairing](#pairing) - Device and room pairing
- [Flows](#flows) - Common usage patterns
- [Events](#events) - Reactive state tracking
- [Errors](#error-handling) - Error codes

## Install

```
npm install cero
```

## Quick Start

```js
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](https://github.com/holepunchto/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.

```js
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.

```js
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

```js
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

```js
const seed = cero.seed.get() // Get 12-word seed
cero.seed.clear() // Clear from memory
cero.seed.validate(seed) // { valid: true }
```

### cero.profile

```js
await cero.profile.get() // { name, alias, meta, avatar }
await cero.profile.set({ name: 'Alice' })
```

#### `cero.profile.subscribe()`

Subscribe to profile updates.

```js
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.

```js
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 }`.

```js
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.

```js
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.

```js
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.

```js
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`.

```js
const theme = await cero.settings.get('theme')
// 'dark'
```

#### `await cero.settings.set(key, value)`

Set a setting value.

```js
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.

```js
await cero.settings.delete('theme')
```

#### `await cero.settings.list()`

List all settings.

```js
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`.

```js
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`.

```js
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`.

```js
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.

```js
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.

```js
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.

```js
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).

```js
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.

```js
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.

```js
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.

```js
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.

```js
await cero.rooms.deactivate(roomId)
// room continues syncing in background mode
```

#### `cero.rooms.isActive(id)`

Check if a room has full sync enabled.

```js
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.

```js
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

```js
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.

```js
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`.

```js
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.

```js
await identity.profile.set({
  name: 'Alice',
  alias: 'alice',
  meta: { bio: 'Hello' },
  avatar: avatarBuffer
})
```

#### `identity.profile.subscribe()`

Subscribe to profile updates.

```js
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.

```js
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.

```js
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.

```js
await identity.devices.set(deviceId, { name: 'Work Laptop' })
```

#### `await identity.devices.delete(id)`

Remove a device from the identity.

```js
await identity.devices.delete(deviceId)
```

#### `await identity.devices.count()`

Get the number of devices.

```js
const count = await identity.devices.count()
```

#### `identity.devices.subscribe(id?)`

Subscribe to device updates. Pass ID to watch single device, omit for all.

```js
// 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 }`.

```js
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.

```js
const invite = await identity.invites.get(inviteId)
// { id, publicKey, role, expires, createdBy, createdAt }
```

#### `await identity.invites.list()`

List all invites.

```js
const invites = await identity.invites.list()
// [{ id, publicKey, role, expires, createdBy, createdAt }, ...]
```

#### `await identity.invites.delete(id)`

Delete an invite.

```js
await identity.invites.delete(inviteId)
```

#### `await identity.invites.count()`

Get the number of invites.

```js
const count = await identity.invites.count()
```

#### `identity.invites.subscribe(id?)`

Subscribe to invite updates.

```js
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.

```js
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.

```js
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`.

```js
const theme = await identity.settings.get('theme')
// 'dark'
```

#### `await identity.settings.set(key, value)`

Set a setting value. Accepts any JSON-serializable value.

```js
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.

```js
const settings = await identity.settings.list()
// [{ key: 'theme', value: 'dark' }, { key: 'fontSize', value: 16 }, ...]
```

#### `await identity.settings.delete(key)`

Delete a setting.

```js
await identity.settings.delete('theme')
```

#### `identity.settings.subscribe(key?)`

Subscribe to setting updates. Pass key to watch single setting, omit for all.

```js
// 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.

```js
const count = await identity.getCounter('messages')
```

#### `await identity.setCounter(name, value)`

Set a counter value.

```js
await identity.setCounter('messages', 10)
```

#### `await identity.incCounter(name)`

Increment a counter by 1. Returns the new value.

```js
const count = await identity.incCounter('messages')
```

#### `await identity.decCounter(name)`

Decrement a counter by 1 (floors at 0). Returns the new value.

```js
const count = await identity.decCounter('messages')
```

### identity.attestation

#### `await identity.attestation.create(data)`

Create an attestation proof for data. Accepts buffers or JSON objects.

```js
const { proof, receipt } = await identity.attestation.create(deviceKey)
```

#### `identity.attestation.verify(proof, data, opts?)`

Verify an attestation proof.

```js
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.

```js
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.

```js
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.

```js
const encryptionKey = identity.getEncryptionKey(identity.publicKey)
const roomKey = identity.getEncryptionKey(roomPublicKey)
```

### Static Methods

#### `Identity.getSeedPhrase()`

Generate a new 12-word BIP39 seed phrase.

```js
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 }`.

```js
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:

```js
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):

```js
// 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

```js
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:

```js
recoverer.on('waiting', () => {
  console.log('Looking for existing device...')
})
```

Cancel recovery if needed:

```js
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

```js
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()`.

```js
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).

```js
await room.leave()
```

### room.profile

#### `await room.profile.get()`

Get room profile.

```js
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.

```js
await room.profile.set({
  name: 'My Room',
  description: 'A collaborative space',
  meta: { category: 'work' },
  avatar: avatarBuffer
})
```

#### `room.profile.subscribe()`

Subscribe to room profile updates.

```js
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.

```js
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.

```js
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.

```js
await room.members.delete(memberId)
```

#### `await room.members.count()`

Get the number of members.

```js
const count = await room.members.count()
```

#### Auto-sync from identity

When you update your identity profile, all rooms automatically sync the changes:

```js
await identity.profile.set({ name: 'Alice Smith' })
// Other room members will see the updated name
```

#### `room.members.subscribe(id?)`

Subscribe to member updates.

```js
// 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.

```js
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.

```js
const device = await room.devices.get(deviceId)
// { id, key, memberId, memberKey, role, isCurrent, attestation, createdAt }
```

#### `await room.devices.count()`

Get the number of devices.

```js
const count = await room.devices.count()
```

#### `room.devices.subscribe(id?)`

Subscribe to device updates.

```js
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 }`.

```js
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.

```js
const invite = await room.invites.get(inviteId)
// { id, invite, publicKey, role, expires, createdBy, createdAt }
```

#### `await room.invites.list()`

List all room invites.

```js
const invites = await room.invites.list()
// [{ id, invite, publicKey, role, expires, createdBy, createdAt }, ...]
```

#### `await room.invites.delete(id)`

Delete a room invite.

```js
await room.invites.delete(inviteId)
```

#### `await room.invites.count()`

Get the number of invites.

```js
const count = await room.invites.count()
```

#### `room.invites.subscribe(id?)`

Subscribe to invite updates.

```js
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.

```js
// 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.

```js
const bytes = await room.files.get(file)
```

#### `await room.files.list()`

List all files stored locally for this room.

```js
const files = await room.files.list()
```

#### `await room.files.clear(file)`

Clear a file from local storage (frees disk space).

```js
await room.files.clear(file)
```

#### `await room.files.clearAll(opts?)`

Clear all files for this room. Returns bytes cleared.

```js
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.

```js
const theme = await room.settings.get('theme')
```

#### `await room.settings.set(key, value)`

Set a room setting value.

```js
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.

```js
const count = await room.getCounter('messages')
```

#### `await room.setCounter(name, value)`

Set a counter value.

```js
await room.setCounter('messages', 10)
```

#### `await room.incCounter(name)`

Increment a counter by 1. Returns the new value.

```js
const count = await room.incCounter('messages')
```

#### `await room.decCounter(name)`

Decrement a counter by 1 (floors at 0). Returns the new value.

```js
const count = await room.decCounter('messages')
```

### Static Methods

#### `Room.pair(store, spec, identity, invite, opts)`

Join a room using an invite.

```js
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

```js
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.

```js
await local.insert('@local/identity', { key, entropy, device })
```

#### `await local.get(collection, query)`

Get a single record.

```js
const record = await local.get('@local/identity', {})
```

#### `await local.find(collection, query)`

Find multiple records. Returns an async iterator.

```js
const rooms = await local.find('@local/rooms', {}).toArray()
```

#### `await local.delete(collection, query)`

Delete a record.

```js
await local.delete('@local/rooms', { key: roomKey })
```

#### `await local.flush()`

Flush pending writes to disk.

```js
await local.flush()
```

### Counters

#### `await local.getCounter(name)`

Get a counter value. Returns 0 if not set.

```js
const count = await local.getCounter('rooms')
```

#### `await local.setCounter(name, value)`

Set a counter value.

```js
await local.setCounter('rooms', 5)
```

#### `await local.incCounter(name)`

Increment a counter by 1. Returns new value.

```js
const count = await local.incCounter('rooms')
```

#### `await local.decCounter(name)`

Decrement a counter by 1 (floors at 0). Returns new value.

```js
const count = await local.decCounter('rooms')
```

---

## Pairing

### Identity Pairing (Multi-Device)

Pair a new device to an existing identity:

```js
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:

```js
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:

```js
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:

```js
const resolvePromise = pairer.resolve()
await pairer.cancel()
// resolvePromise will reject with PAIRING_CANCELLED
```

---

## Flows

### Multi-Device Flow

1. **Create identity on first device:**

```js
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()
```

2. **Create invite:**

```js
const { invite } = await cero.invites.create()
// Share invite with second device (QR code, etc)
```

3. **Pair second device:**

```js
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()
```

4. **Both devices stay in sync:**

```js
// 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:**

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

2. **Wait for recovery to complete:**

```js
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')
  }
}
```

3. **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:**

```js
const cero = new Cero('./storage', spec)
await cero.ready()

const room = await cero.rooms.create()
await room.profile.set({ name: 'Project Alpha' })
```

2. **Create invite:**

```js
const { invite } = await room.invites.create({ role: 'member' })
// Share invite with new member
```

3. **New member joins:**

```js
const joiner = new Cero('./joiner-storage', spec)
await joiner.ready()
await joiner.profile.set({ name: 'Bob' })

const room = await joiner.rooms.pair(invite)
```

4. **Members collaborate:**

```js
// 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).

```js
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.

```js
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.

```js
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.

```js
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.

```js
cero.on('rooms:added', ({ roomId }) => {
  console.log('New room:', roomId)
})
```

#### `rooms:removed`

Fires when a room is deleted.

```js
cero.on('rooms:removed', ({ roomId }) => {
  console.log('Room removed:', roomId)
})
```

---

## Error Handling

### CeroError

Typed errors for consistent error handling:

```js
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

```js
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