# API Reference — canary-kit

> Complete API documentation. For getting started, see [README.md](README.md).

## Session API (Directional Verification)

```typescript
import {
  createSession,
  generateSeed,
  deriveSeed,
  SESSION_PRESETS,
  type Session,
  type SessionConfig,
  type SessionPresetName,
} from 'canary-kit/session'
```

| Function | Description |
|---|---|
| `createSession(config: SessionConfig)` | Create a role-aware verification session |
| `generateSeed()` | Generate a 256-bit cryptographic seed |
| `deriveSeed(masterKey, ...components)` | Derive a seed deterministically from a master key |

**Session interface:**

| Method | Description |
|---|---|
| `session.myToken(nowSec?)` | Token I speak to prove my identity |
| `session.theirToken(nowSec?)` | Token I expect to hear from the other party |
| `session.verify(spoken, nowSec?)` | Verify a spoken word — returns `valid`, `duress`, or `invalid` |
| `session.counter(nowSec?)` | Current counter value (time-based or fixed) |
| `session.pair(nowSec?)` | Both tokens at once, keyed by role name |

**Session presets:**

| Preset | Words | Rotation | Tolerance | Use case |
|--------|-------|----------|-----------|----------|
| `call` | 1 | 30 seconds | ±1 | Phone verification (insurance, banking) |
| `handoff` | 1 | Single-use | 0 | Physical handoff (rideshare, delivery) |

## CANARY Protocol (Universal)

The universal protocol API works with any transport — not just Nostr groups.

```typescript
import {
  deriveToken, deriveTokenBytes,
  deriveDuressToken, deriveDuressTokenBytes,
  verifyToken,
  estimateCanaryVerificationRisk,
  deriveLivenessToken,
  deriveDirectionalPair,
  type TokenVerifyResult, type VerifyOptions,
  type CanaryVerificationRisk,
  type DirectionalPair,
} from 'canary-kit/token'

import {
  encodeAsWords, encodeAsPin, encodeAsHex,
  encodeToken, type TokenEncoding,
} from 'canary-kit/encoding'
```

| Function | Description |
|---|---|
| `deriveToken(secret, context, counter, encoding?)` | Derive an encoded verification token |
| `deriveDuressToken(secret, context, identity, counter, encoding, maxTolerance)` | Derive a duress token for a specific identity |
| `verifyToken(secret, context, counter, input, identities, options?)` | Verify a token — returns `valid`, `duress` (with matching identities), or `invalid` |
| `estimateCanaryVerificationRisk(options?)` | Estimate accepted candidates, output space, online guess probability, and effective bits |
| `deriveLivenessToken(secret, context, identity, counter)` | Derive a liveness heartbeat token for dead man's switch |
| `deriveDirectionalPair(secret, namespace, roles, counter, encoding?)` | Derive two directional tokens from the same secret |

CANARY verification may accept group fallback tokens, per-identity normal tokens,
and per-identity duress tokens across the configured tolerance window. Use
`estimateCanaryVerificationRisk()` when choosing one-word tokens, large rosters,
or non-zero tolerance.

## Core Derivation

```typescript
import {
  deriveVerificationWord,
  deriveVerificationPhrase,
  deriveDuressWord,
  deriveDuressPhrase,
} from 'canary-kit'
```

| Function | Signature | Description |
|---|---|---|
| `deriveVerificationWord` | `(seedHex: string, counter: number) => string` | Derives the single verification word for all group members |
| `deriveVerificationPhrase` | `(seedHex: string, counter: number, wordCount: 1 \| 2 \| 3) => string[]` | Derives a multi-word verification phrase |
| `deriveDuressWord` | `(seedHex: string, memberPubkeyHex: string, counter: number) => string` | Derives a member's duress word |
| `deriveDuressPhrase` | `(seedHex: string, memberPubkeyHex: string, counter: number, wordCount: 1 \| 2 \| 3) => string[]` | Derives a member's multi-word duress phrase |

## Verification

```typescript
import { verifyWord, type VerifyResult, type VerifyStatus } from 'canary-kit'
```

`verifyWord(spokenWord, seedHex, memberPubkeys, counter, wordCount?): VerifyResult`

Checks a spoken word against the group wrapper: current verification word → each
member's duress word → adjacent verification windows (stale) → failed. The
underlying universal verifier also understands per-identity normal tokens for
call/session-style integrations.

```typescript
type VerifyStatus = 'verified' | 'duress' | 'stale' | 'failed'

interface VerifyResult {
  status: VerifyStatus
  members?: string[]  // pubkeys of coerced members (only when status === 'duress')
}
```

## Group Management

```typescript
import {
  createGroup,
  getCurrentWord,
  getCurrentDuressWord,
  advanceCounter,
  reseed,
  addMember,
  removeMember,
  type GroupConfig,
  type GroupState,
} from 'canary-kit'
```

All functions are pure — they return new state without mutating the input.

| Function | Description |
|---|---|
| `createGroup(config: GroupConfig)` | Creates a new group with a cryptographically secure random seed |
| `getCurrentWord(state: GroupState)` | Returns the current verification word or space-joined phrase |
| `getCurrentDuressWord(state: GroupState, memberPubkey: string)` | Returns the current duress word or phrase for a specific member |
| `advanceCounter(state: GroupState)` | Increments the usage offset (burn-after-use rotation) |
| `reseed(state: GroupState)` | Generates a fresh seed and resets the usage offset |
| `addMember(state: GroupState, pubkey: string)` | Adds a member; idempotent if already present |
| `removeMember(state: GroupState, pubkey: string)` | Removes a member (does NOT reseed -- old seed still valid) |
| `removeMemberAndReseed(state: GroupState, pubkey: string)` | Removes a member and immediately reseeds (recommended) |
| `dissolveGroup(state: GroupState)` | Zeroes the seed and clears all members |
| `syncCounter(state: GroupState, nowSec?: number)` | Refreshes counter to current time window (monotonic, never regresses) |

**GroupConfig fields:**

| Field | Type | Description |
|---|---|---|
| `name` | `string` | Group name (required) |
| `members` | `string[]` | Nostr pubkeys, 64-char hex (required) |
| `preset` | `PresetName` | Named threat-profile preset (optional) |
| `creator` | `string` | Pubkey of the group creator -- only the creator is admin at bootstrap. Must be in `members`. Without a creator, `admins` is empty and all privileged sync operations are silently rejected. |
| `rotationInterval` | `number` | Seconds; overrides preset value |
| `wordCount` | `1 \| 2 \| 3` | Words per challenge; overrides preset value |
| `tolerance` | `number` | Counter tolerance for verification: accept tokens within +/-tolerance counter values (default: 1) |
| `beaconInterval` | `number` | Beacon broadcast interval in seconds (default: 300) |
| `beaconPrecision` | `number` | Geohash precision for normal beacons 1--11 (default: 6) |

**GroupState fields:**

| Field | Type | Description |
|---|---|---|
| `name` | `string` | Group name |
| `seed` | `string` | 64-char hex (256-bit shared secret) |
| `members` | `string[]` | Current member pubkeys |
| `admins` | `string[]` | Pubkeys with admin privileges (reseed, add/remove others) |
| `rotationInterval` | `number` | Seconds between automatic word rotation |
| `wordCount` | `1 \| 2 \| 3` | Words per challenge |
| `counter` | `number` | Time-based counter at last sync |
| `usageOffset` | `number` | Burn-after-use offset on top of counter |
| `tolerance` | `number` | Counter tolerance for verification |
| `epoch` | `number` | Monotonic epoch -- increments on reseed (replay protection) |
| `consumedOps` | `string[]` | Consumed operation IDs within current epoch |
| `consumedOpsFloor` | `number?` | Timestamp floor for replay protection after consumedOps eviction |
| `createdAt` | `number` | Unix timestamp of group creation |
| `beaconInterval` | `number` | Seconds between beacon broadcasts |
| `beaconPrecision` | `number` | Geohash precision (1--11) |

## Threat-Profile Presets

```typescript
import { createGroup, PRESETS, type PresetName } from 'canary-kit'
```

**Group presets:**

| Preset | Words | Rotation | Use case |
|--------|-------|----------|----------|
| `family` | 1 | 7 days | Casual family/friend verification |
| `field-ops` | 2 | 24 hours | Journalism, activism, field work |
| `enterprise` | 2 | 48 hours | Corporate incident response |

Explicit config values always override preset defaults.

## Counter

```typescript
import { getCounter, counterToBytes, DEFAULT_ROTATION_INTERVAL } from 'canary-kit'
```

| Export | Description |
|---|---|
| `getCounter(timestampSec, rotationIntervalSec?)` | Returns `floor(timestamp / interval)` — the current time window |
| `counterToBytes(counter)` | Serialises a counter to an 8-byte big-endian `Uint8Array` (RFC 6238 encoding) |
| `DEFAULT_ROTATION_INTERVAL` | `604800` — 7 days in seconds |

## Wordlist

```typescript
import { WORDLIST, WORDLIST_SIZE, getWord, indexOf } from 'canary-kit'
// or: import { WORDLIST, WORDLIST_SIZE, getWord, indexOf } from 'canary-kit/wordlist'
```

| Export | Description |
|---|---|
| `WORDLIST` | `readonly string[]` — 2048 words curated for spoken clarity |
| `WORDLIST_SIZE` | `2048` |
| `getWord(index: number)` | Returns the word at the given index |
| `indexOf(word: string)` | Returns the index of a word, or `-1` if not found |

The wordlist (`en-v1`) is derived from BIP-39 English, filtered for verbal verification: no homophones, no phonetic near-collisions, no emotionally charged words. All words are 3–8 characters, lowercase alphabetic only.

## Nostr Events

```typescript
import {
  buildGroupStateEvent,
  buildStoredSignalEvent,
  buildSignalEvent,
  buildRumourEvent,
  hashGroupId,
  KINDS,
  type UnsignedEvent,
} from 'canary-kit/nostr'
```

All builders return an `UnsignedEvent`. Sign with your own Nostr library. Uses standard Nostr kinds — no custom event kinds.

| Builder | Kind | Description |
|---|---|---|
| `buildGroupStateEvent(params)` | `30078` | Parameterised replaceable group state with `ssg/` d-tag namespace |
| `buildStoredSignalEvent(params)` | `30078` | Parameterised replaceable stored signal with hashed d-tag and 7-day expiration |
| `buildSignalEvent(params)` | `20078` | Ephemeral real-time signal (beacon, word-used, counter-advance) |
| `buildRumourEvent(params)` | `14` | NIP-17 rumour for seed distribution, reseed, and member updates (consumer wraps in kind 1059) |

`KINDS` exports `{ groupState: 30078, signal: 20078, giftWrap: 1059 }`. `hashGroupId(groupId)` returns a SHA-256 hash for privacy-preserving d-tags.

## Beacon & Duress Alerts

```typescript
import {
  deriveBeaconKey,
  encryptBeacon, decryptBeacon,
  buildDuressAlert, encryptDuressAlert, decryptDuressAlert,
} from 'canary-kit/beacon'
```

## Sync Protocol

```typescript
import {
  applySyncMessage,
  applySyncMessageWithResult,
  decodeSyncMessage,
  encodeSyncMessage,
  deriveGroupKey,
  deriveGroupIdentity,
  hashGroupTag,
  encryptEnvelope,
  decryptEnvelope,
  PROTOCOL_VERSION,
  type SyncMessage,
  type SyncApplyResult,
  type SyncTransport,
  type EventSigner,
} from 'canary-kit/sync'
```

Transport-agnostic state synchronisation for group membership, counter advancement, reseeds, beacons, and duress alerts. Messages are validated against an authority model with 6 invariants (admin checks, epoch ordering, replay protection, counter bounds). See [COOKBOOK.md](COOKBOOK.md) for complete workflow examples.

| Function | Signature | Description |
|---|---|---|
| `applySyncMessage` | `(state, msg, nowSec?, sender?) → GroupState` | Apply a sync message. Returns new state, or the same reference if rejected. |
| `applySyncMessageWithResult` | `(state, msg, nowSec?, sender?) → SyncApplyResult` | Same as above but returns `{ state, applied }` for observability. |
| `decodeSyncMessage` | `(json: string) → SyncMessage` | Parse and validate a JSON sync message. Throws on invalid input. |
| `encodeSyncMessage` | `(msg: SyncMessage) → string` | Serialise a sync message to JSON (injects `protocolVersion`). |

**Important:** `applySyncMessage` silently returns unchanged state when a message is rejected (wrong epoch, replay, missing sender, etc.). Use `applySyncMessageWithResult` when you need to distinguish accepted from rejected messages for logging or alerting.

**Sender requirements:**
- Privileged actions (member-join of others, member-leave of others, reseed, state-snapshot) require `sender` to be in `group.admins`.
- `counter-advance` requires `sender` to be in `group.members`.
- Omitting `sender` for these operations causes silent rejection.

```typescript
interface SyncApplyResult {
  state: GroupState
  applied: boolean
}
```

| Message type | Description |
|---|---|
| `member-join` | Add a member (admin-only, or self-join if sender is the pubkey) |
| `member-leave` | Remove a member (admin-only) or self-leave |
| `counter-advance` | Advance the group counter (burn-after-use) |
| `reseed` | Distribute a new seed with epoch bump (admin-only) |
| `beacon` | Encrypted location heartbeat (fire-and-forget) |
| `duress-alert` | Silent duress location alert (fire-and-forget) |
| `duress-clear` | Clear a duress alert |
| `liveness-checkin` | Dead man's switch heartbeat (fire-and-forget) |
| `state-snapshot` | Full state sync for new/rejoining members (admin-only) |
