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 and immediately reseeds |
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,
decodeSyncMessage,
encodeSyncMessage,
deriveGroupKey,
deriveGroupSigningKey,
hashGroupTag,
encryptEnvelope,
decryptEnvelope,
type SyncMessage,
type SyncResult,
} 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).
| Message type | Description |
|---|---|
member-join |
Add a member (admin-only) |
member-leave |
Remove a member or self-leave |
counter-advance |
Advance the group counter (burn-after-use) |
reseed |
Distribute a new seed with epoch bump |
beacon |
Encrypted location heartbeat |
duress-alert |
Silent duress location alert |
liveness-checkin |
Dead man's switch heartbeat |
state-snapshot |
Full state sync for new/rejoining members |