API Reference

July 7, 2026 · View on GitHub

Complete API documentation. For getting started, see README.md.

Session API (Directional Verification)

import {
  createSession,
  generateSeed,
  deriveSeed,
  SESSION_PRESETS,
  type Session,
  type SessionConfig,
  type SessionPresetName,
} from 'canary-kit/session'
FunctionDescription
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:

MethodDescription
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:

PresetWordsRotationToleranceUse case
call130 seconds±1Phone verification (insurance, banking)
handoff1Single-use0Physical handoff (rideshare, delivery)

CANARY Protocol (Universal)

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

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'
FunctionDescription
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

import {
  deriveVerificationWord,
  deriveVerificationPhrase,
  deriveDuressWord,
  deriveDuressPhrase,
} from 'canary-kit'
FunctionSignatureDescription
deriveVerificationWord(seedHex: string, counter: number) => stringDerives 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) => stringDerives 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

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.

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

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

Group Management

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.

FunctionDescription
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:

FieldTypeDescription
namestringGroup name (required)
membersstring[]Nostr pubkeys, 64-char hex (required)
presetPresetNameNamed threat-profile preset (optional)
creatorstringPubkey 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.
rotationIntervalnumberSeconds; overrides preset value
wordCount1 | 2 | 3Words per challenge; overrides preset value
tolerancenumberCounter tolerance for verification: accept tokens within +/-tolerance counter values (default: 1)
beaconIntervalnumberBeacon broadcast interval in seconds (default: 300)
beaconPrecisionnumberGeohash precision for normal beacons 1--11 (default: 6)

GroupState fields:

FieldTypeDescription
namestringGroup name
seedstring64-char hex (256-bit shared secret)
membersstring[]Current member pubkeys
adminsstring[]Pubkeys with admin privileges (reseed, add/remove others)
rotationIntervalnumberSeconds between automatic word rotation
wordCount1 | 2 | 3Words per challenge
counternumberTime-based counter at last sync
usageOffsetnumberBurn-after-use offset on top of counter
tolerancenumberCounter tolerance for verification
epochnumberMonotonic epoch -- increments on reseed (replay protection)
consumedOpsstring[]Consumed operation IDs within current epoch
consumedOpsFloornumber?Timestamp floor for replay protection after consumedOps eviction
createdAtnumberUnix timestamp of group creation
beaconIntervalnumberSeconds between beacon broadcasts
beaconPrecisionnumberGeohash precision (1--11)

Threat-Profile Presets

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

Group presets:

PresetWordsRotationUse case
family17 daysCasual family/friend verification
field-ops224 hoursJournalism, activism, field work
enterprise248 hoursCorporate incident response

Explicit config values always override preset defaults.

Counter

import { getCounter, counterToBytes, DEFAULT_ROTATION_INTERVAL } from 'canary-kit'
ExportDescription
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_INTERVAL604800 — 7 days in seconds

Wordlist

import { WORDLIST, WORDLIST_SIZE, getWord, indexOf } from 'canary-kit'
// or: import { WORDLIST, WORDLIST_SIZE, getWord, indexOf } from 'canary-kit/wordlist'
ExportDescription
WORDLISTreadonly string[] — 2048 words curated for spoken clarity
WORDLIST_SIZE2048
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

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.

BuilderKindDescription
buildGroupStateEvent(params)30078Parameterised replaceable group state with ssg/ d-tag namespace
buildStoredSignalEvent(params)30078Parameterised replaceable stored signal with hashed d-tag and 7-day expiration
buildSignalEvent(params)20078Ephemeral real-time signal (beacon, word-used, counter-advance)
buildRumourEvent(params)14NIP-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

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

Sync Protocol

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 for complete workflow examples.

FunctionSignatureDescription
applySyncMessage(state, msg, nowSec?, sender?) → GroupStateApply a sync message. Returns new state, or the same reference if rejected.
applySyncMessageWithResult(state, msg, nowSec?, sender?) → SyncApplyResultSame as above but returns { state, applied } for observability.
decodeSyncMessage(json: string) → SyncMessageParse and validate a JSON sync message. Throws on invalid input.
encodeSyncMessage(msg: SyncMessage) → stringSerialise 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.
interface SyncApplyResult {
  state: GroupState
  applied: boolean
}
Message typeDescription
member-joinAdd a member (admin-only, or self-join if sender is the pubkey)
member-leaveRemove a member (admin-only) or self-leave
counter-advanceAdvance the group counter (burn-after-use)
reseedDistribute a new seed with epoch bump (admin-only)
beaconEncrypted location heartbeat (fire-and-forget)
duress-alertSilent duress location alert (fire-and-forget)
duress-clearClear a duress alert
liveness-checkinDead man's switch heartbeat (fire-and-forget)
state-snapshotFull state sync for new/rejoining members (admin-only)