Skip to content

Latest commit

 

History

History
306 lines (249 loc) · 12.4 KB

File metadata and controls

306 lines (249 loc) · 12.4 KB

API Reference — canary-kit

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,
  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

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 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')
}

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 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 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)