canary-kit 0.9.0
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/CANARY.md +1065 -0
- package/INTEGRATION.md +351 -0
- package/LICENSE +21 -0
- package/NIP-CANARY.md +624 -0
- package/README.md +187 -0
- package/SECURITY.md +92 -0
- package/dist/beacon.d.ts +104 -0
- package/dist/beacon.d.ts.map +1 -0
- package/dist/beacon.js +197 -0
- package/dist/beacon.js.map +1 -0
- package/dist/counter.d.ts +37 -0
- package/dist/counter.d.ts.map +1 -0
- package/dist/counter.js +62 -0
- package/dist/counter.js.map +1 -0
- package/dist/crypto.d.ts +111 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +309 -0
- package/dist/crypto.js.map +1 -0
- package/dist/derive.d.ts +68 -0
- package/dist/derive.d.ts.map +1 -0
- package/dist/derive.js +85 -0
- package/dist/derive.js.map +1 -0
- package/dist/encoding.d.ts +56 -0
- package/dist/encoding.d.ts.map +1 -0
- package/dist/encoding.js +98 -0
- package/dist/encoding.js.map +1 -0
- package/dist/group.d.ts +185 -0
- package/dist/group.d.ts.map +1 -0
- package/dist/group.js +263 -0
- package/dist/group.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/nostr.d.ts +134 -0
- package/dist/nostr.d.ts.map +1 -0
- package/dist/nostr.js +175 -0
- package/dist/nostr.js.map +1 -0
- package/dist/presets.d.ts +26 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/presets.js +39 -0
- package/dist/presets.js.map +1 -0
- package/dist/session.d.ts +114 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +173 -0
- package/dist/session.js.map +1 -0
- package/dist/sync-crypto.d.ts +66 -0
- package/dist/sync-crypto.d.ts.map +1 -0
- package/dist/sync-crypto.js +125 -0
- package/dist/sync-crypto.js.map +1 -0
- package/dist/sync.d.ts +191 -0
- package/dist/sync.d.ts.map +1 -0
- package/dist/sync.js +568 -0
- package/dist/sync.js.map +1 -0
- package/dist/token.d.ts +186 -0
- package/dist/token.d.ts.map +1 -0
- package/dist/token.js +344 -0
- package/dist/token.js.map +1 -0
- package/dist/verify.d.ts +45 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +59 -0
- package/dist/verify.js.map +1 -0
- package/dist/wordlist.d.ts +28 -0
- package/dist/wordlist.d.ts.map +1 -0
- package/dist/wordlist.js +297 -0
- package/dist/wordlist.js.map +1 -0
- package/llms-full.txt +1461 -0
- package/llms.txt +180 -0
- package/package.json +144 -0
package/dist/session.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { hmacSha256, hexToBytes, concatBytes } from './crypto.js';
|
|
2
|
+
import { MAX_TOLERANCE, deriveToken, verifyToken, deriveDirectionalPair, } from './token.js';
|
|
3
|
+
const encoder = new TextEncoder();
|
|
4
|
+
/**
|
|
5
|
+
* Generate a cryptographically secure 256-bit seed.
|
|
6
|
+
* Uses the global `crypto.getRandomValues` (Web Crypto API).
|
|
7
|
+
*
|
|
8
|
+
* @returns A 32-byte Uint8Array containing cryptographically secure random bytes.
|
|
9
|
+
*/
|
|
10
|
+
export function generateSeed() {
|
|
11
|
+
const bytes = new Uint8Array(32);
|
|
12
|
+
crypto.getRandomValues(bytes);
|
|
13
|
+
return bytes;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Derive a seed deterministically from a master key and string components.
|
|
17
|
+
*
|
|
18
|
+
* Algorithm: HMAC-SHA256(masterKey, utf8(components[0]) || 0x00 || utf8(components[1]) || ...)
|
|
19
|
+
*
|
|
20
|
+
* Null-byte separators prevent concatenation ambiguity.
|
|
21
|
+
*
|
|
22
|
+
* @param masterKey - Master key (hex string or Uint8Array, minimum 16 bytes).
|
|
23
|
+
* @param components - One or more string components for domain separation.
|
|
24
|
+
* @returns A deterministic 32-byte seed derived via HMAC-SHA256.
|
|
25
|
+
* @throws {RangeError} If master key is shorter than 16 bytes.
|
|
26
|
+
*/
|
|
27
|
+
export function deriveSeed(masterKey, ...components) {
|
|
28
|
+
const key = typeof masterKey === 'string' ? hexToBytes(masterKey) : masterKey;
|
|
29
|
+
if (key.length < 16) {
|
|
30
|
+
throw new RangeError(`Master key must be at least 16 bytes, got ${key.length}`);
|
|
31
|
+
}
|
|
32
|
+
const parts = [];
|
|
33
|
+
for (let i = 0; i < components.length; i++) {
|
|
34
|
+
if (i > 0)
|
|
35
|
+
parts.push(new Uint8Array([0x00]));
|
|
36
|
+
parts.push(encoder.encode(components[i]));
|
|
37
|
+
}
|
|
38
|
+
return hmacSha256(key, concatBytes(...parts));
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Built-in session presets for directional verification.
|
|
42
|
+
*
|
|
43
|
+
* | Preset | Words | Rotation | Tolerance | Use case |
|
|
44
|
+
* |-----------|-------|------------|-----------|------------------------------|
|
|
45
|
+
* | `call` | 1 | 30 seconds | 1 | Phone verification |
|
|
46
|
+
* | `handoff` | 1 | single-use | 0 | Physical handoff (rideshare) |
|
|
47
|
+
*/
|
|
48
|
+
export const SESSION_PRESETS = Object.freeze({
|
|
49
|
+
call: Object.freeze({
|
|
50
|
+
wordCount: 1,
|
|
51
|
+
rotationSeconds: 30,
|
|
52
|
+
tolerance: 1,
|
|
53
|
+
directional: true,
|
|
54
|
+
description: 'Phone verification for insurance, banking, and call centres. ' +
|
|
55
|
+
'Single word with 30-second rotation. Deepfake-proof — cloning a voice ' +
|
|
56
|
+
'does not help derive the current word.',
|
|
57
|
+
}),
|
|
58
|
+
handoff: Object.freeze({
|
|
59
|
+
wordCount: 1,
|
|
60
|
+
rotationSeconds: 0,
|
|
61
|
+
tolerance: 0,
|
|
62
|
+
directional: true,
|
|
63
|
+
description: 'Physical handoff verification for rideshare, delivery, and task completion. ' +
|
|
64
|
+
'Single-use token per event. No time dependency — counter is the task/event ID.',
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
67
|
+
/**
|
|
68
|
+
* Create a directional verification session.
|
|
69
|
+
*
|
|
70
|
+
* Wraps the low-level token API with role awareness, time management,
|
|
71
|
+
* and optional duress detection.
|
|
72
|
+
*
|
|
73
|
+
* @param config - Session configuration including secret, namespace, roles, and optional preset.
|
|
74
|
+
* @returns A {@link Session} object with `myToken()`, `theirToken()`, `verify()`, `counter()`, and `pair()` methods.
|
|
75
|
+
* @throws {Error} If namespace is empty or contains null bytes, roles are invalid, or myRole is not in roles.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```ts
|
|
79
|
+
* const session = createSession({
|
|
80
|
+
* secret: sharedSeed,
|
|
81
|
+
* namespace: 'aviva',
|
|
82
|
+
* roles: ['caller', 'agent'],
|
|
83
|
+
* myRole: 'agent',
|
|
84
|
+
* preset: 'call',
|
|
85
|
+
* })
|
|
86
|
+
* session.myToken() // word I speak
|
|
87
|
+
* session.theirToken() // word I expect to hear
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
export function createSession(config) {
|
|
91
|
+
const preset = config.preset ? SESSION_PRESETS[config.preset] : undefined;
|
|
92
|
+
const rotationSeconds = config.rotationSeconds ?? preset?.rotationSeconds ?? 30;
|
|
93
|
+
const tolerance = config.tolerance ?? preset?.tolerance ?? 0;
|
|
94
|
+
const wordCount = preset?.wordCount ?? 1;
|
|
95
|
+
const encoding = config.encoding ?? { format: 'words', count: wordCount };
|
|
96
|
+
if (!config.namespace) {
|
|
97
|
+
throw new Error('namespace must be a non-empty string');
|
|
98
|
+
}
|
|
99
|
+
if (config.namespace.includes('\0')) {
|
|
100
|
+
throw new Error('namespace must not contain null bytes');
|
|
101
|
+
}
|
|
102
|
+
if (!config.roles[0] || !config.roles[1]) {
|
|
103
|
+
throw new Error('Both roles must be non-empty strings');
|
|
104
|
+
}
|
|
105
|
+
if (config.roles[0].includes('\0') || config.roles[1].includes('\0')) {
|
|
106
|
+
throw new Error('Roles must not contain null bytes');
|
|
107
|
+
}
|
|
108
|
+
if (config.roles[0] === config.roles[1]) {
|
|
109
|
+
throw new Error(`Roles must be distinct, got ["${config.roles[0]}", "${config.roles[1]}"]`);
|
|
110
|
+
}
|
|
111
|
+
if (config.myRole !== config.roles[0] && config.myRole !== config.roles[1]) {
|
|
112
|
+
throw new Error(`myRole "${config.myRole}" is not one of the configured roles ["${config.roles[0]}", "${config.roles[1]}"]`);
|
|
113
|
+
}
|
|
114
|
+
if (!Number.isInteger(rotationSeconds) || rotationSeconds < 0) {
|
|
115
|
+
throw new RangeError(`rotationSeconds must be a non-negative integer, got ${rotationSeconds}`);
|
|
116
|
+
}
|
|
117
|
+
if (!Number.isInteger(tolerance) || tolerance < 0) {
|
|
118
|
+
throw new RangeError(`tolerance must be a non-negative integer, got ${tolerance}`);
|
|
119
|
+
}
|
|
120
|
+
if (tolerance > MAX_TOLERANCE) {
|
|
121
|
+
throw new RangeError(`tolerance must be <= ${MAX_TOLERANCE}, got ${tolerance}`);
|
|
122
|
+
}
|
|
123
|
+
if (rotationSeconds === 0 && config.counter === undefined) {
|
|
124
|
+
throw new Error('Fixed counter mode (rotationSeconds=0) requires config.counter');
|
|
125
|
+
}
|
|
126
|
+
if (rotationSeconds === 0 && config.counter !== undefined) {
|
|
127
|
+
if (!Number.isInteger(config.counter) || config.counter < 0 || config.counter > 0xFFFFFFFF) {
|
|
128
|
+
throw new RangeError(`counter must be an integer 0–${0xFFFFFFFF}, got ${config.counter}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (rotationSeconds > 0 && config.counter !== undefined) {
|
|
132
|
+
throw new Error('counter must not be set when rotationSeconds > 0 (counter is derived from time)');
|
|
133
|
+
}
|
|
134
|
+
const secret = typeof config.secret === 'string' ? hexToBytes(config.secret) : config.secret;
|
|
135
|
+
const theirRole = config.roles[0] === config.myRole ? config.roles[1] : config.roles[0];
|
|
136
|
+
// Use null-byte separator to match deriveDirectionalPair and prevent
|
|
137
|
+
// concatenation ambiguity (e.g. namespace "a:b" + role "c" vs "a" + "b:c")
|
|
138
|
+
const myContext = `${config.namespace}\0${config.myRole}`;
|
|
139
|
+
const theirContext = `${config.namespace}\0${theirRole}`;
|
|
140
|
+
const isFixedCounter = rotationSeconds === 0;
|
|
141
|
+
function getCounter(nowSec) {
|
|
142
|
+
if (isFixedCounter) {
|
|
143
|
+
if (config.counter === undefined) {
|
|
144
|
+
throw new Error('Fixed counter mode (rotationSeconds=0) requires config.counter');
|
|
145
|
+
}
|
|
146
|
+
return config.counter;
|
|
147
|
+
}
|
|
148
|
+
const t = nowSec ?? Math.floor(Date.now() / 1000);
|
|
149
|
+
return Math.floor(t / rotationSeconds);
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
counter: getCounter,
|
|
153
|
+
myToken(nowSec) {
|
|
154
|
+
return deriveToken(secret, myContext, getCounter(nowSec), encoding);
|
|
155
|
+
},
|
|
156
|
+
theirToken(nowSec) {
|
|
157
|
+
return deriveToken(secret, theirContext, getCounter(nowSec), encoding);
|
|
158
|
+
},
|
|
159
|
+
verify(spoken, nowSec) {
|
|
160
|
+
const identities = [];
|
|
161
|
+
if (config.theirIdentity)
|
|
162
|
+
identities.push(config.theirIdentity);
|
|
163
|
+
return verifyToken(secret, theirContext, getCounter(nowSec), spoken, identities, {
|
|
164
|
+
encoding,
|
|
165
|
+
tolerance,
|
|
166
|
+
});
|
|
167
|
+
},
|
|
168
|
+
pair(nowSec) {
|
|
169
|
+
return deriveDirectionalPair(secret, config.namespace, config.roles, getCounter(nowSec), encoding);
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=session.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACjE,OAAO,EACL,aAAa,EACb,WAAW,EACX,WAAW,EACX,qBAAqB,GAGtB,MAAM,YAAY,CAAA;AAGnB,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAA;AAEjC;;;;;GAKG;AACH,MAAM,UAAU,YAAY;IAC1B,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,EAAE,CAAC,CAAA;IAChC,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,CAAA;IAC7B,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,UAAU,CACxB,SAA8B,EAC9B,GAAG,UAAoB;IAEvB,MAAM,GAAG,GAAG,OAAO,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IAC7E,IAAI,GAAG,CAAC,MAAM,GAAG,EAAE,EAAE,CAAC;QACpB,MAAM,IAAI,UAAU,CAAC,6CAA6C,GAAG,CAAC,MAAM,EAAE,CAAC,CAAA;IACjF,CAAC;IACD,MAAM,KAAK,GAAiB,EAAE,CAAA;IAC9B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3C,IAAI,CAAC,GAAG,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QAC7C,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAC3C,CAAC;IACD,OAAO,UAAU,CAAC,GAAG,EAAE,WAAW,CAAC,GAAG,KAAK,CAAC,CAAC,CAAA;AAC/C,CAAC;AAmBD;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,eAAe,GAAiE,MAAM,CAAC,MAAM,CAAC;IACzG,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC;QAClB,SAAS,EAAE,CAAC;QACZ,eAAe,EAAE,EAAE;QACnB,SAAS,EAAE,CAAC;QACZ,WAAW,EAAE,IAAI;QACjB,WAAW,EACT,+DAA+D;YAC/D,wEAAwE;YACxE,wCAAwC;KAC3C,CAAC;IACF,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC;QACrB,SAAS,EAAE,CAAC;QACZ,eAAe,EAAE,CAAC;QAClB,SAAS,EAAE,CAAC;QACZ,WAAW,EAAE,IAAI;QACjB,WAAW,EACT,8EAA8E;YAC9E,gFAAgF;KACnF,CAAC;CACH,CAAC,CAAA;AA+CF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,aAAa,CAAC,MAAqB;IACjD,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,eAAe,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IACzE,MAAM,eAAe,GAAG,MAAM,CAAC,eAAe,IAAI,MAAM,EAAE,eAAe,IAAI,EAAE,CAAA;IAC/E,MAAM,SAAS,GAAG,MAAM,CAAC,SAAS,IAAI,MAAM,EAAE,SAAS,IAAI,CAAC,CAAA;IAC5D,MAAM,SAAS,GAAG,MAAM,EAAE,SAAS,IAAI,CAAC,CAAA;IACxC,MAAM,QAAQ,GAAkB,MAAM,CAAC,QAAQ,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,CAAA;IAExF,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAA;IACzD,CAAC;IACD,IAAI,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACpC,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAA;IAC1D,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QACzC,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAA;IACzD,CAAC;IACD,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACrE,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAA;IACtD,CAAC;IACD,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QACxC,MAAM,IAAI,KAAK,CAAC,iCAAiC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;IAC7F,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,KAAK,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,MAAM,KAAK,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3E,MAAM,IAAI,KAAK,CAAC,WAAW,MAAM,CAAC,MAAM,0CAA0C,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;IAC9H,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,eAAe,CAAC,IAAI,eAAe,GAAG,CAAC,EAAE,CAAC;QAC9D,MAAM,IAAI,UAAU,CAAC,uDAAuD,eAAe,EAAE,CAAC,CAAA;IAChG,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,SAAS,CAAC,IAAI,SAAS,GAAG,CAAC,EAAE,CAAC;QAClD,MAAM,IAAI,UAAU,CAAC,iDAAiD,SAAS,EAAE,CAAC,CAAA;IACpF,CAAC;IACD,IAAI,SAAS,GAAG,aAAa,EAAE,CAAC;QAC9B,MAAM,IAAI,UAAU,CAAC,wBAAwB,aAAa,SAAS,SAAS,EAAE,CAAC,CAAA;IACjF,CAAC;IACD,IAAI,eAAe,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1D,MAAM,IAAI,KAAK,CAAC,gEAAgE,CAAC,CAAA;IACnF,CAAC;IACD,IAAI,eAAe,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1D,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,GAAG,UAAU,EAAE,CAAC;YAC3F,MAAM,IAAI,UAAU,CAAC,gCAAgC,UAAU,SAAS,MAAM,CAAC,OAAO,EAAE,CAAC,CAAA;QAC3F,CAAC;IACH,CAAC;IACD,IAAI,eAAe,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;QACxD,MAAM,IAAI,KAAK,CAAC,iFAAiF,CAAC,CAAA;IACpG,CAAC;IAED,MAAM,MAAM,GAAG,OAAO,MAAM,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAA;IAC5F,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;IACvF,qEAAqE;IACrE,2EAA2E;IAC3E,MAAM,SAAS,GAAG,GAAG,MAAM,CAAC,SAAS,KAAK,MAAM,CAAC,MAAM,EAAE,CAAA;IACzD,MAAM,YAAY,GAAG,GAAG,MAAM,CAAC,SAAS,KAAK,SAAS,EAAE,CAAA;IAExD,MAAM,cAAc,GAAG,eAAe,KAAK,CAAC,CAAA;IAE5C,SAAS,UAAU,CAAC,MAAe;QACjC,IAAI,cAAc,EAAE,CAAC;YACnB,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;gBACjC,MAAM,IAAI,KAAK,CAAC,gEAAgE,CAAC,CAAA;YACnF,CAAC;YACD,OAAO,MAAM,CAAC,OAAO,CAAA;QACvB,CAAC;QACD,MAAM,CAAC,GAAG,MAAM,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAA;QACjD,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,eAAe,CAAC,CAAA;IACxC,CAAC;IAED,OAAO;QACL,OAAO,EAAE,UAAU;QAEnB,OAAO,CAAC,MAAe;YACrB,OAAO,WAAW,CAAC,MAAM,EAAE,SAAS,EAAE,UAAU,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC,CAAA;QACrE,CAAC;QAED,UAAU,CAAC,MAAe;YACxB,OAAO,WAAW,CAAC,MAAM,EAAE,YAAY,EAAE,UAAU,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC,CAAA;QACxE,CAAC;QAED,MAAM,CAAC,MAAc,EAAE,MAAe;YACpC,MAAM,UAAU,GAAa,EAAE,CAAA;YAC/B,IAAI,MAAM,CAAC,aAAa;gBAAE,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,CAAA;YAC/D,OAAO,WAAW,CAAC,MAAM,EAAE,YAAY,EAAE,UAAU,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE;gBAC/E,QAAQ;gBACR,SAAS;aACV,CAAC,CAAA;QACJ,CAAC;QAED,IAAI,CAAC,MAAe;YAClB,OAAO,qBAAqB,CAAC,MAAM,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,KAAK,EAAE,UAAU,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC,CAAA;QACpG,CAAC;KACF,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Group key derivation and envelope encryption for CANARY sync.
|
|
3
|
+
*
|
|
4
|
+
* All functions are zero-dependency and use only the Web Crypto API (crypto.subtle)
|
|
5
|
+
* together with the pure-JS primitives in ./crypto.ts.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Derive a 32-byte symmetric group key from a seed.
|
|
9
|
+
*
|
|
10
|
+
* `HMAC-SHA256(hex_to_bytes(seed), utf8("canary:sync:key"))`
|
|
11
|
+
*
|
|
12
|
+
* @param seedHex - Group seed as a 64-character lowercase hex string (32 bytes).
|
|
13
|
+
* @returns 32-byte AES-256 group key for envelope encryption.
|
|
14
|
+
* @throws {Error} If seedHex is not a valid 64-character hex string.
|
|
15
|
+
*/
|
|
16
|
+
export declare function deriveGroupKey(seedHex: string): Uint8Array;
|
|
17
|
+
/**
|
|
18
|
+
* Encrypt a plaintext string with AES-256-GCM using the provided group key.
|
|
19
|
+
*
|
|
20
|
+
* Returns `base64(IV || ciphertext || auth_tag)` where IV is a random 12-byte nonce.
|
|
21
|
+
*
|
|
22
|
+
* @param groupKey - 32-byte AES-256 key from {@link deriveGroupKey}.
|
|
23
|
+
* @param plaintext - UTF-8 string to encrypt.
|
|
24
|
+
* @returns Base64-encoded ciphertext (12-byte IV prepended to AES-GCM output).
|
|
25
|
+
* @throws {Error} If groupKey is not 32 bytes.
|
|
26
|
+
*/
|
|
27
|
+
export declare function encryptEnvelope(groupKey: Uint8Array, plaintext: string): Promise<string>;
|
|
28
|
+
/**
|
|
29
|
+
* Decrypt an envelope produced by `encryptEnvelope`.
|
|
30
|
+
*
|
|
31
|
+
* Expects `base64(IV || ciphertext || auth_tag)`.
|
|
32
|
+
* Throws on authentication failure (wrong key or tampered data).
|
|
33
|
+
*
|
|
34
|
+
* @param groupKey - 32-byte AES-256 key from {@link deriveGroupKey}.
|
|
35
|
+
* @param encoded - Base64-encoded ciphertext from {@link encryptEnvelope}.
|
|
36
|
+
* @returns Decrypted plaintext string.
|
|
37
|
+
* @throws {Error} If groupKey is not 32 bytes, data is too short, or decryption fails.
|
|
38
|
+
*/
|
|
39
|
+
export declare function decryptEnvelope(groupKey: Uint8Array, encoded: string): Promise<string>;
|
|
40
|
+
/**
|
|
41
|
+
* Derive a 32-byte signing key for a participant within a group.
|
|
42
|
+
*
|
|
43
|
+
* `HMAC-SHA256(hex_to_bytes(seed), utf8("canary:sync:sign:") || hex_to_bytes(personalPrivkey))`
|
|
44
|
+
*
|
|
45
|
+
* Binding the personal private key ensures that each participant's signing
|
|
46
|
+
* identity is unique within the group, even across reseed events.
|
|
47
|
+
*
|
|
48
|
+
* @param seedHex - Group seed as a 64-character lowercase hex string (32 bytes).
|
|
49
|
+
* @param personalPrivkeyHex - Participant's private key as a 64-character lowercase hex string.
|
|
50
|
+
* @returns 32-byte signing key unique to this participant within this group epoch.
|
|
51
|
+
* @throws {Error} If seedHex or personalPrivkeyHex are not valid 64-character hex strings.
|
|
52
|
+
*/
|
|
53
|
+
export declare function deriveGroupSigningKey(seedHex: string, personalPrivkeyHex: string): Uint8Array;
|
|
54
|
+
/**
|
|
55
|
+
* Hash a group ID to produce a privacy-preserving public tag.
|
|
56
|
+
*
|
|
57
|
+
* `hex(SHA256(utf8(groupId)))` — returns a 64-character lowercase hex string.
|
|
58
|
+
*
|
|
59
|
+
* Publishing the hash rather than the group ID prevents observers from
|
|
60
|
+
* correlating events to a known group name.
|
|
61
|
+
*
|
|
62
|
+
* @param groupId - The group identifier string.
|
|
63
|
+
* @returns 64-character lowercase hex string (SHA-256 hash of the group ID).
|
|
64
|
+
*/
|
|
65
|
+
export declare function hashGroupTag(groupId: string): string;
|
|
66
|
+
//# sourceMappingURL=sync-crypto.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-crypto.d.ts","sourceRoot":"","sources":["../src/sync-crypto.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAmCH;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CAG1D;AAID;;;;;;;;;GASG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAqB9F;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAgC5F;AAID;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,kBAAkB,EAAE,MAAM,GAAG,UAAU,CAO7F;AAID;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEpD"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Group key derivation and envelope encryption for CANARY sync.
|
|
3
|
+
*
|
|
4
|
+
* All functions are zero-dependency and use only the Web Crypto API (crypto.subtle)
|
|
5
|
+
* together with the pure-JS primitives in ./crypto.ts.
|
|
6
|
+
*/
|
|
7
|
+
import { hmacSha256, sha256, hexToBytes, bytesToBase64, base64ToBytes, bytesToHex, concatBytes, } from './crypto.js';
|
|
8
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
9
|
+
/** Encode a UTF-8 string as a Uint8Array. */
|
|
10
|
+
function utf8(str) {
|
|
11
|
+
return new TextEncoder().encode(str);
|
|
12
|
+
}
|
|
13
|
+
const HEX_64_RE = /^[0-9a-f]{64}$/;
|
|
14
|
+
function validateSeedHex(seedHex) {
|
|
15
|
+
if (!HEX_64_RE.test(seedHex)) {
|
|
16
|
+
throw new Error('seedHex must be a 64-character lowercase hex string (32 bytes)');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function validateAesKey(key) {
|
|
20
|
+
if (key.length !== 32) {
|
|
21
|
+
throw new Error('AES-256-GCM requires a 32-byte key');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// ── Task 1: Group key derivation ──────────────────────────────────────────────
|
|
25
|
+
/**
|
|
26
|
+
* Derive a 32-byte symmetric group key from a seed.
|
|
27
|
+
*
|
|
28
|
+
* `HMAC-SHA256(hex_to_bytes(seed), utf8("canary:sync:key"))`
|
|
29
|
+
*
|
|
30
|
+
* @param seedHex - Group seed as a 64-character lowercase hex string (32 bytes).
|
|
31
|
+
* @returns 32-byte AES-256 group key for envelope encryption.
|
|
32
|
+
* @throws {Error} If seedHex is not a valid 64-character hex string.
|
|
33
|
+
*/
|
|
34
|
+
export function deriveGroupKey(seedHex) {
|
|
35
|
+
validateSeedHex(seedHex);
|
|
36
|
+
return hmacSha256(hexToBytes(seedHex), utf8('canary:sync:key'));
|
|
37
|
+
}
|
|
38
|
+
// ── Task 1: Envelope encryption ───────────────────────────────────────────────
|
|
39
|
+
/**
|
|
40
|
+
* Encrypt a plaintext string with AES-256-GCM using the provided group key.
|
|
41
|
+
*
|
|
42
|
+
* Returns `base64(IV || ciphertext || auth_tag)` where IV is a random 12-byte nonce.
|
|
43
|
+
*
|
|
44
|
+
* @param groupKey - 32-byte AES-256 key from {@link deriveGroupKey}.
|
|
45
|
+
* @param plaintext - UTF-8 string to encrypt.
|
|
46
|
+
* @returns Base64-encoded ciphertext (12-byte IV prepended to AES-GCM output).
|
|
47
|
+
* @throws {Error} If groupKey is not 32 bytes.
|
|
48
|
+
*/
|
|
49
|
+
export async function encryptEnvelope(groupKey, plaintext) {
|
|
50
|
+
validateAesKey(groupKey);
|
|
51
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
52
|
+
const cryptoKey = await crypto.subtle.importKey('raw', groupKey, { name: 'AES-GCM' }, false, ['encrypt']);
|
|
53
|
+
const ciphertextBuf = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, cryptoKey, utf8(plaintext));
|
|
54
|
+
// Web Crypto returns ciphertext || auth_tag concatenated; prepend IV.
|
|
55
|
+
const combined = concatBytes(iv, new Uint8Array(ciphertextBuf));
|
|
56
|
+
return bytesToBase64(combined);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Decrypt an envelope produced by `encryptEnvelope`.
|
|
60
|
+
*
|
|
61
|
+
* Expects `base64(IV || ciphertext || auth_tag)`.
|
|
62
|
+
* Throws on authentication failure (wrong key or tampered data).
|
|
63
|
+
*
|
|
64
|
+
* @param groupKey - 32-byte AES-256 key from {@link deriveGroupKey}.
|
|
65
|
+
* @param encoded - Base64-encoded ciphertext from {@link encryptEnvelope}.
|
|
66
|
+
* @returns Decrypted plaintext string.
|
|
67
|
+
* @throws {Error} If groupKey is not 32 bytes, data is too short, or decryption fails.
|
|
68
|
+
*/
|
|
69
|
+
export async function decryptEnvelope(groupKey, encoded) {
|
|
70
|
+
validateAesKey(groupKey);
|
|
71
|
+
const combined = base64ToBytes(encoded);
|
|
72
|
+
// 12-byte IV + 16-byte GCM auth tag = 28 bytes minimum (matching beacon.ts)
|
|
73
|
+
if (combined.length < 28) {
|
|
74
|
+
throw new Error('decryptEnvelope: encoded data too short (minimum 28 bytes: 12-byte IV + 16-byte GCM tag)');
|
|
75
|
+
}
|
|
76
|
+
const iv = combined.slice(0, 12);
|
|
77
|
+
const ciphertextWithTag = combined.slice(12);
|
|
78
|
+
const cryptoKey = await crypto.subtle.importKey('raw', groupKey, { name: 'AES-GCM' }, false, ['decrypt']);
|
|
79
|
+
let plaintextBuf;
|
|
80
|
+
try {
|
|
81
|
+
plaintextBuf = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, cryptoKey, ciphertextWithTag);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
throw new Error('decryptEnvelope: decryption failed — wrong key or tampered data');
|
|
85
|
+
}
|
|
86
|
+
return new TextDecoder().decode(plaintextBuf);
|
|
87
|
+
}
|
|
88
|
+
// ── Task 2: Per-group derived signing identity ────────────────────────────────
|
|
89
|
+
/**
|
|
90
|
+
* Derive a 32-byte signing key for a participant within a group.
|
|
91
|
+
*
|
|
92
|
+
* `HMAC-SHA256(hex_to_bytes(seed), utf8("canary:sync:sign:") || hex_to_bytes(personalPrivkey))`
|
|
93
|
+
*
|
|
94
|
+
* Binding the personal private key ensures that each participant's signing
|
|
95
|
+
* identity is unique within the group, even across reseed events.
|
|
96
|
+
*
|
|
97
|
+
* @param seedHex - Group seed as a 64-character lowercase hex string (32 bytes).
|
|
98
|
+
* @param personalPrivkeyHex - Participant's private key as a 64-character lowercase hex string.
|
|
99
|
+
* @returns 32-byte signing key unique to this participant within this group epoch.
|
|
100
|
+
* @throws {Error} If seedHex or personalPrivkeyHex are not valid 64-character hex strings.
|
|
101
|
+
*/
|
|
102
|
+
export function deriveGroupSigningKey(seedHex, personalPrivkeyHex) {
|
|
103
|
+
validateSeedHex(seedHex);
|
|
104
|
+
if (!/^[0-9a-f]{64}$/.test(personalPrivkeyHex)) {
|
|
105
|
+
throw new Error('personalPrivkeyHex must be a 64-character lowercase hex string (32 bytes)');
|
|
106
|
+
}
|
|
107
|
+
const data = concatBytes(utf8('canary:sync:sign:'), hexToBytes(personalPrivkeyHex));
|
|
108
|
+
return hmacSha256(hexToBytes(seedHex), data);
|
|
109
|
+
}
|
|
110
|
+
// ── Task 2: Hashed group tag ──────────────────────────────────────────────────
|
|
111
|
+
/**
|
|
112
|
+
* Hash a group ID to produce a privacy-preserving public tag.
|
|
113
|
+
*
|
|
114
|
+
* `hex(SHA256(utf8(groupId)))` — returns a 64-character lowercase hex string.
|
|
115
|
+
*
|
|
116
|
+
* Publishing the hash rather than the group ID prevents observers from
|
|
117
|
+
* correlating events to a known group name.
|
|
118
|
+
*
|
|
119
|
+
* @param groupId - The group identifier string.
|
|
120
|
+
* @returns 64-character lowercase hex string (SHA-256 hash of the group ID).
|
|
121
|
+
*/
|
|
122
|
+
export function hashGroupTag(groupId) {
|
|
123
|
+
return bytesToHex(sha256(utf8(groupId)));
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=sync-crypto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-crypto.js","sourceRoot":"","sources":["../src/sync-crypto.ts"],"names":[],"mappings":"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"}
|
package/dist/sync.d.ts
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
export { deriveGroupKey, deriveGroupSigningKey, hashGroupTag, encryptEnvelope, decryptEnvelope } from './sync-crypto.js';
|
|
2
|
+
import type { GroupState } from './group.js';
|
|
3
|
+
/** A typed, serialisable description of a group state change. */
|
|
4
|
+
export type SyncMessage = {
|
|
5
|
+
type: 'member-join';
|
|
6
|
+
pubkey: string;
|
|
7
|
+
displayName?: string;
|
|
8
|
+
timestamp: number;
|
|
9
|
+
epoch: number;
|
|
10
|
+
opId: string;
|
|
11
|
+
protocolVersion?: number;
|
|
12
|
+
} | {
|
|
13
|
+
type: 'member-leave';
|
|
14
|
+
pubkey: string;
|
|
15
|
+
timestamp: number;
|
|
16
|
+
epoch: number;
|
|
17
|
+
opId: string;
|
|
18
|
+
protocolVersion?: number;
|
|
19
|
+
} | {
|
|
20
|
+
type: 'counter-advance';
|
|
21
|
+
counter: number;
|
|
22
|
+
usageOffset: number;
|
|
23
|
+
timestamp: number;
|
|
24
|
+
protocolVersion?: number;
|
|
25
|
+
} | {
|
|
26
|
+
type: 'reseed';
|
|
27
|
+
seed: Uint8Array;
|
|
28
|
+
counter: number;
|
|
29
|
+
timestamp: number;
|
|
30
|
+
epoch: number;
|
|
31
|
+
opId: string;
|
|
32
|
+
admins: string[];
|
|
33
|
+
members: string[];
|
|
34
|
+
protocolVersion?: number;
|
|
35
|
+
} | {
|
|
36
|
+
type: 'beacon';
|
|
37
|
+
lat: number;
|
|
38
|
+
lon: number;
|
|
39
|
+
accuracy: number;
|
|
40
|
+
timestamp: number;
|
|
41
|
+
opId: string;
|
|
42
|
+
protocolVersion?: number;
|
|
43
|
+
} | {
|
|
44
|
+
type: 'duress-alert';
|
|
45
|
+
lat: number;
|
|
46
|
+
lon: number;
|
|
47
|
+
timestamp: number;
|
|
48
|
+
opId: string;
|
|
49
|
+
subject?: string;
|
|
50
|
+
protocolVersion?: number;
|
|
51
|
+
} | {
|
|
52
|
+
type: 'duress-clear';
|
|
53
|
+
subject: string;
|
|
54
|
+
timestamp: number;
|
|
55
|
+
opId: string;
|
|
56
|
+
protocolVersion?: number;
|
|
57
|
+
} | {
|
|
58
|
+
type: 'liveness-checkin';
|
|
59
|
+
pubkey: string;
|
|
60
|
+
timestamp: number;
|
|
61
|
+
opId: string;
|
|
62
|
+
protocolVersion?: number;
|
|
63
|
+
}
|
|
64
|
+
/** WARNING: seed is a plaintext hex string. This message type MUST be sent inside an encrypted
|
|
65
|
+
* envelope (NIP-44 or AES-GCM via sync-crypto). Callers MUST NOT log SyncMessage objects
|
|
66
|
+
* containing state-snapshot data, as the seed field would be exposed in plaintext. */
|
|
67
|
+
| {
|
|
68
|
+
type: 'state-snapshot';
|
|
69
|
+
seed: string;
|
|
70
|
+
counter: number;
|
|
71
|
+
usageOffset: number;
|
|
72
|
+
members: string[];
|
|
73
|
+
admins: string[];
|
|
74
|
+
epoch: number;
|
|
75
|
+
opId: string;
|
|
76
|
+
timestamp: number;
|
|
77
|
+
prevEpochSeed?: string;
|
|
78
|
+
protocolVersion?: number;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Message types that mutate group state or are safety-critical.
|
|
82
|
+
* These MUST be delivered to offline devices — use a stored event kind.
|
|
83
|
+
* Fire-and-forget messages (beacons, liveness) use ephemeral kinds.
|
|
84
|
+
*/
|
|
85
|
+
export declare const STORED_MESSAGE_TYPES: Set<string>;
|
|
86
|
+
/** Maximum age (in seconds) for fire-and-forget messages before they are dropped. */
|
|
87
|
+
export declare const FIRE_AND_FORGET_FRESHNESS_SEC = 300;
|
|
88
|
+
/** Maximum allowed future skew (in seconds) for fire-and-forget timestamps. */
|
|
89
|
+
export declare const MAX_FUTURE_SKEW_SEC = 60;
|
|
90
|
+
/** Current protocol version. Bump on any breaking wire format change. */
|
|
91
|
+
export declare const PROTOCOL_VERSION = 2;
|
|
92
|
+
/**
|
|
93
|
+
* Encode a sync message as a JSON string for transport.
|
|
94
|
+
* Binary fields (seed) are hex-encoded for safe JSON round-tripping.
|
|
95
|
+
*
|
|
96
|
+
* @param msg - The sync message to serialise.
|
|
97
|
+
* @returns JSON string with protocolVersion injected and binary fields hex-encoded.
|
|
98
|
+
*/
|
|
99
|
+
export declare function encodeSyncMessage(msg: SyncMessage): string;
|
|
100
|
+
/**
|
|
101
|
+
* Recursively produce a JSON string with sorted keys and no whitespace.
|
|
102
|
+
* Handles nested objects, arrays (elements stringified recursively), and
|
|
103
|
+
* all JSON-safe primitives. Used for deterministic signing (H2).
|
|
104
|
+
*
|
|
105
|
+
* @param value - Any JSON-serialisable value (null, boolean, number, string, array, or plain object).
|
|
106
|
+
* @returns Deterministic JSON string with sorted keys and no whitespace.
|
|
107
|
+
* @throws {Error} If value contains a Uint8Array (must be hex-encoded first) or unsupported type.
|
|
108
|
+
*/
|
|
109
|
+
export declare function stableStringify(value: unknown): string;
|
|
110
|
+
/**
|
|
111
|
+
* Return the canonical string representation of a sync message for signing.
|
|
112
|
+
* Keys are sorted recursively, no whitespace. Binary fields are hex-encoded.
|
|
113
|
+
* The message's protocolVersion field is preserved as-is — the send side
|
|
114
|
+
* (encodeSyncMessage) is responsible for injecting PROTOCOL_VERSION before
|
|
115
|
+
* both encode and sign, so the canonical bytes always reflect the actual
|
|
116
|
+
* wire value. This is the format that inner signatures are computed over (H2).
|
|
117
|
+
*
|
|
118
|
+
* @param msg - The sync message to canonicalise.
|
|
119
|
+
* @returns Deterministic JSON string with sorted keys and no whitespace.
|
|
120
|
+
*/
|
|
121
|
+
export declare function canonicaliseSyncMessage(msg: SyncMessage): string;
|
|
122
|
+
/**
|
|
123
|
+
* Decode a sync message from a JSON string.
|
|
124
|
+
* Throws on invalid or unrecognised messages.
|
|
125
|
+
*
|
|
126
|
+
* @param payload - JSON string to decode.
|
|
127
|
+
* @returns Validated {@link SyncMessage} with correct types (e.g. reseed.seed as Uint8Array).
|
|
128
|
+
* @throws {Error} If JSON is invalid, message type is unrecognised, required fields are missing, or protocolVersion does not match.
|
|
129
|
+
*/
|
|
130
|
+
export declare function decodeSyncMessage(payload: string): SyncMessage;
|
|
131
|
+
/**
|
|
132
|
+
* Apply a sync message to group state.
|
|
133
|
+
* Returns a new GroupState with the change applied.
|
|
134
|
+
* Beacons and duress alerts don't modify group state (fire-and-forget).
|
|
135
|
+
*
|
|
136
|
+
* Authority invariants (I1-I6) are enforced for privileged actions.
|
|
137
|
+
* Privileged actions without a sender are rejected (fail-closed).
|
|
138
|
+
*
|
|
139
|
+
* @param group - Current group state.
|
|
140
|
+
* @param msg - The sync message to apply.
|
|
141
|
+
* @param nowSec - Current unix timestamp in seconds (default: `Date.now() / 1000`).
|
|
142
|
+
* @param sender - Hex pubkey of the message sender (required for privileged actions).
|
|
143
|
+
* @returns New group state with the change applied, or unchanged state if rejected.
|
|
144
|
+
*/
|
|
145
|
+
export declare function applySyncMessage(group: GroupState, msg: SyncMessage, nowSec?: number, sender?: string): GroupState;
|
|
146
|
+
/** Result of applying a sync message, including whether it was accepted or rejected. */
|
|
147
|
+
export interface SyncApplyResult {
|
|
148
|
+
/** The resulting group state (unchanged if rejected). */
|
|
149
|
+
state: GroupState;
|
|
150
|
+
/** Whether the message was applied to the group state. */
|
|
151
|
+
applied: boolean;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Apply a sync message and return a result indicating whether it was accepted.
|
|
155
|
+
*
|
|
156
|
+
* Unlike `applySyncMessage` which silently returns unchanged state on rejection,
|
|
157
|
+
* this function tells the caller whether the message was actually applied —
|
|
158
|
+
* enabling logging, alerting, and debugging of rejected messages.
|
|
159
|
+
*
|
|
160
|
+
* Fire-and-forget messages (beacon, duress-alert, liveness-checkin) always
|
|
161
|
+
* return `applied: true` when they pass freshness checks, even though they
|
|
162
|
+
* don't modify group state.
|
|
163
|
+
*
|
|
164
|
+
* @param group - Current group state.
|
|
165
|
+
* @param msg - The sync message to apply.
|
|
166
|
+
* @param nowSec - Current unix timestamp in seconds (default: `Date.now() / 1000`).
|
|
167
|
+
* @param sender - Hex pubkey of the message sender (required for privileged actions).
|
|
168
|
+
* @returns `{ state, applied }` where `applied` indicates whether the message was accepted.
|
|
169
|
+
*/
|
|
170
|
+
export declare function applySyncMessageWithResult(group: GroupState, msg: SyncMessage, nowSec?: number, sender?: string): SyncApplyResult;
|
|
171
|
+
/** Minimal interface any sync transport must implement. */
|
|
172
|
+
export interface SyncTransport {
|
|
173
|
+
/** Send a sync message to all group members. */
|
|
174
|
+
send(groupId: string, message: SyncMessage, recipients?: string[]): Promise<void>;
|
|
175
|
+
/** Subscribe to incoming messages for a group. Returns an unsubscribe function. */
|
|
176
|
+
subscribe(groupId: string, onMessage: (msg: SyncMessage, sender: string) => void): () => void;
|
|
177
|
+
/** Clean up all connections. */
|
|
178
|
+
disconnect(): void;
|
|
179
|
+
}
|
|
180
|
+
/** Abstracts event signing and NIP-44 encryption for any transport that needs it. */
|
|
181
|
+
export interface EventSigner {
|
|
182
|
+
/** The signer's public key (hex). */
|
|
183
|
+
pubkey: string;
|
|
184
|
+
/** Sign an unsigned event. Parameter and return are `unknown` to avoid coupling to any specific event library's types. */
|
|
185
|
+
sign(event: unknown): Promise<unknown>;
|
|
186
|
+
/** NIP-44 encrypt plaintext for a recipient. */
|
|
187
|
+
encrypt(plaintext: string, recipientPubkey: string): Promise<string>;
|
|
188
|
+
/** NIP-44 decrypt ciphertext from a sender. */
|
|
189
|
+
decrypt(ciphertext: string, senderPubkey: string): Promise<string>;
|
|
190
|
+
}
|
|
191
|
+
//# sourceMappingURL=sync.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../src/sync.ts"],"names":[],"mappings":"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"}
|