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/llms-full.txt CHANGED
@@ -38,9 +38,9 @@ import { deriveToken, deriveDuressToken, verifyToken, deriveLivenessToken, deriv
38
38
  import { encodeAsWords, encodeAsPin, encodeAsHex, encodeToken } from 'canary-kit/encoding'
39
39
  import { createSession, generateSeed, deriveSeed, SESSION_PRESETS } from 'canary-kit/session'
40
40
  import { WORDLIST, WORDLIST_SIZE, getWord, indexOf } from 'canary-kit/wordlist'
41
- import { KINDS, buildGroupEvent, buildSeedDistributionEvent, buildMemberUpdateEvent, buildReseedEvent, buildWordUsedEvent, buildBeaconEvent } from 'canary-kit/nostr'
41
+ import { KINDS, buildGroupStateEvent, buildStoredSignalEvent, buildSignalEvent, buildRumourEvent, hashGroupId } from 'canary-kit/nostr'
42
42
  import { deriveBeaconKey, encryptBeacon, decryptBeacon, buildDuressAlert, encryptDuressAlert, decryptDuressAlert } from 'canary-kit/beacon'
43
- import { applySyncMessage, decodeSyncMessage, encodeSyncMessage, deriveGroupKey, deriveGroupSigningKey, hashGroupTag, encryptEnvelope, decryptEnvelope } from 'canary-kit/sync'
43
+ import { applySyncMessage, applySyncMessageWithResult, decodeSyncMessage, encodeSyncMessage, deriveGroupKey, deriveGroupIdentity, hashGroupTag, encryptEnvelope, decryptEnvelope, PROTOCOL_VERSION, type SyncApplyResult, type SyncTransport, type EventSigner } from 'canary-kit/sync'
44
44
  ```
45
45
 
46
46
  ---
@@ -107,8 +107,10 @@ interface GroupConfig {
107
107
  rotationInterval?: number // Seconds; overrides preset value
108
108
  wordCount?: 1 | 2 | 3 // Words per challenge; overrides preset value
109
109
  wordlist?: string // Wordlist identifier (default: 'en-v1')
110
+ tolerance?: number // Counter tolerance for verification (default: 1)
110
111
  beaconInterval?: number // Beacon broadcast interval in seconds (default: 300)
111
112
  beaconPrecision?: number // Geohash precision for normal beacons 1–11 (default: 6)
113
+ creator?: string // Pubkey of group creator — only the creator is admin at bootstrap. Must be in members. Without creator, admins is empty and all privileged sync ops are silently rejected.
112
114
  }
113
115
 
114
116
  /** Serialisable persistent state for a canary group. */
@@ -116,14 +118,19 @@ interface GroupState {
116
118
  name: string
117
119
  seed: string // 64-char hex (256-bit shared secret)
118
120
  members: string[] // Nostr pubkeys (64-char hex)
121
+ admins: string[] // Pubkeys with admin privileges (reseed, add/remove others). Set via GroupConfig.creator.
119
122
  rotationInterval: number // Seconds
120
123
  wordCount: 1 | 2 | 3
121
124
  wordlist: string // e.g. 'en-v1'
122
125
  counter: number // Time-based counter at last sync
123
126
  usageOffset: number // Burn-after-use offset on top of counter
127
+ tolerance: number // Counter tolerance for verification
124
128
  createdAt: number // Unix timestamp
125
129
  beaconInterval: number // Seconds
126
130
  beaconPrecision: number // 1–11
131
+ epoch: number // Monotonic epoch — increments on reseed. Used for replay protection.
132
+ consumedOps: string[] // Consumed operation IDs within current epoch. Cleared on epoch bump.
133
+ consumedOpsFloor?: number // Timestamp floor: reject messages at or below this value (replay protection after consumedOps eviction)
127
134
  }
128
135
 
129
136
  // ── Group Presets ─────────────────────────────────────────────────────────────
@@ -177,7 +184,7 @@ interface Session {
177
184
 
178
185
  // ── Beacon ────────────────────────────────────────────────────────────────────
179
186
 
180
- /** Decrypted content of a kind 20800 location beacon event. */
187
+ /** Decrypted content of an encrypted location beacon (kind 20078 signal). */
181
188
  interface BeaconPayload {
182
189
  geohash: string
183
190
  precision: number
@@ -429,16 +436,41 @@ const updated = addMember(state, '<charlie-pubkey>')
429
436
 
430
437
  ### removeMember(state, pubkey)
431
438
 
432
- Remove a member from the group and immediately reseed to invalidate the old shared secret. Returns new state — does not mutate.
439
+ Remove a member from the group's member list. Does NOT reseed the removed member still possesses the old seed. Use `removeMemberAndReseed()` instead unless you have a specific reason not to. Returns new state — does not mutate.
433
440
 
434
441
  ```typescript
435
442
  removeMember(state: GroupState, pubkey: string): GroupState
436
443
 
437
444
  const updated = removeMember(state, '<alice-pubkey>')
445
+ // updated.seed === state.seed (NOT reseeded — old seed still valid)
446
+ // updated.members does not include alice
447
+ ```
448
+
449
+ ### removeMemberAndReseed(state, pubkey)
450
+
451
+ Remove a member and immediately reseed, atomically invalidating the old seed. This is the recommended way to remove members — ensures forward secrecy by preventing the removed member from deriving future tokens or decrypting future beacons. Returns new state — does not mutate.
452
+
453
+ ```typescript
454
+ removeMemberAndReseed(state: GroupState, pubkey: string): GroupState
455
+
456
+ const updated = removeMemberAndReseed(state, '<alice-pubkey>')
438
457
  // updated.seed !== state.seed (reseeded)
439
458
  // updated.members does not include alice
440
459
  ```
441
460
 
461
+ ### dissolveGroup(state)
462
+
463
+ Dissolve a group, zeroing the seed and clearing all members. Preserves name and timestamps for audit trail. Callers MUST also delete the persisted record (IndexedDB, localStorage, backend) after calling this. Returns new state — does not mutate.
464
+
465
+ ```typescript
466
+ dissolveGroup(state: GroupState): GroupState
467
+
468
+ const dissolved = dissolveGroup(state)
469
+ // dissolved.seed === '0'.repeat(64)
470
+ // dissolved.members === []
471
+ // dissolved.admins === []
472
+ ```
473
+
442
474
  ### syncCounter(state, nowSec?)
443
475
 
444
476
  Refresh the counter to the current time window. Call after loading persisted state. Returns new state — does not mutate.
@@ -1007,117 +1039,126 @@ WORDLIST[2047] // 'zoo'
1007
1039
 
1008
1040
  ## canary-kit/nostr
1009
1041
 
1010
- Nostr event builders for the CANARY NIP. All builders return unsigned events (`UnsignedEvent`) — sign with your own Nostr library (e.g. `nostr-tools`).
1042
+ Nostr event builders for SSG (Simple Shared Secret) groups. Uses standard Nostr kinds — no custom event kinds. All builders return unsigned events (`UnsignedEvent`) — sign with your own Nostr library (e.g. `nostr-tools`).
1043
+
1044
+ Three event kinds are used:
1045
+ - **kind 30078** (parameterised replaceable) — group state and stored signals. The d-tag uses the `ssg/` namespace prefix.
1046
+ - **kind 20078** (ephemeral) — real-time signals between group members.
1047
+ - **kind 14** (rumour / unsigned inner event) — NIP-17 gift-wrapped DMs carrying seed distribution, reseed, and other private payloads.
1011
1048
 
1012
1049
  ### KINDS
1013
1050
 
1014
1051
  ```typescript
1015
1052
  const KINDS = {
1016
- group: 38_800, // replaceable — group announcement
1017
- seedDistribution: 28_800, // ephemeral — seed delivery to member
1018
- memberUpdate: 38_801, // replaceable add/remove member
1019
- reseed: 28_801, // ephemeral — new seed broadcast
1020
- wordUsed: 28_802, // ephemeral — burn-after-use notification
1021
- beacon: 20_800, // ephemeral — encrypted location beacon
1053
+ groupState: 30_078, // parameterised replaceable — group state and stored signals
1054
+ signal: 20_078, // ephemeral — real-time signals
1055
+ giftWrap: 1_059, // NIP-17 gift wrap (consumer wraps kind 14 rumours)
1022
1056
  } as const
1023
1057
  ```
1024
1058
 
1025
- ### buildGroupEvent(params)
1059
+ ### buildGroupStateEvent(params)
1026
1060
 
1027
- Build a kind 38800 group announcement event.
1061
+ Build an unsigned kind 30078 group state event. The d-tag uses plaintext `ssg/<groupId>` since content is NIP-44 encrypted. Includes NIP-32 labels for the SSG namespace and p-tags for each member.
1028
1062
 
1029
1063
  ```typescript
1030
- buildGroupEvent(params: GroupEventParams): UnsignedEvent
1064
+ buildGroupStateEvent(params: GroupStateEventParams): UnsignedEvent
1031
1065
 
1032
- buildGroupEvent({
1066
+ interface GroupStateEventParams {
1067
+ groupId: string
1068
+ members: string[] // hex pubkeys
1069
+ encryptedContent: string // NIP-44 encrypted group config
1070
+ rotationInterval?: number // seconds between word rotations
1071
+ tolerance?: number // counter tolerance window
1072
+ expiration?: number // NIP-40 expiration (unix seconds)
1073
+ }
1074
+
1075
+ buildGroupStateEvent({
1033
1076
  groupId: 'alpine-team-1',
1034
- name: 'Alpine Team',
1035
1077
  members: ['<alice-pubkey>', '<bob-pubkey>'],
1078
+ encryptedContent: '<NIP-44 encrypted group config>',
1036
1079
  rotationInterval: 86_400,
1037
- wordCount: 2,
1038
- wordlist: 'en-v1',
1039
- encryptedContent: '<NIP-44 encrypted group metadata>',
1040
- expiration: Math.floor(Date.now() / 1000) + 30 * 86_400, // 30 days
1080
+ tolerance: 1,
1081
+ expiration: Math.floor(Date.now() / 1000) + 30 * 86_400,
1041
1082
  })
1042
- // { kind: 38800, content: '...', tags: [['d','alpine-team-1'],['name','Alpine Team'],['p','<alice>'],['p','<bob>'],['rotation','86400'],['words','2'],['wordlist','en-v1'],['expiration','...']], created_at: ... }
1083
+ // { kind: 30078, content: '...', tags: [['d','ssg/alpine-team-1'],['p','<alice>'],['p','<bob>'],['L','ssg'],['l','group','ssg'],['rotation','86400'],['tolerance','1'],['expiration','...']], created_at: ... }
1043
1084
  ```
1044
1085
 
1045
- ### buildSeedDistributionEvent(params)
1086
+ ### buildStoredSignalEvent(params)
1046
1087
 
1047
- Build a kind 28800 seed distribution event, sent privately to each member.
1088
+ Build an unsigned kind 30078 stored signal event. The d-tag uses a SHA-256 hash of the group ID (for privacy) scoped by signal type: `ssg/<SHA256(groupId)>:<signalType>`. Includes a 7-day NIP-40 expiration tag.
1048
1089
 
1049
1090
  ```typescript
1050
- buildSeedDistributionEvent(params: SeedDistributionParams): UnsignedEvent
1091
+ buildStoredSignalEvent(params: StoredSignalEventParams): UnsignedEvent
1051
1092
 
1052
- buildSeedDistributionEvent({
1053
- recipientPubkey: '<alice-pubkey>',
1054
- groupEventId: '<kind-38800-event-id>',
1055
- encryptedContent: '<NIP-44 encrypted seed for alice>',
1056
- })
1057
- // { kind: 28800, content: '...', tags: [['p','<alice>'],['e','<group-id>']], created_at: ... }
1058
- ```
1059
-
1060
- ### buildMemberUpdateEvent(params)
1061
-
1062
- Build a kind 38801 member update event.
1063
-
1064
- ```typescript
1065
- buildMemberUpdateEvent(params: MemberUpdateParams): UnsignedEvent
1093
+ interface StoredSignalEventParams {
1094
+ groupId: string
1095
+ signalType: string // e.g. 'counter-advance', 'word-used'
1096
+ encryptedContent: string
1097
+ }
1066
1098
 
1067
- buildMemberUpdateEvent({
1099
+ buildStoredSignalEvent({
1068
1100
  groupId: 'alpine-team-1',
1069
- action: 'add',
1070
- memberPubkey: '<charlie-pubkey>',
1071
- reseed: true,
1072
- encryptedContent: '<encrypted update content>',
1101
+ signalType: 'counter-advance',
1102
+ encryptedContent: '<encrypted signal payload>',
1073
1103
  })
1074
- // { kind: 38801, ... tags: [['d','alpine-team-1'],['action','add'],['p','<charlie>'],['reseed','true']], ... }
1104
+ // { kind: 30078, content: '...', tags: [['d','ssg/<hash>:counter-advance'],['expiration','...']], created_at: ... }
1075
1105
  ```
1076
1106
 
1077
- ### buildReseedEvent(params)
1107
+ ### buildSignalEvent(params)
1078
1108
 
1079
- Build a kind 28801 reseed event.
1109
+ Build an unsigned kind 20078 ephemeral signal event. The d-tag uses a SHA-256 hash of the group ID (for privacy). A t-tag carries the signal type. No expiration tag — ephemeral events are not stored by relays.
1080
1110
 
1081
1111
  ```typescript
1082
- buildReseedEvent(params: ReseedParams): UnsignedEvent
1112
+ buildSignalEvent(params: SignalEventParams): UnsignedEvent
1113
+
1114
+ interface SignalEventParams {
1115
+ groupId: string
1116
+ signalType: string // e.g. 'beacon', 'word-used', 'counter-advance'
1117
+ encryptedContent: string
1118
+ }
1083
1119
 
1084
- buildReseedEvent({
1085
- groupEventId: '<kind-38800-event-id>',
1086
- reason: 'member_removed',
1087
- encryptedContent: '<NIP-44 encrypted new seed>',
1120
+ buildSignalEvent({
1121
+ groupId: 'alpine-team-1',
1122
+ signalType: 'beacon',
1123
+ encryptedContent: '<AES-256-GCM encrypted beacon payload>',
1088
1124
  })
1089
- // { kind: 28801, ... tags: [['e','<group-id>'],['reason','member_removed']], ... }
1125
+ // { kind: 20078, content: '...', tags: [['d','ssg/<hash>'],['t','beacon']], created_at: ... }
1090
1126
  ```
1091
1127
 
1092
- Valid reason values: `'member_removed'` | `'compromise'` | `'scheduled'` | `'duress'`
1128
+ ### buildRumourEvent(params)
1093
1129
 
1094
- ### buildWordUsedEvent(params)
1130
+ Build an unsigned kind 14 rumour event for NIP-17 gift wrapping. The consumer must set the `pubkey` field before computing the event ID, then seal (NIP-44 encrypt + kind 13) and gift-wrap (kind 1059) the rumour.
1095
1131
 
1096
- Build a kind 28802 word-used (burn-after-use) notification event.
1132
+ Used for seed distribution, reseed notifications, and member updates — anything that must be sent privately to a specific member.
1097
1133
 
1098
1134
  ```typescript
1099
- buildWordUsedEvent(params: WordUsedParams): UnsignedEvent
1135
+ buildRumourEvent(params: RumourEventParams): UnsignedEvent
1136
+
1137
+ interface RumourEventParams {
1138
+ recipientPubkey: string // 64-char hex pubkey
1139
+ subject: string // e.g. 'ssg:seed-distribution', 'ssg:reseed', 'ssg:member-update'
1140
+ encryptedContent: string // NIP-44 encrypted payload
1141
+ groupEventId?: string // optional reference to the group state event
1142
+ }
1100
1143
 
1101
- buildWordUsedEvent({
1102
- groupEventId: '<kind-38800-event-id>',
1103
- encryptedContent: '<encrypted burn notification>',
1144
+ buildRumourEvent({
1145
+ recipientPubkey: '<alice-pubkey>',
1146
+ subject: 'ssg:seed-distribution',
1147
+ encryptedContent: '<NIP-44 encrypted seed>',
1148
+ groupEventId: '<group-state-event-id>',
1104
1149
  })
1105
- // { kind: 28802, ... tags: [['e','<group-id>']], ... }
1150
+ // { kind: 14, content: '...', tags: [['p','<alice>'],['subject','ssg:seed-distribution'],['e','<group-id>']], created_at: ... }
1106
1151
  ```
1107
1152
 
1108
- ### buildBeaconEvent(params)
1153
+ ### hashGroupId(groupId)
1109
1154
 
1110
- Build a kind 20800 encrypted location beacon event.
1155
+ SHA-256 hash a group ID for use in d-tags where privacy matters.
1111
1156
 
1112
1157
  ```typescript
1113
- buildBeaconEvent(params: BeaconEventParams): UnsignedEvent
1158
+ hashGroupId(groupId: string): string
1114
1159
 
1115
- buildBeaconEvent({
1116
- groupId: 'alpine-team-1',
1117
- encryptedContent: '<AES-256-GCM encrypted beacon payload>',
1118
- expiration: Math.floor(Date.now() / 1000) + 3600, // 1 hour
1119
- })
1120
- // { kind: 20800, ... tags: [['h','alpine-team-1'],['expiration','...']], ... }
1160
+ hashGroupId('alpine-team-1')
1161
+ // '3a7f...' (64-char hex)
1121
1162
  ```
1122
1163
 
1123
1164
  ---
@@ -1150,26 +1191,53 @@ type SyncMessage =
1150
1191
  | { type: 'liveness-checkin'; pubkey: string; timestamp: number; opId: string }
1151
1192
  | { type: 'state-snapshot'; seed: string; counter: number; usageOffset: number; members: string[]; admins: string[]; epoch: number; opId: string; timestamp: number; prevEpochSeed?: string }
1152
1193
 
1153
- interface SyncResult {
1154
- state: GroupState
1155
- applied: boolean
1156
- reason?: string
1194
+ /** Result of applying a sync message, indicating whether it was accepted. */
1195
+ interface SyncApplyResult {
1196
+ state: GroupState // The resulting group state (unchanged if rejected)
1197
+ applied: boolean // Whether the message was applied
1198
+ }
1199
+
1200
+ /** Minimal interface any sync transport must implement. */
1201
+ interface SyncTransport {
1202
+ send(groupId: string, message: SyncMessage, recipients?: string[]): Promise<void>
1203
+ subscribe(groupId: string, onMessage: (msg: SyncMessage, sender: string) => void): () => void
1204
+ disconnect(): void
1205
+ }
1206
+
1207
+ /** Abstracts event signing and NIP-44 encryption for transports that need it. */
1208
+ interface EventSigner {
1209
+ pubkey: string
1210
+ sign(event: unknown): Promise<unknown>
1211
+ encrypt(plaintext: string, recipientPubkey: string): Promise<string>
1212
+ decrypt(ciphertext: string, senderPubkey: string): Promise<string>
1157
1213
  }
1158
1214
  ```
1159
1215
 
1160
1216
  ### Functions
1161
1217
 
1162
1218
  ```typescript
1163
- decodeSyncMessage(json: string): SyncMessage // Parse and validate a JSON sync message
1164
- encodeSyncMessage(msg: SyncMessage): string // Serialise a sync message to JSON
1165
- applySyncMessage(state: GroupState, msg: SyncMessage, sender?: string): SyncResult // Apply a validated message to group state
1219
+ decodeSyncMessage(json: string): SyncMessage // Parse and validate a JSON sync message. Throws on invalid input or wrong protocolVersion.
1220
+ encodeSyncMessage(msg: SyncMessage): string // Serialise a sync message to JSON. Injects protocolVersion automatically.
1221
+ applySyncMessage(state: GroupState, msg: SyncMessage, nowSec?: number, sender?: string): GroupState // Apply a sync message. Returns new state, or SAME REFERENCE if rejected (silent rejection).
1222
+ applySyncMessageWithResult(state: GroupState, msg: SyncMessage, nowSec?: number, sender?: string): SyncApplyResult // Same as applySyncMessage but returns { state, applied } for observability.
1166
1223
  ```
1167
1224
 
1225
+ **IMPORTANT — silent rejection:** `applySyncMessage` returns the unchanged GroupState reference when a message is rejected. Common causes of silent rejection:
1226
+ - Privileged action without sender, or sender not in `group.admins` (I1)
1227
+ - Replayed opId already in `consumedOps` (I2)
1228
+ - Epoch mismatch: non-reseed ops must match local epoch (I3), reseed must be local epoch + 1 (I4)
1229
+ - Stale epoch for any privileged op (I6)
1230
+ - `counter-advance` without sender, or sender not in `group.members`
1231
+ - `counter-advance` that would regress the effective counter (monotonic)
1232
+ - Fire-and-forget messages older than 5 minutes or >60s in the future
1233
+
1234
+ Use `applySyncMessageWithResult` when you need to log or alert on rejected messages.
1235
+
1168
1236
  ### Envelope Encryption
1169
1237
 
1170
1238
  ```typescript
1171
1239
  deriveGroupKey(seed: string): Uint8Array // Derive AES-256-GCM group key from seed
1172
- deriveGroupSigningKey(seed: string): Uint8Array // Derive HMAC signing key from seed
1240
+ deriveGroupIdentity(seed: string): Uint8Array // Derive HMAC signing key from seed
1173
1241
  hashGroupTag(groupId: string): string // Privacy-preserving group tag hash
1174
1242
  encryptEnvelope(key: Uint8Array, plaintext: string): Promise<string> // AES-256-GCM encrypt
1175
1243
  decryptEnvelope(key: Uint8Array, ciphertext: string): Promise<string> // AES-256-GCM decrypt
@@ -1223,16 +1291,15 @@ Used with `createSession({ preset: '...' })`.
1223
1291
 
1224
1292
  ## Nostr Event Kinds
1225
1293
 
1226
- | Kind | Type | Name | Description |
1294
+ Uses standard Nostr kinds no custom event kinds.
1295
+
1296
+ | Kind | Type | KINDS key | Description |
1227
1297
  |---|---|---|---|
1228
- | 38800 | Replaceable | `group` | Group announcement with member list and configuration |
1229
- | 28800 | Ephemeral | `seedDistribution` | Encrypted seed delivery to a specific member |
1230
- | 38801 | Replaceable | `memberUpdate` | Add or remove a group member (with optional reseed) |
1231
- | 28801 | Ephemeral | `reseed` | New seed broadcast after compromise or member removal |
1232
- | 28802 | Ephemeral | `wordUsed` | Burn-after-use notification — advance counter for all members |
1233
- | 20800 | Ephemeral | `beacon` | AES-256-GCM encrypted location beacon |
1298
+ | 30078 | Parameterised replaceable | `groupState` | Group state (d-tag `ssg/<groupId>`) and stored signals (d-tag `ssg/<hash>:<signalType>`) |
1299
+ | 20078 | Ephemeral | `signal` | Real-time signals between group members (beacon, word-used, counter-advance) |
1300
+ | 14 → 1059 | NIP-17 gift wrap | `giftWrap` | Seed distribution, reseed, member updates (kind 14 rumour sealed + wrapped) |
1234
1301
 
1235
- Replaceable events (38xxx) use the `d` tag for addressability. Ephemeral events (28xxx, 20xxx) are not stored by relays.
1302
+ Kind 30078 events use the `ssg/` d-tag namespace prefix. Kind 20078 events hash the group ID in the d-tag for privacy. Kind 14 rumours are sealed (kind 13) and gift-wrapped (kind 1059) by the consumer — the library builds the unsigned rumour only.
1236
1303
 
1237
1304
  ---
1238
1305
 
@@ -1343,22 +1410,22 @@ state = advanceCounter(state) // next call will use a different word
1343
1410
  state = addMember(state, '<diana-pubkey>')
1344
1411
  // Distribute new seed to diana via encrypted Nostr DM
1345
1412
 
1346
- // Remove a compromised member (auto-reseeds)
1347
- state = removeMember(state, '<charlie-pubkey>')
1413
+ // Remove a compromised member and reseed atomically
1414
+ state = removeMemberAndReseed(state, '<charlie-pubkey>')
1348
1415
  // Distribute new state.seed to remaining members
1349
1416
  ```
1350
1417
 
1351
1418
  ### Nostr Group Integration
1352
1419
 
1353
- Publish group state and encrypted seeds to Nostr relays so all members can synchronise.
1420
+ Publish group state and encrypted seeds to Nostr relays so all members can synchronise. Uses standard Nostr kinds (30078, 20078, NIP-17 gift wrap) — no custom event kinds.
1354
1421
 
1355
1422
  ```typescript
1356
- import { createGroup, syncCounter, deriveBeaconKey, encryptBeacon } from 'canary-kit'
1423
+ import { createGroup, deriveBeaconKey, encryptBeacon } from 'canary-kit'
1357
1424
  import {
1358
1425
  KINDS,
1359
- buildGroupEvent,
1360
- buildSeedDistributionEvent,
1361
- buildBeaconEvent,
1426
+ buildGroupStateEvent,
1427
+ buildSignalEvent,
1428
+ buildRumourEvent,
1362
1429
  } from 'canary-kit/nostr'
1363
1430
 
1364
1431
  // 1. Create the group
@@ -1368,37 +1435,34 @@ let state = createGroup({
1368
1435
  preset: 'field-ops',
1369
1436
  })
1370
1437
 
1371
- // 2. Publish a kind 38800 group event (encrypted content via NIP-44)
1372
- const groupEvent = buildGroupEvent({
1438
+ // 2. Publish a kind 30078 group state event (encrypted content via NIP-44)
1439
+ const groupEvent = buildGroupStateEvent({
1373
1440
  groupId: 'alpine-team-1',
1374
- name: state.name,
1375
1441
  members: state.members,
1376
- rotationInterval: state.rotationInterval,
1377
- wordCount: state.wordCount,
1378
- wordlist: state.wordlist,
1379
1442
  encryptedContent: '<NIP-44 encrypted group config>',
1380
- expiration: Math.floor(Date.now() / 1000) + 30 * 86_400,
1443
+ rotationInterval: state.rotationInterval,
1381
1444
  })
1382
1445
  // sign and publish groupEvent to relays
1383
1446
 
1384
- // 3. Send seed to each member via kind 28800 (one event per member)
1447
+ // 3. Send seed to each member via NIP-17 gift wrap (kind 14 rumour → seal → wrap)
1385
1448
  for (const memberPubkey of state.members) {
1386
- const seedEvent = buildSeedDistributionEvent({
1449
+ const rumour = buildRumourEvent({
1387
1450
  recipientPubkey: memberPubkey,
1388
- groupEventId: '<signed-group-event-id>',
1451
+ subject: 'ssg:seed-distribution',
1389
1452
  encryptedContent: '<NIP-44 encrypted seed for this member>',
1453
+ groupEventId: '<signed-group-event-id>',
1390
1454
  })
1391
- // sign and publish seedEvent
1455
+ // set pubkey, compute id, then seal (kind 13) and gift-wrap (kind 1059)
1392
1456
  }
1393
1457
 
1394
- // 4. Publish periodic location beacons (kind 20800)
1458
+ // 4. Publish periodic location beacons as kind 20078 ephemeral signals
1395
1459
  const beaconKey = deriveBeaconKey(state.seed)
1396
1460
  const encryptedBeacon = await encryptBeacon(beaconKey, 'gcpvjb', 6)
1397
1461
 
1398
- const beaconEvent = buildBeaconEvent({
1462
+ const beaconEvent = buildSignalEvent({
1399
1463
  groupId: 'alpine-team-1',
1464
+ signalType: 'beacon',
1400
1465
  encryptedContent: encryptedBeacon,
1401
- expiration: Math.floor(Date.now() / 1000) + state.beaconInterval * 2,
1402
1466
  })
1403
1467
  // sign and publish beaconEvent
1404
1468
 
package/llms.txt CHANGED
@@ -25,14 +25,16 @@ Core derivation:
25
25
  - verifyWord(spokenWord, seedHex, memberPubkeys, counter, wordCount?) → VerifyResult
26
26
 
27
27
  Group lifecycle:
28
- - createGroup(config) → GroupState
28
+ - createGroup(config) → GroupState (config.creator sets initial admin; without creator, admins is empty)
29
29
  - getCurrentWord(state) → string
30
30
  - getCurrentDuressWord(state, memberPubkey) → string
31
31
  - advanceCounter(state) → GroupState
32
32
  - reseed(state) → GroupState
33
33
  - addMember(state, pubkey) → GroupState
34
- - removeMember(state, pubkey) → GroupState
35
- - syncCounter(state, nowSec?) → GroupState
34
+ - removeMember(state, pubkey) → GroupState (does NOT reseed)
35
+ - removeMemberAndReseed(state, pubkey) → GroupState (recommended: removes + reseeds atomically)
36
+ - dissolveGroup(state) → GroupState (zeroes seed, clears members)
37
+ - syncCounter(state, nowSec?) → GroupState (refreshes counter to current time window, monotonic)
36
38
 
37
39
  Counter:
38
40
  - getCounter(timestampSec, rotationIntervalSec?) → number
@@ -100,19 +102,18 @@ Session methods:
100
102
 
101
103
  ### canary-kit/nostr
102
104
 
103
- Unsigned Nostr event builders for the CANARY protocol (NIP-CANARY). Sign events with your preferred Nostr library.
105
+ 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.
104
106
 
105
- - KINDS — { group: 38800, seedDistribution: 28800, memberUpdate: 38801, reseed: 28801, wordUsed: 28802, beacon: 20800 }
106
- - buildGroupEvent(params) → UnsignedEvent
107
- - buildSeedDistributionEvent(params) → UnsignedEvent
108
- - buildMemberUpdateEvent(params) → UnsignedEvent
109
- - buildReseedEvent(params) → UnsignedEvent
110
- - buildWordUsedEvent(params) → UnsignedEvent
111
- - buildBeaconEvent(params) → UnsignedEvent
107
+ - KINDS — { groupState: 30078, signal: 20078, giftWrap: 1059 }
108
+ - buildGroupStateEvent(params) → UnsignedEvent — kind 30078, d-tag `ssg/<groupId>`, p-tags for members, NIP-32 labels
109
+ - buildStoredSignalEvent(params) → UnsignedEvent — kind 30078, d-tag `ssg/<SHA256(groupId)>:<signalType>`, 7-day expiration
110
+ - buildSignalEvent(params) → UnsignedEvent — kind 20078 ephemeral, d-tag `ssg/<SHA256(groupId)>`, t-tag for signal type
111
+ - buildRumourEvent(params) → UnsignedEvent — kind 14 rumour for NIP-17 gift wrapping (seed distribution, reseed, member updates)
112
+ - hashGroupId(groupId) → string — SHA-256 hash a group ID for privacy-preserving d-tags
112
113
 
113
114
  ### canary-kit/beacon
114
115
 
115
- AES-256-GCM encrypted location beacons and duress alerts for Nostr kind 20800 events.
116
+ AES-256-GCM encrypted location beacons and duress alerts. Beacons are published as kind 20078 ephemeral signal events.
116
117
 
117
118
  - deriveBeaconKey(seedHex) → Uint8Array
118
119
  - encryptBeacon(key, geohash, precision) → Promise\<string\>
@@ -125,18 +126,23 @@ Types: BeaconPayload ({ geohash, precision, timestamp }), DuressAlert ({ type, m
125
126
 
126
127
  ### canary-kit/sync
127
128
 
128
- Transport-agnostic group state synchronisation. Authority model with 6 invariants, replay protection, epoch boundaries.
129
+ Transport-agnostic group state synchronisation. Authority model with 6 invariants, replay protection, epoch boundaries. See COOKBOOK.md for complete workflow examples.
129
130
 
130
- - decodeSyncMessage(json) → SyncMessage (validates and parses)
131
- - encodeSyncMessage(msg) → string (serialises to JSON)
132
- - applySyncMessage(state, msg, sender?) → SyncResult ({ state, applied, reason? })
131
+ - decodeSyncMessage(json) → SyncMessage (validates and parses; throws on invalid input)
132
+ - encodeSyncMessage(msg) → string (serialises to JSON; injects protocolVersion)
133
+ - applySyncMessage(state, msg, nowSec?, sender?) → GroupState (apply a sync message; returns same reference if rejected)
134
+ - applySyncMessageWithResult(state, msg, nowSec?, sender?) → SyncApplyResult ({ state, applied }) (same as above but indicates acceptance)
133
135
  - deriveGroupKey(seed) → Uint8Array (AES-256-GCM group key)
134
- - deriveGroupSigningKey(seed) → Uint8Array (HMAC signing key)
136
+ - deriveGroupIdentity(seed) → Uint8Array (group identity key)
135
137
  - hashGroupTag(groupId) → string (privacy-preserving group tag)
136
138
  - encryptEnvelope(key, plaintext) → Promise<string> (AES-256-GCM)
137
139
  - decryptEnvelope(key, ciphertext) → Promise<string> (AES-256-GCM)
138
140
 
139
- Message types: member-join, member-leave, counter-advance, reseed, beacon, duress-alert, liveness-checkin, state-snapshot
141
+ 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.
142
+
143
+ Message types: member-join, member-leave, counter-advance, reseed, beacon, duress-alert, duress-clear, liveness-checkin, state-snapshot
144
+
145
+ Types: SyncApplyResult ({ state: GroupState, applied: boolean }), SyncTransport (interface for pluggable transports), EventSigner (interface for Nostr signing)
140
146
 
141
147
  ## Quick Examples
142
148
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canary-kit",
3
- "version": "2.6.2",
3
+ "version": "2.7.1",
4
4
  "description": "Deepfake-proof identity verification. Open protocol, minimal dependencies.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -46,7 +46,9 @@
46
46
  "files": [
47
47
  "dist/",
48
48
  "LICENSE",
49
+ "API.md",
49
50
  "CANARY.md",
51
+ "COOKBOOK.md",
50
52
  "NIP-CANARY.md",
51
53
  "INTEGRATION.md",
52
54
  "SECURITY.md",
@@ -143,5 +145,9 @@
143
145
  "@scure/bip39": "^2.0.1",
144
146
  "nsec-tree": "^1.4.0",
145
147
  "spoken-token": "^2.0.2"
148
+ },
149
+ "funding": {
150
+ "type": "lightning",
151
+ "url": "lightning:thedonkey@strike.me"
146
152
  }
147
153
  }