canary-kit 2.6.2 → 2.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/API.md ADDED
@@ -0,0 +1,306 @@
1
+ # API Reference — canary-kit
2
+
3
+ > Complete API documentation. For getting started, see [README.md](README.md).
4
+
5
+ ## Session API (Directional Verification)
6
+
7
+ ```typescript
8
+ import {
9
+ createSession,
10
+ generateSeed,
11
+ deriveSeed,
12
+ SESSION_PRESETS,
13
+ type Session,
14
+ type SessionConfig,
15
+ type SessionPresetName,
16
+ } from 'canary-kit/session'
17
+ ```
18
+
19
+ | Function | Description |
20
+ |---|---|
21
+ | `createSession(config: SessionConfig)` | Create a role-aware verification session |
22
+ | `generateSeed()` | Generate a 256-bit cryptographic seed |
23
+ | `deriveSeed(masterKey, ...components)` | Derive a seed deterministically from a master key |
24
+
25
+ **Session interface:**
26
+
27
+ | Method | Description |
28
+ |---|---|
29
+ | `session.myToken(nowSec?)` | Token I speak to prove my identity |
30
+ | `session.theirToken(nowSec?)` | Token I expect to hear from the other party |
31
+ | `session.verify(spoken, nowSec?)` | Verify a spoken word — returns `valid`, `duress`, or `invalid` |
32
+ | `session.counter(nowSec?)` | Current counter value (time-based or fixed) |
33
+ | `session.pair(nowSec?)` | Both tokens at once, keyed by role name |
34
+
35
+ **Session presets:**
36
+
37
+ | Preset | Words | Rotation | Tolerance | Use case |
38
+ |--------|-------|----------|-----------|----------|
39
+ | `call` | 1 | 30 seconds | ±1 | Phone verification (insurance, banking) |
40
+ | `handoff` | 1 | Single-use | 0 | Physical handoff (rideshare, delivery) |
41
+
42
+ ## CANARY Protocol (Universal)
43
+
44
+ The universal protocol API works with any transport — not just Nostr groups.
45
+
46
+ ```typescript
47
+ import {
48
+ deriveToken, deriveTokenBytes,
49
+ deriveDuressToken, deriveDuressTokenBytes,
50
+ verifyToken,
51
+ deriveLivenessToken,
52
+ deriveDirectionalPair,
53
+ type TokenVerifyResult, type VerifyOptions,
54
+ type DirectionalPair,
55
+ } from 'canary-kit/token'
56
+
57
+ import {
58
+ encodeAsWords, encodeAsPin, encodeAsHex,
59
+ encodeToken, type TokenEncoding,
60
+ } from 'canary-kit/encoding'
61
+ ```
62
+
63
+ | Function | Description |
64
+ |---|---|
65
+ | `deriveToken(secret, context, counter, encoding?)` | Derive an encoded verification token |
66
+ | `deriveDuressToken(secret, context, identity, counter, encoding, maxTolerance)` | Derive a duress token for a specific identity |
67
+ | `verifyToken(secret, context, counter, input, identities, options?)` | Verify a token — returns `valid`, `duress` (with matching identities), or `invalid` |
68
+ | `deriveLivenessToken(secret, context, identity, counter)` | Derive a liveness heartbeat token for dead man's switch |
69
+ | `deriveDirectionalPair(secret, namespace, roles, counter, encoding?)` | Derive two directional tokens from the same secret |
70
+
71
+ ## Core Derivation
72
+
73
+ ```typescript
74
+ import {
75
+ deriveVerificationWord,
76
+ deriveVerificationPhrase,
77
+ deriveDuressWord,
78
+ deriveDuressPhrase,
79
+ } from 'canary-kit'
80
+ ```
81
+
82
+ | Function | Signature | Description |
83
+ |---|---|---|
84
+ | `deriveVerificationWord` | `(seedHex: string, counter: number) => string` | Derives the single verification word for all group members |
85
+ | `deriveVerificationPhrase` | `(seedHex: string, counter: number, wordCount: 1 \| 2 \| 3) => string[]` | Derives a multi-word verification phrase |
86
+ | `deriveDuressWord` | `(seedHex: string, memberPubkeyHex: string, counter: number) => string` | Derives a member's duress word |
87
+ | `deriveDuressPhrase` | `(seedHex: string, memberPubkeyHex: string, counter: number, wordCount: 1 \| 2 \| 3) => string[]` | Derives a member's multi-word duress phrase |
88
+
89
+ ## Verification
90
+
91
+ ```typescript
92
+ import { verifyWord, type VerifyResult, type VerifyStatus } from 'canary-kit'
93
+ ```
94
+
95
+ `verifyWord(spokenWord, seedHex, memberPubkeys, counter, wordCount?): VerifyResult`
96
+
97
+ Checks a spoken word in order: current verification word → each member's duress word → previous window (stale) → failed.
98
+
99
+ ```typescript
100
+ type VerifyStatus = 'verified' | 'duress' | 'stale' | 'failed'
101
+
102
+ interface VerifyResult {
103
+ status: VerifyStatus
104
+ members?: string[] // pubkeys of coerced members (only when status === 'duress')
105
+ }
106
+ ```
107
+
108
+ ## Group Management
109
+
110
+ ```typescript
111
+ import {
112
+ createGroup,
113
+ getCurrentWord,
114
+ getCurrentDuressWord,
115
+ advanceCounter,
116
+ reseed,
117
+ addMember,
118
+ removeMember,
119
+ type GroupConfig,
120
+ type GroupState,
121
+ } from 'canary-kit'
122
+ ```
123
+
124
+ All functions are pure — they return new state without mutating the input.
125
+
126
+ | Function | Description |
127
+ |---|---|
128
+ | `createGroup(config: GroupConfig)` | Creates a new group with a cryptographically secure random seed |
129
+ | `getCurrentWord(state: GroupState)` | Returns the current verification word or space-joined phrase |
130
+ | `getCurrentDuressWord(state: GroupState, memberPubkey: string)` | Returns the current duress word or phrase for a specific member |
131
+ | `advanceCounter(state: GroupState)` | Increments the usage offset (burn-after-use rotation) |
132
+ | `reseed(state: GroupState)` | Generates a fresh seed and resets the usage offset |
133
+ | `addMember(state: GroupState, pubkey: string)` | Adds a member; idempotent if already present |
134
+ | `removeMember(state: GroupState, pubkey: string)` | Removes a member (does NOT reseed -- old seed still valid) |
135
+ | `removeMemberAndReseed(state: GroupState, pubkey: string)` | Removes a member and immediately reseeds (recommended) |
136
+ | `dissolveGroup(state: GroupState)` | Zeroes the seed and clears all members |
137
+ | `syncCounter(state: GroupState, nowSec?: number)` | Refreshes counter to current time window (monotonic, never regresses) |
138
+
139
+ **GroupConfig fields:**
140
+
141
+ | Field | Type | Description |
142
+ |---|---|---|
143
+ | `name` | `string` | Group name (required) |
144
+ | `members` | `string[]` | Nostr pubkeys, 64-char hex (required) |
145
+ | `preset` | `PresetName` | Named threat-profile preset (optional) |
146
+ | `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. |
147
+ | `rotationInterval` | `number` | Seconds; overrides preset value |
148
+ | `wordCount` | `1 \| 2 \| 3` | Words per challenge; overrides preset value |
149
+ | `tolerance` | `number` | Counter tolerance for verification: accept tokens within +/-tolerance counter values (default: 1) |
150
+ | `beaconInterval` | `number` | Beacon broadcast interval in seconds (default: 300) |
151
+ | `beaconPrecision` | `number` | Geohash precision for normal beacons 1--11 (default: 6) |
152
+
153
+ **GroupState fields:**
154
+
155
+ | Field | Type | Description |
156
+ |---|---|---|
157
+ | `name` | `string` | Group name |
158
+ | `seed` | `string` | 64-char hex (256-bit shared secret) |
159
+ | `members` | `string[]` | Current member pubkeys |
160
+ | `admins` | `string[]` | Pubkeys with admin privileges (reseed, add/remove others) |
161
+ | `rotationInterval` | `number` | Seconds between automatic word rotation |
162
+ | `wordCount` | `1 \| 2 \| 3` | Words per challenge |
163
+ | `counter` | `number` | Time-based counter at last sync |
164
+ | `usageOffset` | `number` | Burn-after-use offset on top of counter |
165
+ | `tolerance` | `number` | Counter tolerance for verification |
166
+ | `epoch` | `number` | Monotonic epoch -- increments on reseed (replay protection) |
167
+ | `consumedOps` | `string[]` | Consumed operation IDs within current epoch |
168
+ | `consumedOpsFloor` | `number?` | Timestamp floor for replay protection after consumedOps eviction |
169
+ | `createdAt` | `number` | Unix timestamp of group creation |
170
+ | `beaconInterval` | `number` | Seconds between beacon broadcasts |
171
+ | `beaconPrecision` | `number` | Geohash precision (1--11) |
172
+
173
+ ## Threat-Profile Presets
174
+
175
+ ```typescript
176
+ import { createGroup, PRESETS, type PresetName } from 'canary-kit'
177
+ ```
178
+
179
+ **Group presets:**
180
+
181
+ | Preset | Words | Rotation | Use case |
182
+ |--------|-------|----------|----------|
183
+ | `family` | 1 | 7 days | Casual family/friend verification |
184
+ | `field-ops` | 2 | 24 hours | Journalism, activism, field work |
185
+ | `enterprise` | 2 | 48 hours | Corporate incident response |
186
+
187
+ Explicit config values always override preset defaults.
188
+
189
+ ## Counter
190
+
191
+ ```typescript
192
+ import { getCounter, counterToBytes, DEFAULT_ROTATION_INTERVAL } from 'canary-kit'
193
+ ```
194
+
195
+ | Export | Description |
196
+ |---|---|
197
+ | `getCounter(timestampSec, rotationIntervalSec?)` | Returns `floor(timestamp / interval)` — the current time window |
198
+ | `counterToBytes(counter)` | Serialises a counter to an 8-byte big-endian `Uint8Array` (RFC 6238 encoding) |
199
+ | `DEFAULT_ROTATION_INTERVAL` | `604800` — 7 days in seconds |
200
+
201
+ ## Wordlist
202
+
203
+ ```typescript
204
+ import { WORDLIST, WORDLIST_SIZE, getWord, indexOf } from 'canary-kit'
205
+ // or: import { WORDLIST, WORDLIST_SIZE, getWord, indexOf } from 'canary-kit/wordlist'
206
+ ```
207
+
208
+ | Export | Description |
209
+ |---|---|
210
+ | `WORDLIST` | `readonly string[]` — 2048 words curated for spoken clarity |
211
+ | `WORDLIST_SIZE` | `2048` |
212
+ | `getWord(index: number)` | Returns the word at the given index |
213
+ | `indexOf(word: string)` | Returns the index of a word, or `-1` if not found |
214
+
215
+ 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.
216
+
217
+ ## Nostr Events
218
+
219
+ ```typescript
220
+ import {
221
+ buildGroupStateEvent,
222
+ buildStoredSignalEvent,
223
+ buildSignalEvent,
224
+ buildRumourEvent,
225
+ hashGroupId,
226
+ KINDS,
227
+ type UnsignedEvent,
228
+ } from 'canary-kit/nostr'
229
+ ```
230
+
231
+ All builders return an `UnsignedEvent`. Sign with your own Nostr library. Uses standard Nostr kinds — no custom event kinds.
232
+
233
+ | Builder | Kind | Description |
234
+ |---|---|---|
235
+ | `buildGroupStateEvent(params)` | `30078` | Parameterised replaceable group state with `ssg/` d-tag namespace |
236
+ | `buildStoredSignalEvent(params)` | `30078` | Parameterised replaceable stored signal with hashed d-tag and 7-day expiration |
237
+ | `buildSignalEvent(params)` | `20078` | Ephemeral real-time signal (beacon, word-used, counter-advance) |
238
+ | `buildRumourEvent(params)` | `14` | NIP-17 rumour for seed distribution, reseed, and member updates (consumer wraps in kind 1059) |
239
+
240
+ `KINDS` exports `{ groupState: 30078, signal: 20078, giftWrap: 1059 }`. `hashGroupId(groupId)` returns a SHA-256 hash for privacy-preserving d-tags.
241
+
242
+ ## Beacon & Duress Alerts
243
+
244
+ ```typescript
245
+ import {
246
+ deriveBeaconKey,
247
+ encryptBeacon, decryptBeacon,
248
+ buildDuressAlert, encryptDuressAlert, decryptDuressAlert,
249
+ } from 'canary-kit/beacon'
250
+ ```
251
+
252
+ ## Sync Protocol
253
+
254
+ ```typescript
255
+ import {
256
+ applySyncMessage,
257
+ applySyncMessageWithResult,
258
+ decodeSyncMessage,
259
+ encodeSyncMessage,
260
+ deriveGroupKey,
261
+ deriveGroupIdentity,
262
+ hashGroupTag,
263
+ encryptEnvelope,
264
+ decryptEnvelope,
265
+ PROTOCOL_VERSION,
266
+ type SyncMessage,
267
+ type SyncApplyResult,
268
+ type SyncTransport,
269
+ type EventSigner,
270
+ } from 'canary-kit/sync'
271
+ ```
272
+
273
+ 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](COOKBOOK.md) for complete workflow examples.
274
+
275
+ | Function | Signature | Description |
276
+ |---|---|---|
277
+ | `applySyncMessage` | `(state, msg, nowSec?, sender?) → GroupState` | Apply a sync message. Returns new state, or the same reference if rejected. |
278
+ | `applySyncMessageWithResult` | `(state, msg, nowSec?, sender?) → SyncApplyResult` | Same as above but returns `{ state, applied }` for observability. |
279
+ | `decodeSyncMessage` | `(json: string) → SyncMessage` | Parse and validate a JSON sync message. Throws on invalid input. |
280
+ | `encodeSyncMessage` | `(msg: SyncMessage) → string` | Serialise a sync message to JSON (injects `protocolVersion`). |
281
+
282
+ **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.
283
+
284
+ **Sender requirements:**
285
+ - Privileged actions (member-join of others, member-leave of others, reseed, state-snapshot) require `sender` to be in `group.admins`.
286
+ - `counter-advance` requires `sender` to be in `group.members`.
287
+ - Omitting `sender` for these operations causes silent rejection.
288
+
289
+ ```typescript
290
+ interface SyncApplyResult {
291
+ state: GroupState
292
+ applied: boolean
293
+ }
294
+ ```
295
+
296
+ | Message type | Description |
297
+ |---|---|
298
+ | `member-join` | Add a member (admin-only, or self-join if sender is the pubkey) |
299
+ | `member-leave` | Remove a member (admin-only) or self-leave |
300
+ | `counter-advance` | Advance the group counter (burn-after-use) |
301
+ | `reseed` | Distribute a new seed with epoch bump (admin-only) |
302
+ | `beacon` | Encrypted location heartbeat (fire-and-forget) |
303
+ | `duress-alert` | Silent duress location alert (fire-and-forget) |
304
+ | `duress-clear` | Clear a duress alert |
305
+ | `liveness-checkin` | Dead man's switch heartbeat (fire-and-forget) |
306
+ | `state-snapshot` | Full state sync for new/rejoining members (admin-only) |