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 +306 -0
- package/COOKBOOK.md +763 -0
- package/INTEGRATION.md +329 -3
- package/README.md +25 -9
- package/SECURITY.md +2 -1
- package/dist/beacon.d.ts +2 -2
- package/dist/beacon.d.ts.map +1 -1
- package/llms-full.txt +170 -106
- package/llms.txt +24 -18
- package/package.json +7 -1
package/INTEGRATION.md
CHANGED
|
@@ -367,19 +367,122 @@ authorisation:
|
|
|
367
367
|
|
|
368
368
|
CANARY tokens rotate on a time-based counter (P4: Replay Resistance). For
|
|
369
369
|
transaction-specific dynamic linking (required by FCA for remote electronic
|
|
370
|
-
payments), construct the session namespace or counter from transaction parameters
|
|
370
|
+
payments), construct the session namespace or counter from transaction parameters.
|
|
371
|
+
|
|
372
|
+
#### Full FCA SCA implementation
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
import { createSession, deriveSeed, generateSeed } from 'canary-kit/session'
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
**Step 1: Server-side seed derivation (backend, runs once per customer)**
|
|
371
379
|
|
|
372
380
|
```typescript
|
|
381
|
+
// The master key lives in an HSM — never in application code
|
|
382
|
+
// deriveSeed uses HMAC-SHA256 with null-byte-separated components
|
|
383
|
+
const customerSeed = deriveSeed(
|
|
384
|
+
hsmMasterKey, // Uint8Array from HSM (min 16 bytes)
|
|
385
|
+
customerId, // e.g. 'CUST-2026-00491'
|
|
386
|
+
seedVersion.toString(), // increment to rotate without re-enrolment
|
|
387
|
+
)
|
|
388
|
+
// Deliver customerSeed to the customer's app over TLS during login
|
|
389
|
+
// Store in iOS Keychain / Android KeyStore (possession factor)
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
**Step 2: Standard call verification (agent side)**
|
|
393
|
+
|
|
394
|
+
```typescript
|
|
395
|
+
// Agent's desktop app — seed was derived from the same master_key + customer_id
|
|
373
396
|
const session = createSession({
|
|
374
|
-
secret:
|
|
375
|
-
namespace:
|
|
397
|
+
secret: customerSeed,
|
|
398
|
+
namespace: 'aviva',
|
|
399
|
+
roles: ['caller', 'agent'],
|
|
400
|
+
myRole: 'agent',
|
|
401
|
+
preset: 'call', // 30-second rotation, 1 word, ±1 tolerance
|
|
402
|
+
theirIdentity: customerId,
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
// Agent asks: "What's your verification word?"
|
|
406
|
+
// Customer speaks their word from the app
|
|
407
|
+
const result = session.verify(spokenWord)
|
|
408
|
+
|
|
409
|
+
switch (result.status) {
|
|
410
|
+
case 'valid':
|
|
411
|
+
// Customer identity confirmed — two SCA factors satisfied:
|
|
412
|
+
// Knowledge: customer knows the seed (derives the correct word)
|
|
413
|
+
// Possession: seed stored on their device behind biometrics
|
|
414
|
+
const agentWord = session.myToken()
|
|
415
|
+
// Agent reads aloud: "Your confirmation word is: [agentWord]"
|
|
416
|
+
// Customer verifies on their app — bidirectional authentication
|
|
417
|
+
break
|
|
418
|
+
|
|
419
|
+
case 'duress':
|
|
420
|
+
// Customer spoke their duress word — they are under coercion
|
|
421
|
+
showVerified() // maintain plausible deniability
|
|
422
|
+
triggerDuressProtocol(result.identities) // silent security alert
|
|
423
|
+
break
|
|
424
|
+
|
|
425
|
+
case 'invalid':
|
|
426
|
+
// Word does not match — escalate to manual identity checks
|
|
427
|
+
escalateToSecurity()
|
|
428
|
+
break
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
**Step 3: Transaction-specific dynamic linking (FCA requirement)**
|
|
433
|
+
|
|
434
|
+
FCA SCA requires authentication codes to be dynamically linked to the specific
|
|
435
|
+
transaction amount and payee for remote electronic payments. Encode transaction
|
|
436
|
+
parameters into the session namespace:
|
|
437
|
+
|
|
438
|
+
```typescript
|
|
439
|
+
// Payment: £1,250.00 to payee 'ACME-CORP'
|
|
440
|
+
const paymentSession = createSession({
|
|
441
|
+
secret: customerSeed,
|
|
442
|
+
namespace: `aviva:payment:ACME-CORP:125000`, // payee + amount in pence
|
|
376
443
|
roles: ['caller', 'agent'],
|
|
377
444
|
myRole: 'agent',
|
|
378
445
|
preset: 'call',
|
|
379
446
|
theirIdentity: customerId,
|
|
380
447
|
})
|
|
448
|
+
|
|
449
|
+
// This session produces different words from the standard session —
|
|
450
|
+
// the token is cryptographically bound to the transaction parameters.
|
|
451
|
+
// Changing the amount or payee produces a different word.
|
|
452
|
+
const paymentWord = paymentSession.myToken()
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
**Step 4: Customer-side implementation (mobile app)**
|
|
456
|
+
|
|
457
|
+
```typescript
|
|
458
|
+
// Customer's app — same seed, same namespace, opposite role
|
|
459
|
+
const customerSession = createSession({
|
|
460
|
+
secret: customerSeed, // retrieved from Keychain/KeyStore
|
|
461
|
+
namespace: 'aviva',
|
|
462
|
+
roles: ['caller', 'agent'],
|
|
463
|
+
myRole: 'caller', // customer is the caller
|
|
464
|
+
preset: 'call',
|
|
465
|
+
theirIdentity: 'aviva-agent',
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
// App displays:
|
|
469
|
+
// "Your word: [customerSession.myToken()]" — speak this to the agent
|
|
470
|
+
// "Expect to hear: [customerSession.theirToken()]" — agent should say this
|
|
471
|
+
// Countdown bar showing seconds until rotation
|
|
381
472
|
```
|
|
382
473
|
|
|
474
|
+
**Replacing SMS OTP with CANARY**
|
|
475
|
+
|
|
476
|
+
| Property | SMS OTP | CANARY |
|
|
477
|
+
|----------|---------|--------|
|
|
478
|
+
| Factor type | Possession only (SIM) | Knowledge + Possession |
|
|
479
|
+
| Phishing resistance | None (code can be relayed) | High (HMAC-derived, no transmittable code) |
|
|
480
|
+
| Offline capable | No (requires SMS delivery) | Yes (derived locally after initial sync) |
|
|
481
|
+
| Bidirectional | No (institution never proves identity) | Yes (mutual verification) |
|
|
482
|
+
| Coercion resistance | None | Duress word triggers silent alert |
|
|
483
|
+
| SIM-swap vulnerability | Critical | None (seed in device secure storage) |
|
|
484
|
+
| Dynamic linking | Separate mechanism needed | Built into namespace construction |
|
|
485
|
+
|
|
383
486
|
### RBI Digital Payment Authentication (India)
|
|
384
487
|
|
|
385
488
|
Pattern 1 satisfies the RBI's two-factor authentication requirement for digital
|
|
@@ -634,6 +737,229 @@ Nostr relays. The vault is a single JSON blob containing all group states,
|
|
|
634
737
|
encrypted with the user's own keypair, published as a kind 30078 event with
|
|
635
738
|
a `d` tag of `canary:vault`.
|
|
636
739
|
|
|
740
|
+
## Distributed Deployment Architecture
|
|
741
|
+
|
|
742
|
+
When deploying CANARY across multiple nodes (multi-region call centres, redundant
|
|
743
|
+
backend services, or clustered enterprise systems), verification state must be
|
|
744
|
+
synchronised without compromising the protocol's deepfake-proof guarantees.
|
|
745
|
+
|
|
746
|
+
### Architecture overview
|
|
747
|
+
|
|
748
|
+
```
|
|
749
|
+
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
|
750
|
+
│ Node A │ │ Node B │ │ Node C │
|
|
751
|
+
│ (London) │ │ (Mumbai) │ │ (Dubai) │
|
|
752
|
+
│ │ │ │ │ │
|
|
753
|
+
│ GroupState │◄───►│ GroupState │◄───►│ GroupState │
|
|
754
|
+
│ applySyncMsg │ │ applySyncMsg │ │ applySyncMsg │
|
|
755
|
+
│ │ │ │ │ │
|
|
756
|
+
│ Verify ✓ │ │ Verify ✓ │ │ Verify ✓ │
|
|
757
|
+
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
|
|
758
|
+
│ │ │
|
|
759
|
+
└────────────────────┼────────────────────┘
|
|
760
|
+
│
|
|
761
|
+
┌───────┴───────┐
|
|
762
|
+
│ Nostr Relays │
|
|
763
|
+
│ (sync layer) │
|
|
764
|
+
└───────────────┘
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
### Key principle: verification is stateless, sync is not
|
|
768
|
+
|
|
769
|
+
Verification word derivation (`deriveVerificationWord`, `deriveToken`,
|
|
770
|
+
`session.myToken()`) is a pure function of `(seed, counter)`. Any node with the
|
|
771
|
+
seed can derive the correct word independently. No network round-trip needed.
|
|
772
|
+
|
|
773
|
+
State synchronisation is only required for:
|
|
774
|
+
- **Counter advances** (burn-after-use rotation)
|
|
775
|
+
- **Member changes** (join/leave triggers reseed)
|
|
776
|
+
- **Reseed events** (new seed distribution)
|
|
777
|
+
|
|
778
|
+
This means nodes can verify callers even during network partitions. Sync is
|
|
779
|
+
an optimisation for counter freshness, not a requirement for verification.
|
|
780
|
+
|
|
781
|
+
### Conflict resolution via authority invariants
|
|
782
|
+
|
|
783
|
+
The sync protocol enforces six invariants (I1-I6) that guarantee convergence:
|
|
784
|
+
|
|
785
|
+
```typescript
|
|
786
|
+
import {
|
|
787
|
+
applySyncMessage,
|
|
788
|
+
decodeSyncMessage,
|
|
789
|
+
decryptEnvelope,
|
|
790
|
+
deriveGroupKey,
|
|
791
|
+
type SyncMessage,
|
|
792
|
+
} from 'canary-kit/sync'
|
|
793
|
+
|
|
794
|
+
// Each node maintains its own GroupState and applies messages identically
|
|
795
|
+
function handleIncomingSync(
|
|
796
|
+
localState: GroupState,
|
|
797
|
+
encryptedPayload: string,
|
|
798
|
+
senderPubkey: string,
|
|
799
|
+
): GroupState {
|
|
800
|
+
const groupKey = deriveGroupKey(localState.seed)
|
|
801
|
+
const decrypted = await decryptEnvelope(groupKey, encryptedPayload)
|
|
802
|
+
const msg = decodeSyncMessage(decrypted)
|
|
803
|
+
const nowSec = Math.floor(Date.now() / 1000)
|
|
804
|
+
|
|
805
|
+
// applySyncMessage enforces all invariants:
|
|
806
|
+
// I1: sender must be in admins (for privileged actions)
|
|
807
|
+
// I2: opId must not be consumed (replay protection)
|
|
808
|
+
// I3: epoch must match local epoch (non-reseed ops)
|
|
809
|
+
// I4: reseed epoch must be local.epoch + 1
|
|
810
|
+
// I5: snapshot epoch must be >= local epoch
|
|
811
|
+
// I6: stale epoch messages are dropped
|
|
812
|
+
return applySyncMessage(localState, msg, nowSec, senderPubkey)
|
|
813
|
+
}
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
### Eventual consistency model
|
|
817
|
+
|
|
818
|
+
CANARY's sync protocol guarantees **eventual consistency** across nodes:
|
|
819
|
+
|
|
820
|
+
| Scenario | Resolution |
|
|
821
|
+
|----------|------------|
|
|
822
|
+
| Two nodes advance counter simultaneously | Monotonic rule: `effective(incoming) > effective(local)` — highest wins, lower is no-op |
|
|
823
|
+
| Node misses a counter advance | Next advance carries the latest counter; stale node catches up |
|
|
824
|
+
| Node misses a reseed | Stale node rejects subsequent messages (epoch mismatch I3); admin must re-invite |
|
|
825
|
+
| Network partition heals | Nodes exchange messages; invariant checks accept valid, reject stale |
|
|
826
|
+
| Duplicate message delivery | opId replay guard (I2) silently drops duplicates |
|
|
827
|
+
| Clock skew between nodes | ±60 second future skew tolerance; counter derived from `floor(time / interval)` absorbs drift |
|
|
828
|
+
|
|
829
|
+
### Multi-node sync implementation
|
|
830
|
+
|
|
831
|
+
```typescript
|
|
832
|
+
import {
|
|
833
|
+
encodeSyncMessage,
|
|
834
|
+
applySyncMessage,
|
|
835
|
+
applySyncMessageWithResult,
|
|
836
|
+
encryptEnvelope,
|
|
837
|
+
decryptEnvelope,
|
|
838
|
+
deriveGroupKey,
|
|
839
|
+
STORED_MESSAGE_TYPES,
|
|
840
|
+
} from 'canary-kit/sync'
|
|
841
|
+
import { buildSignalEvent, buildStoredSignalEvent } from 'canary-kit/nostr'
|
|
842
|
+
|
|
843
|
+
// Publish a state change to all nodes via Nostr relay
|
|
844
|
+
async function broadcastSync(
|
|
845
|
+
group: GroupState,
|
|
846
|
+
msg: SyncMessage,
|
|
847
|
+
signer: EventSigner,
|
|
848
|
+
): Promise<void> {
|
|
849
|
+
const groupKey = deriveGroupKey(group.seed)
|
|
850
|
+
const encrypted = await encryptEnvelope(groupKey, encodeSyncMessage(msg))
|
|
851
|
+
|
|
852
|
+
// Stored messages (member changes, reseeds) use kind 30078
|
|
853
|
+
// so offline nodes receive them when they reconnect.
|
|
854
|
+
// Ephemeral messages (beacons, liveness) use kind 20078.
|
|
855
|
+
const event = STORED_MESSAGE_TYPES.has(msg.type)
|
|
856
|
+
? buildStoredSignalEvent({
|
|
857
|
+
groupId: group.name,
|
|
858
|
+
signalType: msg.type,
|
|
859
|
+
encryptedContent: encrypted,
|
|
860
|
+
})
|
|
861
|
+
: buildSignalEvent({
|
|
862
|
+
groupId: group.name,
|
|
863
|
+
signalType: msg.type,
|
|
864
|
+
encryptedContent: encrypted,
|
|
865
|
+
})
|
|
866
|
+
|
|
867
|
+
const signed = await signer.sign({ ...event, pubkey: signer.pubkey })
|
|
868
|
+
await relay.publish(signed)
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// Receive and apply sync messages from other nodes
|
|
872
|
+
function onSyncMessage(
|
|
873
|
+
localState: GroupState,
|
|
874
|
+
encryptedPayload: string,
|
|
875
|
+
senderPubkey: string,
|
|
876
|
+
): { state: GroupState; applied: boolean } {
|
|
877
|
+
const groupKey = deriveGroupKey(localState.seed)
|
|
878
|
+
const decrypted = await decryptEnvelope(groupKey, encryptedPayload)
|
|
879
|
+
const msg = decodeSyncMessage(decrypted)
|
|
880
|
+
const nowSec = Math.floor(Date.now() / 1000)
|
|
881
|
+
|
|
882
|
+
// applySyncMessageWithResult tells you whether the message was applied
|
|
883
|
+
// or silently rejected by invariant checks — useful for logging
|
|
884
|
+
const result = applySyncMessageWithResult(localState, msg, nowSec, senderPubkey)
|
|
885
|
+
|
|
886
|
+
if (!result.applied) {
|
|
887
|
+
// Message rejected — log for debugging (never log seed material)
|
|
888
|
+
auditLog.warn('sync_rejected', {
|
|
889
|
+
type: msg.type,
|
|
890
|
+
epoch: (msg as { epoch?: number }).epoch,
|
|
891
|
+
localEpoch: localState.epoch,
|
|
892
|
+
sender: senderPubkey,
|
|
893
|
+
})
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
return result
|
|
897
|
+
}
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
### Handling network partitions
|
|
901
|
+
|
|
902
|
+
During a partition, each node continues to derive and verify words locally.
|
|
903
|
+
When the partition heals:
|
|
904
|
+
|
|
905
|
+
1. **Counter advances** — the monotonic rule resolves ordering automatically.
|
|
906
|
+
Only the highest effective counter is kept; lower values are no-ops.
|
|
907
|
+
|
|
908
|
+
2. **Member changes during partition** — if an admin added/removed members while
|
|
909
|
+
a node was partitioned, the node will reject post-reseed messages (epoch
|
|
910
|
+
mismatch). The admin must send a `state-snapshot` to catch up partitioned
|
|
911
|
+
nodes within the same epoch, or re-invite for cross-epoch recovery.
|
|
912
|
+
|
|
913
|
+
3. **Verification during partition** — works normally. Words derive from
|
|
914
|
+
`(seed, counter)` which both sides share. The only risk is counter drift
|
|
915
|
+
if one side burned words (burn-after-use) while the other didn't. The
|
|
916
|
+
tolerance window (`±tolerance` counter values) absorbs small drift.
|
|
917
|
+
|
|
918
|
+
### Error handling
|
|
919
|
+
|
|
920
|
+
```typescript
|
|
921
|
+
import { decodeSyncMessage } from 'canary-kit/sync'
|
|
922
|
+
|
|
923
|
+
// decodeSyncMessage validates all fields and rejects malformed messages
|
|
924
|
+
try {
|
|
925
|
+
const msg = decodeSyncMessage(payload)
|
|
926
|
+
} catch (err) {
|
|
927
|
+
// Common rejection reasons:
|
|
928
|
+
// 'Unsupported protocol version' — sender is on a different protocol version
|
|
929
|
+
// 'Invalid sync message type' — unknown message type (forward compatibility)
|
|
930
|
+
// 'not valid JSON' — corrupted or tampered payload
|
|
931
|
+
// All are safe to log and discard
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// applySyncMessage returns unchanged state on rejection (fail-closed)
|
|
935
|
+
// Use applySyncMessageWithResult to distinguish applied from rejected
|
|
936
|
+
const { state, applied } = applySyncMessageWithResult(group, msg, nowSec, sender)
|
|
937
|
+
if (!applied) {
|
|
938
|
+
// Invariant violation — message is either:
|
|
939
|
+
// - From a non-admin (I1)
|
|
940
|
+
// - Replayed opId (I2)
|
|
941
|
+
// - Wrong epoch (I3/I4/I6)
|
|
942
|
+
// - Stale counter (monotonic check)
|
|
943
|
+
// Safe to discard. Do not retry.
|
|
944
|
+
}
|
|
945
|
+
```
|
|
946
|
+
|
|
947
|
+
### Deepfake-proof guarantees in distributed deployments
|
|
948
|
+
|
|
949
|
+
The distributed architecture preserves all CANARY security properties:
|
|
950
|
+
|
|
951
|
+
| Property | How it's preserved across nodes |
|
|
952
|
+
|----------|-------------------------------|
|
|
953
|
+
| P1: Token Unpredictability | Each node derives tokens from the same `(seed, counter)` — pure HMAC-SHA256 |
|
|
954
|
+
| P2: Duress Indistinguishability | Duress derivation is local; duress alerts propagate via encrypted sync |
|
|
955
|
+
| P4: Replay Resistance | Counter monotonicity enforced by `applySyncMessage`; opId deduplication prevents replay |
|
|
956
|
+
| P5: Coercion Resistance | Duress signals broadcast to all nodes via sync; all nodes alert security teams |
|
|
957
|
+
| P6: Liveness Guarantee | Liveness heartbeats are fire-and-forget; freshness gate (300s) prevents stale replay |
|
|
958
|
+
| P8: Timing Safety | Constant-time string comparison (`timingSafeStringEqual`) on every node |
|
|
959
|
+
|
|
960
|
+
The sync layer is a consistency optimisation. The security properties are
|
|
961
|
+
properties of the cryptographic derivation, not the transport.
|
|
962
|
+
|
|
637
963
|
## Licence
|
|
638
964
|
|
|
639
965
|
MIT — same as canary-kit.
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# canary-kit
|
|
2
2
|
|
|
3
|
+
**Nostr:** [`npub1mgvlrnf5hm9yf0n5mf9nqmvarhvxkc6remu5ec3vf8r0txqkuk7su0e7q2`](https://njump.me/npub1mgvlrnf5hm9yf0n5mf9nqmvarhvxkc6remu5ec3vf8r0txqkuk7su0e7q2)
|
|
4
|
+
|
|
3
5
|
> Deepfake-proof identity verification. Open protocol, minimal dependencies.
|
|
4
6
|
|
|
5
7
|
[](https://www.npmjs.com/package/canary-kit)
|
|
@@ -148,7 +150,7 @@ import { createSession } from 'canary-kit/session' // just sessions
|
|
|
148
150
|
import { deriveToken } from 'canary-kit/token' // just derivation
|
|
149
151
|
import { encodeAsWords } from 'canary-kit/encoding' // just encoding
|
|
150
152
|
import { WORDLIST } from 'canary-kit/wordlist' // just the wordlist
|
|
151
|
-
import {
|
|
153
|
+
import { buildGroupStateEvent } from 'canary-kit/nostr' // just Nostr
|
|
152
154
|
import { encryptBeacon } from 'canary-kit/beacon' // just beacons
|
|
153
155
|
import { applySyncMessage } from 'canary-kit/sync' // just sync protocol
|
|
154
156
|
```
|
|
@@ -172,7 +174,7 @@ See [SECURITY.md](SECURITY.md) for vulnerability disclosure and known limitation
|
|
|
172
174
|
| `canary-kit/token` | `deriveToken`, `verifyToken`, `deriveDuressToken`, `deriveLivenessToken` |
|
|
173
175
|
| `canary-kit/encoding` | `encodeAsWords`, `encodeAsPin`, `encodeAsHex` |
|
|
174
176
|
| `canary-kit` | `createGroup`, `getCurrentWord`, `verifyWord`, `addMember`, `reseed` |
|
|
175
|
-
| `canary-kit/nostr` | `
|
|
177
|
+
| `canary-kit/nostr` | `buildGroupStateEvent`, `buildSignalEvent`, `buildStoredSignalEvent`, `buildRumourEvent` |
|
|
176
178
|
| `canary-kit/beacon` | `encryptBeacon`, `decryptBeacon`, `buildDuressAlert` |
|
|
177
179
|
| `canary-kit/sync` | `applySyncMessage`, `encodeSyncMessage`, `deriveGroupKey` |
|
|
178
180
|
| `canary-kit/wordlist` | `WORDLIST`, `getWord`, `indexOf` |
|
|
@@ -185,14 +187,11 @@ The full protocol specification is in [CANARY.md](CANARY.md). The Nostr binding
|
|
|
185
187
|
|
|
186
188
|
| Event | Kind | Type |
|
|
187
189
|
|---|---|---|
|
|
188
|
-
| Group
|
|
189
|
-
|
|
|
190
|
-
|
|
|
191
|
-
| Reseed | `28801` | Ephemeral |
|
|
192
|
-
| Word used | `28802` | Ephemeral |
|
|
193
|
-
| Encrypted location beacon | `20800` | Ephemeral |
|
|
190
|
+
| Group state / stored signals | `30078` | Parameterised replaceable |
|
|
191
|
+
| Real-time signals | `20078` | Ephemeral |
|
|
192
|
+
| Seed distribution / member updates | `14` → `1059` | NIP-17 gift wrap (kind 14 rumour sealed + wrapped) |
|
|
194
193
|
|
|
195
|
-
Content is encrypted with **NIP-44**. Events may carry a **NIP-40** `expiration` tag.
|
|
194
|
+
Content is encrypted with **NIP-44**. Group state events use the `ssg/` d-tag namespace. Seed distribution and member updates use **NIP-17** gift wrapping (kind 14 rumour → kind 13 seal → kind 1059 gift wrap). Events may carry a **NIP-40** `expiration` tag.
|
|
196
195
|
|
|
197
196
|
## For AI Assistants
|
|
198
197
|
|
|
@@ -208,6 +207,23 @@ If you find canary-kit useful, consider sending a tip:
|
|
|
208
207
|
- **Lightning:** `thedonkey@strike.me`
|
|
209
208
|
- **Nostr zaps:** `npub1mgvlrnf5hm9yf0n5mf9nqmvarhvxkc6remu5ec3vf8r0txqkuk7su0e7q2`
|
|
210
209
|
|
|
210
|
+
## Part of the ForgeSworn Toolkit
|
|
211
|
+
|
|
212
|
+
[ForgeSworn](https://forgesworn.dev) builds open-source cryptographic identity, payments, and coordination tools for Nostr.
|
|
213
|
+
|
|
214
|
+
| Library | What it does |
|
|
215
|
+
|---------|-------------|
|
|
216
|
+
| [nsec-tree](https://github.com/forgesworn/nsec-tree) | Deterministic sub-identity derivation |
|
|
217
|
+
| [ring-sig](https://github.com/forgesworn/ring-sig) | SAG/LSAG ring signatures on secp256k1 |
|
|
218
|
+
| [range-proof](https://github.com/forgesworn/range-proof) | Pedersen commitment range proofs |
|
|
219
|
+
| [canary-kit](https://github.com/forgesworn/canary-kit) | Coercion-resistant spoken verification |
|
|
220
|
+
| [spoken-token](https://github.com/forgesworn/spoken-token) | Human-speakable verification tokens |
|
|
221
|
+
| [toll-booth](https://github.com/forgesworn/toll-booth) | L402 payment middleware |
|
|
222
|
+
| [geohash-kit](https://github.com/forgesworn/geohash-kit) | Geohash toolkit with polygon coverage |
|
|
223
|
+
| [nostr-attestations](https://github.com/forgesworn/nostr-attestations) | NIP-VA verifiable attestations |
|
|
224
|
+
| [dominion](https://github.com/forgesworn/dominion) | Epoch-based encrypted access control |
|
|
225
|
+
| [nostr-veil](https://github.com/forgesworn/nostr-veil) | Privacy-preserving Web of Trust |
|
|
226
|
+
|
|
211
227
|
## Licence
|
|
212
228
|
|
|
213
229
|
MIT
|
package/SECURITY.md
CHANGED
package/dist/beacon.d.ts
CHANGED
|
@@ -27,7 +27,7 @@ export declare function deriveBeaconKey(seedHex: string): Uint8Array;
|
|
|
27
27
|
* @throws {Error} If seedHex is not a valid 64-character hex string.
|
|
28
28
|
*/
|
|
29
29
|
export declare function deriveDuressKey(seedHex: string): Uint8Array;
|
|
30
|
-
/** Decrypted content of
|
|
30
|
+
/** Decrypted content of an encrypted location beacon (kind 20078 signal). */
|
|
31
31
|
export interface BeaconPayload {
|
|
32
32
|
geohash: string;
|
|
33
33
|
precision: number;
|
|
@@ -56,7 +56,7 @@ export declare function encryptBeacon(key: Uint8Array, geohash: string, precisio
|
|
|
56
56
|
export declare function decryptBeacon(key: Uint8Array, content: string): Promise<BeaconPayload>;
|
|
57
57
|
/** Duress propagation scope. */
|
|
58
58
|
export type DuressScope = 'group' | 'persona' | 'master';
|
|
59
|
-
/** Decrypted content of a duress alert beacon (kind
|
|
59
|
+
/** Decrypted content of a duress alert beacon (kind 20078 signal, AES-GCM encrypted). */
|
|
60
60
|
export interface DuressAlert {
|
|
61
61
|
type: 'duress';
|
|
62
62
|
member: string;
|
package/dist/beacon.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"beacon.d.ts","sourceRoot":"","sources":["../src/beacon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AA0BH;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CAG3D;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CAG3D;AA2CD
|
|
1
|
+
{"version":3,"file":"beacon.d.ts","sourceRoot":"","sources":["../src/beacon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AA0BH;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CAG3D;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CAG3D;AA2CD,6EAA6E;AAC7E,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,UAAU,EACf,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,CAAC,CAgBjB;AAED;;;;;;;;GAQG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,UAAU,EACf,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,aAAa,CAAC,CAmBxB;AAMD,gCAAgC;AAChC,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,SAAS,GAAG,QAAQ,CAAA;AAExD,yFAAyF;AACzF,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,QAAQ,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,cAAc,EAAE,QAAQ,GAAG,UAAU,GAAG,MAAM,CAAA;IAC9C,SAAS,EAAE,MAAM,CAAA;IACjB,oGAAoG;IACpG,KAAK,EAAE,WAAW,CAAA;IAClB,qGAAqG;IACrG,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,0EAA0E;AAC1E,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,cAAc,EAAE,QAAQ,GAAG,UAAU,CAAA;CACtC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAC9B,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,cAAc,GAAG,IAAI,EAC/B,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,WAAW,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE,GACxD,WAAW,CAmCb;AAED;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,UAAU,EACf,KAAK,EAAE,WAAW,GACjB,OAAO,CAAC,MAAM,CAAC,CAEjB;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,UAAU,EACf,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,WAAW,CAAC,CA2CtB"}
|