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'
| 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.
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
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
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.
| 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
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
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
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
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
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.
| 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
senderto be ingroup.admins. counter-advancerequiressenderto be ingroup.members.- Omitting
senderfor these operations causes silent rejection.
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) |