Complete API documentation. For getting started, see README.md.
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) |
The universal protocol API works with any transport — not just Nostr groups.
import {
deriveToken, deriveTokenBytes,
deriveDuressToken, deriveDuressTokenBytes,
verifyToken,
deriveLivenessToken,
deriveDirectionalPair,
type TokenVerifyResult, type VerifyOptions,
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 |
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 |
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 |
import { verifyWord, type VerifyResult, type VerifyStatus } from 'canary-kit'verifyWord(spokenWord, seedHex, memberPubkeys, counter, wordCount?): VerifyResult
Checks a spoken word in order: current verification word → each member's duress word → previous window (stale) → failed.
type VerifyStatus = 'verified' | 'duress' | 'stale' | 'failed'
interface VerifyResult {
status: VerifyStatus
members?: string[] // pubkeys of coerced members (only when status === 'duress')
}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) |
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.
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 |
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.
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.
import {
deriveBeaconKey,
encryptBeacon, decryptBeacon,
buildDuressAlert, encryptDuressAlert, decryptDuressAlert,
} from 'canary-kit/beacon'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) |