canary-kit 2.7.0 → 2.7.2
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 +306 -0
- package/COOKBOOK.md +763 -0
- package/INTEGRATION.md +329 -3
- package/README.md +17 -0
- package/SECURITY.md +2 -1
- package/llms-full.txt +71 -12
- package/llms.txt +16 -9
- package/package.json +3 -1
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) |
|