# canary-kit > Spoken secrets. Silent alerts. Dead man's switch. canary-kit is a minimal-dependency TypeScript library for deepfake-proof identity verification — spoken-word tokens derived from shared secrets, with built-in duress signalling, liveness heartbeats, and encrypted location beacons. Open protocol. Interactive demo: https://canary.trotters.cc/ GitHub: https://github.com/forgesworn/canary-kit ESM-only. Eight subpath exports: `canary-kit` (main), `canary-kit/token`, `canary-kit/encoding`, `canary-kit/session`, `canary-kit/wordlist`, `canary-kit/nostr`, `canary-kit/beacon`, `canary-kit/sync`. ## Install npm install canary-kit ## Subpath Exports ### canary-kit (main entry) Re-exports the full API. Includes group lifecycle, presets, and all protocol primitives. Core derivation: - deriveVerificationWord(seedHex, counter) → string - deriveVerificationPhrase(seedHex, counter, wordCount) → string[] - deriveDuressWord(seedHex, memberPubkeyHex, counter) → string - deriveDuressPhrase(seedHex, memberPubkeyHex, counter, wordCount) → string[] - verifyWord(spokenWord, seedHex, memberPubkeys, counter, wordCount?) → VerifyResult Group lifecycle: - createGroup(config) → GroupState (config.creator sets initial admin; without creator, admins is empty) - getCurrentWord(state) → string - getCurrentDuressWord(state, memberPubkey) → string - advanceCounter(state) → GroupState - reseed(state) → GroupState - addMember(state, pubkey) → GroupState - removeMember(state, pubkey) → GroupState (does NOT reseed) - removeMemberAndReseed(state, pubkey) → GroupState (recommended: removes + reseeds atomically) - dissolveGroup(state) → GroupState (zeroes seed, clears members) - syncCounter(state, nowSec?) → GroupState (refreshes counter to current time window, monotonic) Counter: - getCounter(timestampSec, rotationIntervalSec?) → number - counterToBytes(counter) → Uint8Array - DEFAULT_ROTATION_INTERVAL — 604800 (7 days) Presets: - PRESETS — { family, field-ops, enterprise } (wordCount, rotationInterval, description) - PresetName — 'family' | 'field-ops' | 'enterprise' Also re-exports all of: canary-kit/token, canary-kit/encoding, canary-kit/beacon, canary-kit/wordlist ### canary-kit/token Universal CANARY protocol — context-string-based derivation. Use this to build custom integrations outside the group model. - deriveTokenBytes(secret, context, counter) → Uint8Array - deriveToken(secret, context, counter, encoding?) → string - deriveDuressTokenBytes(secret, context, identity, counter) → Uint8Array - MAX_TOLERANCE — 10 (upper bound for tolerance/maxTolerance) - deriveDuressToken(secret, context, identity, counter, encoding, maxTolerance) → string (both required — maxTolerance must match verifier's tolerance) - verifyToken(secret, context, counter, input, identities, options?) → TokenVerifyResult - deriveLivenessToken(secret, context, identity, counter) → Uint8Array - deriveDirectionalPair(secret, namespace, roles, counter, encoding?) → { [role]: string } Types: TokenVerifyResult ({ status: 'valid' | 'duress' | 'invalid', identities? }), VerifyOptions ({ encoding?, tolerance? }) ### canary-kit/encoding Encode raw HMAC bytes into human-readable formats. - encodeAsWords(bytes, count?, wordlist?) → string[] - encodeAsPin(bytes, digits?) → string - encodeAsHex(bytes, length?) → string - encodeToken(bytes, encoding?) → string - DEFAULT_ENCODING — { format: 'words', count: 1 } Type: TokenEncoding — { format: 'words', count?, wordlist? } | { format: 'pin', digits? } | { format: 'hex', length? } ### canary-kit/session Role-aware, time-managed two-party verification sessions. Ideal for phone calls and physical handoffs. - createSession(config) → Session - generateSeed() → Uint8Array - deriveSeed(masterKey, ...components) → Uint8Array - SESSION_PRESETS — { call, handoff } (wordCount, rotationSeconds, tolerance, directional) - SessionPresetName — 'call' | 'handoff' Session methods: - session.counter(nowSec?) → number - session.myToken(nowSec?) → string - session.theirToken(nowSec?) → string - session.verify(spoken, nowSec?) → TokenVerifyResult - session.pair(nowSec?) → { [role]: string } ### canary-kit/wordlist 2048-word spoken-clarity wordlist (en-v1). Based on BIP-39 English, filtered for verbal clarity — no homophones, no phonetic near-collisions. - WORDLIST — readonly string[] (2048 entries) - WORDLIST_SIZE — 2048 - getWord(index) → string - indexOf(word) → number (-1 if not found) ### canary-kit/nostr Unsigned Nostr event builders for SSG (Simple Shared Secret) groups. Uses standard Nostr kinds — no custom event kinds. Sign events with your preferred Nostr library. - KINDS — { groupState: 30078, signal: 20078, giftWrap: 1059 } - buildGroupStateEvent(params) → UnsignedEvent — kind 30078, d-tag `ssg/`, p-tags for members, NIP-32 labels - buildStoredSignalEvent(params) → UnsignedEvent — kind 30078, d-tag `ssg/:`, 7-day expiration - buildSignalEvent(params) → UnsignedEvent — kind 20078 ephemeral, d-tag `ssg/`, t-tag for signal type - buildRumourEvent(params) → UnsignedEvent — kind 14 rumour for NIP-17 gift wrapping (seed distribution, reseed, member updates) - hashGroupId(groupId) → string — SHA-256 hash a group ID for privacy-preserving d-tags ### canary-kit/beacon AES-256-GCM encrypted location beacons and duress alerts. Beacons are published as kind 20078 ephemeral signal events. - deriveBeaconKey(seedHex) → Uint8Array - encryptBeacon(key, geohash, precision) → Promise\ - decryptBeacon(key, content) → Promise\ - buildDuressAlert(memberPubkey, location) → DuressAlert - encryptDuressAlert(key, alert) → Promise\ - decryptDuressAlert(key, content) → Promise\ Types: BeaconPayload ({ geohash, precision, timestamp }), DuressAlert ({ type, member, geohash, precision, locationSource, timestamp }), DuressLocation ({ geohash, precision, locationSource }) ### canary-kit/sync Transport-agnostic group state synchronisation. Authority model with 6 invariants, replay protection, epoch boundaries. See COOKBOOK.md for complete workflow examples. - decodeSyncMessage(json) → SyncMessage (validates and parses; throws on invalid input) - encodeSyncMessage(msg) → string (serialises to JSON; injects protocolVersion) - applySyncMessage(state, msg, nowSec?, sender?) → GroupState (apply a sync message; returns same reference if rejected) - applySyncMessageWithResult(state, msg, nowSec?, sender?) → SyncApplyResult ({ state, applied }) (same as above but indicates acceptance) - deriveGroupKey(seed) → Uint8Array (AES-256-GCM group key) - deriveGroupIdentity(seed) → Uint8Array (group identity key) - hashGroupTag(groupId) → string (privacy-preserving group tag) - encryptEnvelope(key, plaintext) → Promise (AES-256-GCM) - decryptEnvelope(key, ciphertext) → Promise (AES-256-GCM) IMPORTANT: applySyncMessage silently returns unchanged state when rejected. Privileged actions (member-join of others, reseed, state-snapshot) require sender in group.admins. counter-advance requires sender in group.members. Omitting sender causes silent rejection. Set creator in createGroup() to populate admins. Message types: member-join, member-leave, counter-advance, reseed, beacon, duress-alert, duress-clear, liveness-checkin, state-snapshot Types: SyncApplyResult ({ state: GroupState, applied: boolean }), SyncTransport (interface for pluggable transports), EventSigner (interface for Nostr signing) ## Quick Examples ```typescript // --- Session-based phone verification (bank/insurance call centre) --- import { createSession } from 'canary-kit/session' const session = createSession({ secret: sharedSecretHex, namespace: 'aviva', roles: ['caller', 'agent'], myRole: 'agent', preset: 'call', // 30-second rotation, tolerance 1, directional theirIdentity: customerId, // enables duress detection }) // The caller speaks session.theirToken() — the agent hears and verifies: const result = session.verify(spokenWord) // result.status === 'valid' | 'duress' | 'invalid' // result.identities → who signalled duress (if any) ``` ```typescript // --- Group-based family verification (weekly rotating passphrase) --- import { createGroup, getCurrentWord, getCurrentDuressWord, verifyWord, getCounter } from 'canary-kit' const group = createGroup({ preset: 'family', members: [alicePubkey, bobPubkey] }) const word = getCurrentWord(group) // e.g. "granite" // On a call, Alice says the current word. Bob verifies: const counter = getCounter(Math.floor(Date.now() / 1000), group.rotationInterval) const result = verifyWord(spokenWord, group.seed, group.members, counter, group.wordCount) // result.status === 'verified' | 'duress' | 'stale' | 'failed' // If Alice is under coercion, she says her duress word instead: const duressWord = getCurrentDuressWord(group, alicePubkey) // always distinct from the normal word ``` ## Optional - llms-full.txt: Full API reference with all type signatures, protocol details, and canonical test vectors