@muhkoo/connect 0.14.0-alpha.7 → 0.14.0-alpha.9
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/README.md +62 -12
- package/dist/browser/index.js +7369 -5138
- package/dist/browser/index.js.map +1 -1
- package/dist/connect.d.ts +2194 -1482
- package/dist/connect.workers.d.ts +1188 -0
- package/dist/server/index.js +10153 -8123
- package/dist/server/index.js.map +1 -1
- package/dist/workers/index.js +73 -13
- package/dist/workers/index.js.map +1 -1
- package/package.json +25 -12
|
@@ -0,0 +1,1188 @@
|
|
|
1
|
+
declare class Logger {
|
|
2
|
+
private prefix;
|
|
3
|
+
private logLevel;
|
|
4
|
+
private console;
|
|
5
|
+
private showCaller;
|
|
6
|
+
level: string;
|
|
7
|
+
constructor(prefix?: string, LOGLEVEL?: string, showCaller?: boolean);
|
|
8
|
+
private shouldLog;
|
|
9
|
+
/**
|
|
10
|
+
* Extract caller information from stack trace
|
|
11
|
+
*/
|
|
12
|
+
private getCallerInfo;
|
|
13
|
+
/**
|
|
14
|
+
* Format caller info for display
|
|
15
|
+
*/
|
|
16
|
+
private formatCallerInfo;
|
|
17
|
+
setLevel(level: string): void;
|
|
18
|
+
setShowCaller(show: boolean): void;
|
|
19
|
+
info(...args: any[]): void;
|
|
20
|
+
log(...args: any[]): void;
|
|
21
|
+
verbose(...args: any[]): void;
|
|
22
|
+
success(...args: any[]): void;
|
|
23
|
+
warn(...args: any[]): void;
|
|
24
|
+
error(...args: any[]): void;
|
|
25
|
+
debug(...args: any[]): void;
|
|
26
|
+
assert(condition?: boolean, ...data: any[]): void;
|
|
27
|
+
clear(): void;
|
|
28
|
+
count(label?: string): void;
|
|
29
|
+
countReset(label?: string): void;
|
|
30
|
+
dir(item?: any, options?: any): void;
|
|
31
|
+
dirxml(...data: any[]): void;
|
|
32
|
+
group(...data: any[]): void;
|
|
33
|
+
groupCollapsed(...data: any[]): void;
|
|
34
|
+
groupEnd(): void;
|
|
35
|
+
table(tabularData?: any, properties?: string[]): void;
|
|
36
|
+
time(label?: string): void;
|
|
37
|
+
timeEnd(label?: string): void;
|
|
38
|
+
timeLog(label?: string, ...data: any[]): void;
|
|
39
|
+
trace(...data: any[]): void;
|
|
40
|
+
profile(label?: string): void;
|
|
41
|
+
profileEnd(label?: string): void;
|
|
42
|
+
timeStamp(label?: string): void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
declare global {
|
|
46
|
+
interface Console {
|
|
47
|
+
success(...args: any[]): void;
|
|
48
|
+
verbose(...args: any[]): void;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The SDK's process-global logger.
|
|
54
|
+
*
|
|
55
|
+
* Eight modules under `src/` read a bare `appLogger` off `globalThis` rather
|
|
56
|
+
* than importing one (`src/crypto/{KeyStore,DoubleRatchet,DoubleRatchetManager,
|
|
57
|
+
* Authenticator}.ts`, `src/core/Client.ts`,
|
|
58
|
+
* `src/core/namespaces/HostedAuth.ts`, `src/events/EventCore.ts`), so the
|
|
59
|
+
* binding has to exist before any of them runs, and the `declare global` below
|
|
60
|
+
* is what makes that typecheck.
|
|
61
|
+
*
|
|
62
|
+
* This module is the single place that bootstrap happens. It previously lived
|
|
63
|
+
* inline in three entry files — `src/browser/index.ts`, `src/workers/index.ts`,
|
|
64
|
+
* and `src/core/index.ts` (which `src/server/index.ts` picked up as an import
|
|
65
|
+
* side effect) — three copies that could drift.
|
|
66
|
+
*
|
|
67
|
+
* `??=` makes it idempotent, so importing it from several entries in one
|
|
68
|
+
* process is safe and the first writer wins.
|
|
69
|
+
*
|
|
70
|
+
* KNOWN HAZARD: package.json declares `"sideEffects": false`, which entitles a
|
|
71
|
+
* tree-shaker to drop a module imported purely for effect. The entries import
|
|
72
|
+
* this one for its `export`s as well, so it survives today — but the durable
|
|
73
|
+
* fix is to convert those eight bare reads into real imports of `appLogger`
|
|
74
|
+
* from here, which removes the reliance on a global entirely.
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
declare global {
|
|
78
|
+
var appLogger: InstanceType<typeof Logger>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @public
|
|
83
|
+
* @remarks
|
|
84
|
+
* The body type is a generic type that can be any value. Internally we handle the serialization and deserialization of the body.
|
|
85
|
+
*/
|
|
86
|
+
type MessageBody = any;
|
|
87
|
+
/**
|
|
88
|
+
* @public
|
|
89
|
+
* @remarks
|
|
90
|
+
* The message headers type is a simple record object that contains key value pairs. Think of these as metadata for the message, similar to HTTP headers.
|
|
91
|
+
*/
|
|
92
|
+
type MessageHeaders = Map<string, string>;
|
|
93
|
+
/**
|
|
94
|
+
* @public
|
|
95
|
+
* @remarks
|
|
96
|
+
* The message options type is a record object that contains the options for the message.
|
|
97
|
+
*/
|
|
98
|
+
interface MessageOptions {
|
|
99
|
+
id?: string;
|
|
100
|
+
body: MessageBody;
|
|
101
|
+
status?: "pending" | "processed" | "failed" | "delivered";
|
|
102
|
+
checksum?: string;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* @public
|
|
106
|
+
* @remarks
|
|
107
|
+
* The message class is a simple class that represents a message object.
|
|
108
|
+
* @sealed
|
|
109
|
+
*/
|
|
110
|
+
declare class Message {
|
|
111
|
+
id: string;
|
|
112
|
+
timestamp: number;
|
|
113
|
+
status?: "pending" | "processed" | "failed" | "delivered";
|
|
114
|
+
checksum?: string;
|
|
115
|
+
private _body;
|
|
116
|
+
constructor(msg: MessageBody);
|
|
117
|
+
constructor(msg: MessageOptions);
|
|
118
|
+
get body(): MessageBody;
|
|
119
|
+
set body(value: MessageBody);
|
|
120
|
+
verifyChecksum: () => void;
|
|
121
|
+
private validatebodySize;
|
|
122
|
+
serialize: () => string;
|
|
123
|
+
static deserialize: (data: string) => Message;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
type Address = string;
|
|
127
|
+
interface PacketOptions {
|
|
128
|
+
id?: string;
|
|
129
|
+
subject: string;
|
|
130
|
+
source: Address;
|
|
131
|
+
target: Address;
|
|
132
|
+
headers?: {
|
|
133
|
+
[key: string]: string | number | boolean | undefined;
|
|
134
|
+
};
|
|
135
|
+
message?: Message;
|
|
136
|
+
ttl?: number;
|
|
137
|
+
timestamp?: number;
|
|
138
|
+
signature?: string;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* @public
|
|
142
|
+
* @remarks
|
|
143
|
+
* This class represents a message object that can be sent over the network
|
|
144
|
+
* @sealed
|
|
145
|
+
*/
|
|
146
|
+
declare class Packet {
|
|
147
|
+
id: string;
|
|
148
|
+
subject: string;
|
|
149
|
+
source: Address;
|
|
150
|
+
target: Address;
|
|
151
|
+
headers?: PacketOptions['headers'];
|
|
152
|
+
message?: Message;
|
|
153
|
+
timestamp: number;
|
|
154
|
+
ttl?: number;
|
|
155
|
+
signature?: string;
|
|
156
|
+
constructor({ id, subject, source, target, headers, message, ttl, signature, timestamp }: PacketOptions);
|
|
157
|
+
serialize(): string;
|
|
158
|
+
static deserialize(data: string): Packet;
|
|
159
|
+
isExpired(): boolean;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
declare function SerializeMessage(target: any, propertyKey: string, descriptor: PropertyDescriptor): PropertyDescriptor;
|
|
163
|
+
|
|
164
|
+
declare const decorators: {
|
|
165
|
+
SerializeMessage: typeof SerializeMessage;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Shared cryptographic types for Connect and Accelerator
|
|
170
|
+
*/
|
|
171
|
+
/**
|
|
172
|
+
* Supported ECDH curves
|
|
173
|
+
*/
|
|
174
|
+
type ECDHCurve = 'P-256' | 'P-384';
|
|
175
|
+
/**
|
|
176
|
+
* JSON Web Key format for public keys
|
|
177
|
+
*/
|
|
178
|
+
interface PublicKeyJWK {
|
|
179
|
+
kty: 'EC';
|
|
180
|
+
crv: 'P-256' | 'P-384';
|
|
181
|
+
x: string;
|
|
182
|
+
y: string;
|
|
183
|
+
ext?: boolean;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Keypair with dehydrated public key (JWK) and CryptoKey private key
|
|
187
|
+
*/
|
|
188
|
+
interface Keypair {
|
|
189
|
+
publicKey: PublicKeyJWK;
|
|
190
|
+
privateKey: CryptoKey;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Dehydrated keypair (both keys as strings for transport/storage)
|
|
194
|
+
*/
|
|
195
|
+
interface DehydratedKeypair {
|
|
196
|
+
publicKey: string;
|
|
197
|
+
privateKey: string;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Shared identity types for Connect and Accelerator
|
|
202
|
+
*/
|
|
203
|
+
/**
|
|
204
|
+
* User identity - public key is the primary identifier
|
|
205
|
+
*/
|
|
206
|
+
interface UserIdentity {
|
|
207
|
+
publicKey: string;
|
|
208
|
+
accountType: 'self-sovereign' | 'custodial';
|
|
209
|
+
did?: string;
|
|
210
|
+
provider?: string;
|
|
211
|
+
createdAt: number;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Session
|
|
215
|
+
*/
|
|
216
|
+
interface Session {
|
|
217
|
+
sessionId: string;
|
|
218
|
+
publicKey: string;
|
|
219
|
+
appPublicKey: string;
|
|
220
|
+
createdAt: number;
|
|
221
|
+
expiresAt: number;
|
|
222
|
+
sharedSecret?: string;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Authentication types
|
|
226
|
+
*/
|
|
227
|
+
type AuthType = 'self-sovereign' | 'custodial';
|
|
228
|
+
/**
|
|
229
|
+
* OAuth provider types
|
|
230
|
+
*/
|
|
231
|
+
type OAuthProvider = 'google' | 'github' | 'discord' | 'twitter';
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Shared permission types for Connect and Accelerator
|
|
235
|
+
*/
|
|
236
|
+
/**
|
|
237
|
+
* Permission types for resources
|
|
238
|
+
*/
|
|
239
|
+
type Permission = 'read' | 'write' | 'delete' | 'admin' | 'share' | 'invite';
|
|
240
|
+
/**
|
|
241
|
+
* Role types
|
|
242
|
+
*/
|
|
243
|
+
type Role = 'owner' | 'admin' | 'editor' | 'viewer' | 'member';
|
|
244
|
+
/**
|
|
245
|
+
* ACL Entry - grants permissions to a user (identified by public key)
|
|
246
|
+
*/
|
|
247
|
+
interface ACLEntry {
|
|
248
|
+
subject: string;
|
|
249
|
+
permissions: Permission[];
|
|
250
|
+
role?: Role;
|
|
251
|
+
expiresAt?: number;
|
|
252
|
+
conditions?: {
|
|
253
|
+
ipAllowlist?: string[];
|
|
254
|
+
ipBlocklist?: string[];
|
|
255
|
+
validAfter?: number;
|
|
256
|
+
validBefore?: number;
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Access Control List for a resource
|
|
261
|
+
*/
|
|
262
|
+
interface ACL {
|
|
263
|
+
resource: string;
|
|
264
|
+
entries: Record<string, ACLEntry>;
|
|
265
|
+
defaultPermissions?: Permission[];
|
|
266
|
+
publicPermissions?: Permission[];
|
|
267
|
+
inheritFromParent?: boolean;
|
|
268
|
+
createdAt: number;
|
|
269
|
+
updatedAt: number;
|
|
270
|
+
createdBy: string;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Participant in a shared space
|
|
274
|
+
*/
|
|
275
|
+
interface Participant {
|
|
276
|
+
publicKey: string;
|
|
277
|
+
role: Role;
|
|
278
|
+
addedAt: number;
|
|
279
|
+
addedBy: string;
|
|
280
|
+
customPermissions?: Permission[];
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Permission check request
|
|
284
|
+
*/
|
|
285
|
+
interface PermissionCheckRequest {
|
|
286
|
+
publicKey: string;
|
|
287
|
+
resource: string;
|
|
288
|
+
requiredPermissions: Permission | Permission[];
|
|
289
|
+
context?: {
|
|
290
|
+
timestamp?: number;
|
|
291
|
+
ipAddress?: string;
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Permission check result
|
|
296
|
+
*/
|
|
297
|
+
interface PermissionCheckResult {
|
|
298
|
+
granted: boolean;
|
|
299
|
+
reason?: string;
|
|
300
|
+
grantedBy?: string;
|
|
301
|
+
permissions: Permission[];
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Shared messaging types for Connect and Accelerator
|
|
306
|
+
*/
|
|
307
|
+
/**
|
|
308
|
+
* Message event for pub/sub messaging
|
|
309
|
+
*/
|
|
310
|
+
interface MessageEvent {
|
|
311
|
+
type: string;
|
|
312
|
+
topic: string;
|
|
313
|
+
data: any;
|
|
314
|
+
senderPublicKey?: string;
|
|
315
|
+
timestamp: number;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Message subscription
|
|
319
|
+
*/
|
|
320
|
+
interface Subscription {
|
|
321
|
+
connectionId: string;
|
|
322
|
+
publicKey: string;
|
|
323
|
+
topic: string;
|
|
324
|
+
filters?: any;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* WebSocket message types
|
|
328
|
+
*/
|
|
329
|
+
type WSMessageType = 'connect' | 'connected' | 'subscribe' | 'subscribed' | 'unsubscribe' | 'publish' | 'event' | 'message' | 'error';
|
|
330
|
+
/**
|
|
331
|
+
* WebSocket message
|
|
332
|
+
*/
|
|
333
|
+
interface WSMessage {
|
|
334
|
+
type: WSMessageType;
|
|
335
|
+
topic?: string;
|
|
336
|
+
data?: any;
|
|
337
|
+
publicKey?: string;
|
|
338
|
+
connectionId?: string;
|
|
339
|
+
timestamp?: number;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Shared zero-knowledge types and circuit constants.
|
|
344
|
+
*
|
|
345
|
+
* Used by both the browser/server builds (where snarkjs handles proof generation
|
|
346
|
+
* and verification) and the workers build (where verification runs through
|
|
347
|
+
* `bn128.wasm` via `src/workers/groth16-verifier.ts`). Keeping these here ensures
|
|
348
|
+
* proof shapes and verification keys stay identical across runtimes.
|
|
349
|
+
*/
|
|
350
|
+
/**
|
|
351
|
+
* Groth16 proof structure (BN128).
|
|
352
|
+
*
|
|
353
|
+
* - `pi_a`: G1 point as `[x, y]` or projective `[x, y, z]`.
|
|
354
|
+
* - `pi_b`: G2 point as `[[x0, x1], [y0, y1]]` or projective `[..., [z0, z1]]`.
|
|
355
|
+
* - `pi_c`: G1 point as `[x, y]` or projective `[x, y, z]`.
|
|
356
|
+
*
|
|
357
|
+
* Field elements are decimal-string-encoded BigInts (snarkjs convention).
|
|
358
|
+
*/
|
|
359
|
+
interface Groth16Proof {
|
|
360
|
+
pi_a: string[];
|
|
361
|
+
pi_b: string[][];
|
|
362
|
+
pi_c: string[];
|
|
363
|
+
protocol?: string;
|
|
364
|
+
curve?: string;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Groth16 verification key (BN128).
|
|
368
|
+
*
|
|
369
|
+
* Field elements are decimal-string-encoded BigInts (snarkjs convention).
|
|
370
|
+
*/
|
|
371
|
+
interface VerificationKey {
|
|
372
|
+
protocol: string;
|
|
373
|
+
curve: string;
|
|
374
|
+
nPublic: number;
|
|
375
|
+
vk_alpha_1: string[];
|
|
376
|
+
vk_beta_2: string[][];
|
|
377
|
+
vk_gamma_2: string[][];
|
|
378
|
+
vk_delta_2: string[][];
|
|
379
|
+
vk_alphabeta_12: string[][][];
|
|
380
|
+
IC: string[][];
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Verification key for the `preimagePoK` circuit (Poseidon-commitment
|
|
384
|
+
* proof-of-knowledge with three public signals: commitment, nonce, ecdsaPubHash).
|
|
385
|
+
*
|
|
386
|
+
* Pinned here so worker-side and SDK-side verification agree on the key being
|
|
387
|
+
* checked. If the circuit is recompiled, regenerate this constant and
|
|
388
|
+
* `public/circuits/build/preimagePoK_verification_key.json` together.
|
|
389
|
+
*/
|
|
390
|
+
declare const PREIMAGE_POK_VERIFICATION_KEY: VerificationKey;
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* WebCrypto subtle-interface resolver.
|
|
394
|
+
*
|
|
395
|
+
* Available natively in every runtime this SDK targets (modern browsers,
|
|
396
|
+
* Node 16+, CF Workers). Centralized here so the runtime check + error
|
|
397
|
+
* message are consistent across every primitive — and so a future polyfill
|
|
398
|
+
* shim has one place to plug in.
|
|
399
|
+
*/
|
|
400
|
+
declare function getSubtle(): SubtleCrypto;
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Cryptographically secure random bytes.
|
|
404
|
+
*
|
|
405
|
+
* Wraps `crypto.getRandomValues` with two small affordances every caller
|
|
406
|
+
* needs:
|
|
407
|
+
* - Allocates over a fresh, non-shared `ArrayBuffer` so the returned
|
|
408
|
+
* `Uint8Array` satisfies WebCrypto's `BufferSource` type under strict
|
|
409
|
+
* TS lib settings (where `Uint8Array<ArrayBufferLike>` isn't directly
|
|
410
|
+
* assignable).
|
|
411
|
+
* - Chunks requests larger than 65 KiB, the per-call cap browsers enforce
|
|
412
|
+
* on `getRandomValues`.
|
|
413
|
+
*/
|
|
414
|
+
declare function randomBytes(n: number): Uint8Array;
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* AES-256-GCM encryption / decryption with raw key bytes.
|
|
418
|
+
*
|
|
419
|
+
* The two functions here are the single canonical AES-GCM call site in the
|
|
420
|
+
* SDK. Earlier copies of this logic existed inline in `DoubleRatchet`, in the
|
|
421
|
+
* storage `ChunkCipher`, and in the passphrase-wrap helpers — they all now
|
|
422
|
+
* route through here so the algorithm, IV size, and tag handling stay in one
|
|
423
|
+
* place.
|
|
424
|
+
*
|
|
425
|
+
* Conventions:
|
|
426
|
+
* - 256-bit keys only. Passing 16- or 24-byte keys throws; callers that
|
|
427
|
+
* need AES-128 should add a dedicated primitive.
|
|
428
|
+
* - 96-bit IVs. WebCrypto's recommended GCM IV length; trades the 4-byte
|
|
429
|
+
* IV economy of 128-bit for compatibility with every spec-compliant
|
|
430
|
+
* implementation.
|
|
431
|
+
* - The 16-byte GCM authentication tag is appended to the ciphertext by
|
|
432
|
+
* WebCrypto's default behavior. Callers don't manage it separately.
|
|
433
|
+
*
|
|
434
|
+
* If a future need arises for AAD (associated data), add an optional `aad`
|
|
435
|
+
* parameter to both functions rather than introducing a parallel primitive.
|
|
436
|
+
*/
|
|
437
|
+
/** Length of the AES key in bytes. */
|
|
438
|
+
declare const AES_GCM_KEY_BYTES = 32;
|
|
439
|
+
/** Length of the AES-GCM IV in bytes (WebCrypto-recommended). */
|
|
440
|
+
declare const AES_GCM_IV_BYTES = 12;
|
|
441
|
+
/** Length of the AES-GCM authentication tag in bytes (appended to ciphertext). */
|
|
442
|
+
declare const AES_GCM_TAG_BYTES = 16;
|
|
443
|
+
/**
|
|
444
|
+
* Encrypt `plaintext` under a 32-byte AES-256 key with the supplied 12-byte
|
|
445
|
+
* IV. Returns ciphertext with the 16-byte GCM tag appended (so the output
|
|
446
|
+
* length is `plaintext.length + 16`).
|
|
447
|
+
*
|
|
448
|
+
* The IV must be unique per (key, message). Callers that generate the IV
|
|
449
|
+
* randomly should grab it via `randomBytes(AES_GCM_IV_BYTES)`; callers
|
|
450
|
+
* managing a counter pass their own.
|
|
451
|
+
*/
|
|
452
|
+
declare function encryptAesGcm(key: Uint8Array, iv: Uint8Array, plaintext: Uint8Array): Promise<Uint8Array>;
|
|
453
|
+
/**
|
|
454
|
+
* Inverse of {@link encryptAesGcm}. Throws if the GCM auth tag fails — this
|
|
455
|
+
* captures "wrong key", "wrong IV", or "ciphertext was tampered with",
|
|
456
|
+
* indistinguishably. Callers that care about distinguishing those should
|
|
457
|
+
* keep separate provenance bookkeeping, not invent a parallel MAC.
|
|
458
|
+
*/
|
|
459
|
+
declare function decryptAesGcm(key: Uint8Array, iv: Uint8Array, ciphertext: Uint8Array): Promise<Uint8Array>;
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Key-derivation primitives — PBKDF2 (for passphrase → key) and HKDF (for
|
|
463
|
+
* symmetric key material → expanded key material).
|
|
464
|
+
*
|
|
465
|
+
* PBKDF2 is the slow, salted KDF the passphrase-wrap helper uses to harden
|
|
466
|
+
* user passwords against offline guessing. HKDF is the fast, deterministic
|
|
467
|
+
* KDF the Double Ratchet uses to derive root + chain + message keys from
|
|
468
|
+
* shared secrets.
|
|
469
|
+
*
|
|
470
|
+
* Both are SHA-256-backed everywhere in this codebase — keep it that way
|
|
471
|
+
* unless there's a concrete reason to diverge.
|
|
472
|
+
*/
|
|
473
|
+
/**
|
|
474
|
+
* Iteration count default for PBKDF2 passphrase hardening. Tuned for
|
|
475
|
+
* 2024–2026 hardware: slow enough to deter offline guessing of weak
|
|
476
|
+
* passphrases (~100ms on commodity laptops), fast enough that interactive
|
|
477
|
+
* unlock stays sub-second.
|
|
478
|
+
*/
|
|
479
|
+
declare const PBKDF2_DEFAULT_ITERATIONS = 200000;
|
|
480
|
+
/**
|
|
481
|
+
* Derive a 256-bit AES-GCM key from `passphrase + salt` via PBKDF2-SHA256.
|
|
482
|
+
*
|
|
483
|
+
* Returned as raw bytes (not a `CryptoKey`) so callers can hand it straight
|
|
484
|
+
* to {@link encryptAesGcm} — that's the convention everywhere else in the
|
|
485
|
+
* primitive layer. If a future caller needs the non-extractable
|
|
486
|
+
* `CryptoKey` form, add a parallel `deriveCryptoKeyFromPassphrase` variant
|
|
487
|
+
* rather than overloading this one.
|
|
488
|
+
*/
|
|
489
|
+
declare function deriveAesKeyFromPassphrase(passphrase: string, salt: Uint8Array, iterations?: number): Promise<Uint8Array>;
|
|
490
|
+
/**
|
|
491
|
+
* HKDF-Expand with SHA-256, returning `lengthBytes` raw bytes of key material.
|
|
492
|
+
*
|
|
493
|
+
* `info` is the per-purpose label (e.g. `"DoubleRatchetMsg"`). Optional
|
|
494
|
+
* `salt` defaults to an empty buffer, matching the way the Double Ratchet
|
|
495
|
+
* code has always used it.
|
|
496
|
+
*
|
|
497
|
+
* Note: this is intentionally a one-shot Extract-then-Expand call rather than
|
|
498
|
+
* exposing Extract and Expand separately — every call site in this codebase
|
|
499
|
+
* wants the same shape, and a richer API can be added later if needed.
|
|
500
|
+
*/
|
|
501
|
+
declare function deriveBitsHkdf(inputKeyMaterial: Uint8Array, info: string, lengthBytes: number, salt?: Uint8Array): Promise<Uint8Array>;
|
|
502
|
+
|
|
503
|
+
interface KeyPair {
|
|
504
|
+
privateKey: CryptoKey | null;
|
|
505
|
+
publicKey: CryptoKey;
|
|
506
|
+
}
|
|
507
|
+
interface AuthKeyPair {
|
|
508
|
+
privateKey: CryptoKey | null;
|
|
509
|
+
publicKey: CryptoKey;
|
|
510
|
+
}
|
|
511
|
+
interface DehydratedKeys {
|
|
512
|
+
ecdhPub: string;
|
|
513
|
+
ecdhPriv: string;
|
|
514
|
+
ecdsaPub: string;
|
|
515
|
+
ecdsaPriv: string;
|
|
516
|
+
}
|
|
517
|
+
declare class KeyStore {
|
|
518
|
+
private static instance;
|
|
519
|
+
private _keys;
|
|
520
|
+
private _authKeys;
|
|
521
|
+
private constructor();
|
|
522
|
+
static getInstance(): KeyStore;
|
|
523
|
+
get keys(): Map<string, KeyPair>;
|
|
524
|
+
get authKeys(): Map<string, AuthKeyPair>;
|
|
525
|
+
generateOwnKeyPair(id: string): Promise<KeyPair>;
|
|
526
|
+
storeRemotePublicKeys(id: string, ecdhPublicKey: CryptoKey, ecdsaPublicKey: CryptoKey): Promise<void>;
|
|
527
|
+
hydrateKeyPair(id: string, dehydrated: DehydratedKeys): Promise<void>;
|
|
528
|
+
getKeyPair(id: string): KeyPair | null;
|
|
529
|
+
getAuthKeyPair(id: string): AuthKeyPair | null;
|
|
530
|
+
dehydrateKeyPair(id: string): Promise<DehydratedKeys>;
|
|
531
|
+
getRawEcdsaPublicKey(id: string): Promise<Uint8Array | null>;
|
|
532
|
+
packDehydratedKeys(id: string): Promise<string>;
|
|
533
|
+
hydrateFromPacked(id: string, masterKey: string): Promise<void>;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
interface CipherMessageHeader {
|
|
537
|
+
dhPub: string;
|
|
538
|
+
prevChainLength: number;
|
|
539
|
+
messageNumber: number;
|
|
540
|
+
sessionId: string;
|
|
541
|
+
sessionType: 'global' | 'specific';
|
|
542
|
+
senderId: string;
|
|
543
|
+
recipientId: string;
|
|
544
|
+
timestamp: number;
|
|
545
|
+
signature: string;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
interface CipherMessage {
|
|
549
|
+
header: CipherMessageHeader;
|
|
550
|
+
ciphertext: string; // Hex-encoded
|
|
551
|
+
nonce: string; // Hex-encoded
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// interface RatchetState {
|
|
555
|
+
// clientDhPriv: Uint8Array;
|
|
556
|
+
// clientDhPub: Uint8Array;
|
|
557
|
+
// serverDhPriv: Uint8Array;
|
|
558
|
+
// serverDhPub: Uint8Array;
|
|
559
|
+
// rootKey: Uint8Array | null;
|
|
560
|
+
// sendChainKey: Uint8Array | null;
|
|
561
|
+
// recvChainKey: Uint8Array | null;
|
|
562
|
+
// sendCount: number;
|
|
563
|
+
// recvCount: number;
|
|
564
|
+
// prevChainLength: number;
|
|
565
|
+
// skippedKeys: Map<number, Uint8Array>;
|
|
566
|
+
// }
|
|
567
|
+
|
|
568
|
+
// interface RatchetState {
|
|
569
|
+
// clientDhPriv: CryptoKey | null; // ECDH private key
|
|
570
|
+
// clientDhPub: CryptoKey; //
|
|
571
|
+
// serverDhPriv: CryptoKey | null; // ECDH private key
|
|
572
|
+
// serverDhPub: CryptoKey; // ECDH public key
|
|
573
|
+
// rootKey: CryptoKey | null; // AES-GCM key
|
|
574
|
+
// sendChainKey: CryptoKey | null; // AES-GCM key for sending
|
|
575
|
+
// recvChainKey: CryptoKey | null; // AES-GCM key for receiving
|
|
576
|
+
// sendCount: number; // Number of messages sent
|
|
577
|
+
// recvCount: number; // Number of messages received
|
|
578
|
+
// prevChainLength: number; // Length of the previous chain
|
|
579
|
+
// skippedKeys: Map<number, CryptoKey>; // Skipped keys for messages
|
|
580
|
+
// }
|
|
581
|
+
|
|
582
|
+
interface RatchetState {
|
|
583
|
+
clientDhPriv: CryptoKey | null;
|
|
584
|
+
clientDhPub: CryptoKey ;
|
|
585
|
+
serverDhPriv: CryptoKey | null;
|
|
586
|
+
serverDhPub: CryptoKey ;
|
|
587
|
+
// Symmetric key material (root key + chain keys + skipped message keys).
|
|
588
|
+
// Stored as Uint8Array rather than Node's Buffer so this type works in the
|
|
589
|
+
// browser/Workers builds.
|
|
590
|
+
rootKey: Uint8Array | null;
|
|
591
|
+
sendChainKey: Uint8Array | null;
|
|
592
|
+
recvChainKey: Uint8Array | null;
|
|
593
|
+
sendCount: number;
|
|
594
|
+
recvCount: number;
|
|
595
|
+
prevChainLength: number;
|
|
596
|
+
currentSkippedKeys: Map<number, Uint8Array>;
|
|
597
|
+
oldSkippedMessageKeys: Map<string, { skips: Map<number, Uint8Array>; created: number }>;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
declare class DoubleRatchet {
|
|
601
|
+
private state;
|
|
602
|
+
private sessionType;
|
|
603
|
+
private maxSkip;
|
|
604
|
+
private overlapPeriod;
|
|
605
|
+
private windowSize;
|
|
606
|
+
constructor(senderId: string, recipientId: string, sessionType: 'global' | 'specific', isClient?: boolean);
|
|
607
|
+
private hkdf;
|
|
608
|
+
initializeSession(isClient?: boolean): Promise<void>;
|
|
609
|
+
private dhRatchet;
|
|
610
|
+
private symmetricRatchet;
|
|
611
|
+
encrypt(plaintext: string, newDhKey: boolean | undefined, senderId: string, recipientId: string, sessionId: string, sessionType: 'global' | 'specific'): Promise<CipherMessage>;
|
|
612
|
+
private cleanOldSkippedKeys;
|
|
613
|
+
decrypt(message: CipherMessage, isClient?: boolean): Promise<string>;
|
|
614
|
+
getState(): RatchetState;
|
|
615
|
+
setState(state: RatchetState): void;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Per-chunk AES-256-GCM encryption with random keys and IVs.
|
|
620
|
+
*
|
|
621
|
+
* Each chunk of a stored file gets its own freshly generated 256-bit AES key
|
|
622
|
+
* and 96-bit IV. The (key, IV) pair lives in the file manifest, which the
|
|
623
|
+
* application is responsible for storing under whatever access-control regime
|
|
624
|
+
* it uses (in this codebase: the `SharedSpaceClient` writes manifests into
|
|
625
|
+
* ZK-gated multi-user spaces; the shard store itself is open).
|
|
626
|
+
*
|
|
627
|
+
* Per-chunk keys (rather than a per-file DEK) lets future versions revoke or
|
|
628
|
+
* re-share individual chunks without re-encrypting the whole file. The
|
|
629
|
+
* additional manifest size is dominated by the shard hash list anyway.
|
|
630
|
+
*
|
|
631
|
+
* The cipher itself delegates to the canonical `encryptAesGcm` / `decryptAesGcm`
|
|
632
|
+
* primitives — this class only wraps them with key-material generation and
|
|
633
|
+
* the JSON-friendly base64 serialization the manifest needs.
|
|
634
|
+
*/
|
|
635
|
+
/**
|
|
636
|
+
* Per-chunk key material, base64-encoded for JSON-friendly storage in the
|
|
637
|
+
* manifest. The IV is unique per chunk; the key is unique per chunk too in
|
|
638
|
+
* this codebase, though that's a stricter property than AES-GCM strictly
|
|
639
|
+
* requires (which is "unique IV per key").
|
|
640
|
+
*/
|
|
641
|
+
interface ChunkKeyMaterial {
|
|
642
|
+
/** Base64-encoded 32-byte AES-256 key. */
|
|
643
|
+
key: string;
|
|
644
|
+
/** Base64-encoded 12-byte AES-GCM IV. */
|
|
645
|
+
iv: string;
|
|
646
|
+
}
|
|
647
|
+
declare class ChunkCipher {
|
|
648
|
+
/**
|
|
649
|
+
* Generate a fresh random (key, iv) pair encoded for the manifest.
|
|
650
|
+
* Callers should generate one of these per chunk before calling
|
|
651
|
+
* {@link encryptChunk}.
|
|
652
|
+
*/
|
|
653
|
+
generateKeyMaterial(): ChunkKeyMaterial;
|
|
654
|
+
/**
|
|
655
|
+
* Encrypt `plaintext` under the supplied key + IV. Returns the AES-GCM
|
|
656
|
+
* ciphertext with the 16-byte auth tag appended (WebCrypto's default
|
|
657
|
+
* GCM behavior).
|
|
658
|
+
*/
|
|
659
|
+
encryptChunk(plaintext: Uint8Array, material: ChunkKeyMaterial): Promise<Uint8Array>;
|
|
660
|
+
/**
|
|
661
|
+
* Inverse of {@link encryptChunk}. Throws if the auth tag fails — that
|
|
662
|
+
* means the ciphertext was tampered with, the key/iv was wrong, or the
|
|
663
|
+
* shards were reassembled incorrectly.
|
|
664
|
+
*/
|
|
665
|
+
decryptChunk(ciphertext: Uint8Array, material: ChunkKeyMaterial): Promise<Uint8Array>;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Passphrase-based wrap / unwrap helpers using WebCrypto.
|
|
670
|
+
*
|
|
671
|
+
* Encrypt a payload with a user-supplied passphrase before persisting it
|
|
672
|
+
* somewhere the server can see (e.g. the manifest blob in a ZK-gated
|
|
673
|
+
* SharedSpace, or chat-key material in a PersonalSpace). The server never
|
|
674
|
+
* sees the passphrase or the plaintext — both wrap and unwrap run entirely
|
|
675
|
+
* client-side.
|
|
676
|
+
*
|
|
677
|
+
* Algorithm (the only one this codebase has ever shipped):
|
|
678
|
+
* - PBKDF2-SHA256 with 200_000 iterations derives a 256-bit AES key from
|
|
679
|
+
* `passphrase || salt`.
|
|
680
|
+
* - AES-256-GCM encrypts the plaintext with a fresh random 12-byte IV.
|
|
681
|
+
* - The auth tag is appended to the ciphertext by WebCrypto's default
|
|
682
|
+
* GCM behaviour, so {@link WrappedPayload.ciphertext} carries it implicitly.
|
|
683
|
+
*
|
|
684
|
+
* The returned {@link WrappedPayload} is JSON-friendly (all base64 fields), so
|
|
685
|
+
* it can be `JSON.stringify`-ed straight into a KV layer.
|
|
686
|
+
*/
|
|
687
|
+
/**
|
|
688
|
+
* The persisted, JSON-friendly representation of a passphrase-wrapped blob.
|
|
689
|
+
*
|
|
690
|
+
* The `alg` field is a self-describing marker — if we ever rotate KDF/cipher,
|
|
691
|
+
* unwrap can switch on this value to keep older payloads readable.
|
|
692
|
+
*/
|
|
693
|
+
interface WrappedPayload {
|
|
694
|
+
/** Random per-wrap salt for PBKDF2, base64. */
|
|
695
|
+
salt: string;
|
|
696
|
+
/** AES-GCM IV, base64. */
|
|
697
|
+
iv: string;
|
|
698
|
+
/** AES-GCM ciphertext (includes 16-byte GCM auth tag), base64. */
|
|
699
|
+
ciphertext: string;
|
|
700
|
+
/** Algorithm marker so future readers know what they're looking at. */
|
|
701
|
+
alg: "PBKDF2-SHA256/AES-256-GCM";
|
|
702
|
+
/** PBKDF2 iteration count used. */
|
|
703
|
+
iter: number;
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Encrypt `plaintext` under a key derived from `passphrase`. The returned
|
|
707
|
+
* payload bundles the salt, IV, and ciphertext so unwrap only needs the
|
|
708
|
+
* passphrase to reverse the operation.
|
|
709
|
+
*/
|
|
710
|
+
declare function wrapWithPassphrase(passphrase: string, plaintext: Uint8Array): Promise<WrappedPayload>;
|
|
711
|
+
/**
|
|
712
|
+
* Decrypt a {@link WrappedPayload} produced by {@link wrapWithPassphrase}.
|
|
713
|
+
* Throws if the passphrase is wrong, the payload is tampered with, or the
|
|
714
|
+
* algorithm marker is unrecognised.
|
|
715
|
+
*/
|
|
716
|
+
declare function unwrapWithPassphrase(passphrase: string, wrapped: WrappedPayload): Promise<Uint8Array>;
|
|
717
|
+
|
|
718
|
+
declare enum EventCoreEvents {
|
|
719
|
+
CONNECTED = "connected",
|
|
720
|
+
DISCONNECTED = "disconnected",
|
|
721
|
+
RECONNECTING = "reconnecting",
|
|
722
|
+
ERROR = "error",
|
|
723
|
+
DATA_RECEIVED = "data_received",
|
|
724
|
+
DATA_SENT = "data_sent",
|
|
725
|
+
MESSAGE = "message",
|
|
726
|
+
GET_HISTORY = "get_history",
|
|
727
|
+
RECEIVED_HISTORY = "received_history",
|
|
728
|
+
ONLINE = "online",
|
|
729
|
+
OFFLINE = "offline",
|
|
730
|
+
SYNCING = "syncing",
|
|
731
|
+
SYNC_PROGRESS = "sync_progress",
|
|
732
|
+
SYNC_COMPLETE = "sync_complete",
|
|
733
|
+
SYNC_ERROR = "sync_error"
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* @public
|
|
737
|
+
* EventCore is a static event emitter with singleton functionality for global event handling.
|
|
738
|
+
*/
|
|
739
|
+
declare class EventCore {
|
|
740
|
+
private static eventTarget;
|
|
741
|
+
/**
|
|
742
|
+
* `event -> (caller's handler -> the wrapper actually registered on the
|
|
743
|
+
* EventTarget)`. Two levels because one event can have many listeners and
|
|
744
|
+
* {@link off} must remove exactly the wrapper it registered for that caller.
|
|
745
|
+
*/
|
|
746
|
+
private static events;
|
|
747
|
+
/**
|
|
748
|
+
* @remarks Listen to an event.
|
|
749
|
+
* @example EventCore.on('event', (e) =\> appLogger.debug(e));
|
|
750
|
+
*/
|
|
751
|
+
static on(event: EventCoreEvents, handler: EventListener | CallableFunction): void;
|
|
752
|
+
/**
|
|
753
|
+
* @remarks Stop listening to an event.
|
|
754
|
+
* @example EventCore.off('event', handler);
|
|
755
|
+
*/
|
|
756
|
+
static off(event: EventCoreEvents, handler: EventListener | CallableFunction): void;
|
|
757
|
+
/**
|
|
758
|
+
* @remarks Emit an event.
|
|
759
|
+
* @example EventCore.emit('event', data);
|
|
760
|
+
*/
|
|
761
|
+
static emit(event: EventCoreEvents, data: any): void;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* An end-to-end-encrypted communication session for one local identity
|
|
766
|
+
* (`myId`) talking to an arbitrary set of peers. Wraps `KeyStore` +
|
|
767
|
+
* per-pair `DoubleRatchet` instances and owns the protocol bookkeeping
|
|
768
|
+
* (handshake reciprocation, dedup, role assignment, broadcast fan-out,
|
|
769
|
+
* recipient filtering) so application code doesn't have to.
|
|
770
|
+
*
|
|
771
|
+
* Transport-agnostic by design: the Session never touches a WebSocket
|
|
772
|
+
* (or HTTP, or anything else). Application code:
|
|
773
|
+
* - calls `session.encrypt(text)` and gets back an array of frames it
|
|
774
|
+
* ships over whatever transport it has;
|
|
775
|
+
* - calls `session.receive(frame)` with each inbound frame and acts on
|
|
776
|
+
* the typed result (plaintext, handshake-done, ignored).
|
|
777
|
+
*
|
|
778
|
+
* Frames are plain JSON objects with the same shape the existing chat
|
|
779
|
+
* protocol uses (`{ keyExchange: {...} }` and `{ cipherMessage: {...} }`),
|
|
780
|
+
* so this can drop into a chat app without changing the wire format.
|
|
781
|
+
*/
|
|
782
|
+
|
|
783
|
+
interface SessionOptions {
|
|
784
|
+
/** This client's identifier (e.g. username / DID / public-key string). */
|
|
785
|
+
myId: string;
|
|
786
|
+
}
|
|
787
|
+
/** A handshake frame the application ships to peers (or receives from them). */
|
|
788
|
+
interface KeyExchangeFrame {
|
|
789
|
+
type: "handshake" | "update";
|
|
790
|
+
userId: string;
|
|
791
|
+
/** Base64-encoded JWK of the sender's ECDH public key. */
|
|
792
|
+
ecdhPublicKey: string;
|
|
793
|
+
/** Base64-encoded JWK of the sender's ECDSA public key. */
|
|
794
|
+
ecdsaPublicKey: string;
|
|
795
|
+
}
|
|
796
|
+
/** A ciphertext frame, addressed to exactly one peer. */
|
|
797
|
+
interface CipherFrame {
|
|
798
|
+
cipherMessage: CipherMessage;
|
|
799
|
+
}
|
|
800
|
+
/** Anything inbound the application might hand to `session.receive()`. */
|
|
801
|
+
type IncomingFrame = {
|
|
802
|
+
keyExchange: KeyExchangeFrame;
|
|
803
|
+
} | {
|
|
804
|
+
cipherMessage: CipherMessage;
|
|
805
|
+
} | Record<string, unknown>;
|
|
806
|
+
/** Outcome of feeding one frame through `session.receive()`. */
|
|
807
|
+
type ReceiveResult = {
|
|
808
|
+
kind: "plaintext";
|
|
809
|
+
/** The sender's id (from the cipherMessage header). */
|
|
810
|
+
from: string;
|
|
811
|
+
/** Decrypted message body. */
|
|
812
|
+
text: string;
|
|
813
|
+
/** The original cipherMessage, for callers that want it. */
|
|
814
|
+
cipherMessage: CipherMessage;
|
|
815
|
+
} | {
|
|
816
|
+
kind: "handshake";
|
|
817
|
+
/** The peer whose handshake was just processed. */
|
|
818
|
+
peerId: string;
|
|
819
|
+
/**
|
|
820
|
+
* If non-null, the application MUST send this frame back to the peer
|
|
821
|
+
* (e.g. broadcast on the room WebSocket). It's our reciprocation —
|
|
822
|
+
* giving the peer our public keys so they can verify our messages.
|
|
823
|
+
*
|
|
824
|
+
* The Session dedups internally: we only emit this on the first
|
|
825
|
+
* handshake we process for a given peer in this Session's lifetime.
|
|
826
|
+
*/
|
|
827
|
+
outbound: {
|
|
828
|
+
keyExchange: KeyExchangeFrame;
|
|
829
|
+
} | null;
|
|
830
|
+
} | {
|
|
831
|
+
kind: "ignored";
|
|
832
|
+
/** Why we ignored the frame (debugging aid; not for branching on). */
|
|
833
|
+
reason: string;
|
|
834
|
+
};
|
|
835
|
+
declare class EncryptedSession {
|
|
836
|
+
private readonly myId;
|
|
837
|
+
private readonly keyStore;
|
|
838
|
+
/** One DoubleRatchet per peer pair. */
|
|
839
|
+
private readonly ratchets;
|
|
840
|
+
/** Peers we've already sent our keyExchange to in this Session. */
|
|
841
|
+
private readonly sentHandshakeTo;
|
|
842
|
+
/** Resolved when own keys are generated and ready to use. */
|
|
843
|
+
private readonly _ready;
|
|
844
|
+
private _resolveReady;
|
|
845
|
+
private _initialized;
|
|
846
|
+
constructor(opts: SessionOptions);
|
|
847
|
+
/** A promise that resolves once `initialize()` has completed. */
|
|
848
|
+
get ready(): Promise<void>;
|
|
849
|
+
/** Our local identifier. */
|
|
850
|
+
get id(): string;
|
|
851
|
+
/**
|
|
852
|
+
* Generate this client's ECDH + ECDSA keypair (idempotent — safe to call
|
|
853
|
+
* multiple times across reconnects; only the first call generates).
|
|
854
|
+
*/
|
|
855
|
+
initialize(): Promise<void>;
|
|
856
|
+
/**
|
|
857
|
+
* Build the keyExchange frame this client should send to peers when it
|
|
858
|
+
* comes online. Application is responsible for actually transmitting it
|
|
859
|
+
* (e.g. ws.send(JSON.stringify(frame))).
|
|
860
|
+
*/
|
|
861
|
+
getOwnKeyExchange(): Promise<{
|
|
862
|
+
keyExchange: KeyExchangeFrame;
|
|
863
|
+
}>;
|
|
864
|
+
/**
|
|
865
|
+
* Encrypt a plaintext to every peer we currently have a ratchet for, and
|
|
866
|
+
* return one cipherMessage frame per recipient. The application sends
|
|
867
|
+
* each frame over its transport.
|
|
868
|
+
*
|
|
869
|
+
* If there are no peers yet (or all peers are still mid-handshake), the
|
|
870
|
+
* returned array may be empty — the caller should typically render the
|
|
871
|
+
* outgoing message locally in that case (since there's no one to send
|
|
872
|
+
* it to yet).
|
|
873
|
+
*/
|
|
874
|
+
encrypt(plaintext: string): Promise<CipherFrame[]>;
|
|
875
|
+
/**
|
|
876
|
+
* Hand an inbound frame to the session. Returns a typed result the
|
|
877
|
+
* application acts on.
|
|
878
|
+
*
|
|
879
|
+
* - `kind: 'plaintext'` — decrypted; render `result.text`
|
|
880
|
+
* - `kind: 'handshake'` — a peer's keys are now stored, and if
|
|
881
|
+
* `result.outbound` is set, send it
|
|
882
|
+
* - `kind: 'ignored'` — not for us, malformed, duplicate, etc.
|
|
883
|
+
*
|
|
884
|
+
* Calling code should always handle `'ignored'` (no UI side effect).
|
|
885
|
+
*/
|
|
886
|
+
receive(frame: IncomingFrame): Promise<ReceiveResult>;
|
|
887
|
+
/** List of peer ids we have an established ratchet for. */
|
|
888
|
+
peers(): string[];
|
|
889
|
+
/** Has the handshake completed for this peer? */
|
|
890
|
+
hasRatchetFor(peerId: string): boolean;
|
|
891
|
+
/**
|
|
892
|
+
* Tear down a peer's ratchet (e.g. when they leave the room). The peer's
|
|
893
|
+
* stored keys remain in `KeyStore` so a fresh handshake can be set up
|
|
894
|
+
* later without re-fetching them, but the ratchet state is dropped.
|
|
895
|
+
*/
|
|
896
|
+
forgetPeer(peerId: string): void;
|
|
897
|
+
/** Deterministic per-pair session id — same string on both sides. */
|
|
898
|
+
private _sessionIdFor;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* Pure WebSocket transport — owns the socket lifecycle and nothing else.
|
|
903
|
+
*
|
|
904
|
+
* Hands raw frame strings up (via the `MESSAGE` event) and accepts raw frame
|
|
905
|
+
* strings down (via `send`). Anything frame-shape, encryption, or serialization
|
|
906
|
+
* related belongs in the layer above (e.g. `BroadcastChannel` for client↔server
|
|
907
|
+
* encrypted pipes, `BroadcastChannel` for multi-peer E2E rooms).
|
|
908
|
+
*
|
|
909
|
+
* Responsibilities:
|
|
910
|
+
* - Open / close the WebSocket
|
|
911
|
+
* - Auto-reconnect with linear backoff after an unexpected close
|
|
912
|
+
* - Buffer outbound frames while disconnected (capped queue)
|
|
913
|
+
* - Emit lifecycle events (CONNECTED / DISCONNECTED / RECONNECTING / ERROR)
|
|
914
|
+
* plus raw inbound MESSAGE events
|
|
915
|
+
*/
|
|
916
|
+
|
|
917
|
+
interface WSTransportOptions {
|
|
918
|
+
/** WebSocket URL, e.g. `ws://localhost:8787/foo`. */
|
|
919
|
+
url: string;
|
|
920
|
+
/**
|
|
921
|
+
* Optional async provider for a FRESH connection URL, called before each
|
|
922
|
+
* RECONNECT. Use it when the URL carries a single-use/expiring credential
|
|
923
|
+
* (e.g. a one-time WS upgrade ticket): on reconnect the baked-in ticket is
|
|
924
|
+
* already consumed/expired, so without this the reconnect is rejected and the
|
|
925
|
+
* transport burns through its attempts. The initial connect uses `url` as-is.
|
|
926
|
+
*/
|
|
927
|
+
urlProvider?: () => string | Promise<string>;
|
|
928
|
+
/**
|
|
929
|
+
* Stable identifier for this connection (a channel / space / personal-space
|
|
930
|
+
* id). Lifecycle + MESSAGE events are namespaced by it on the shared
|
|
931
|
+
* `EventCore` bus so concurrent transports (chat room A, room B, the storage
|
|
932
|
+
* feed, …) never receive each other's frames. Optional — a unique id is
|
|
933
|
+
* generated when omitted; pass a semantic one for clearer debugging.
|
|
934
|
+
*/
|
|
935
|
+
id?: string;
|
|
936
|
+
/** Reconnect automatically after unexpected disconnect (default `true`). */
|
|
937
|
+
autoReconnect?: boolean;
|
|
938
|
+
/** Delay between reconnect attempts in ms (default `3000`). */
|
|
939
|
+
reconnectDelay?: number;
|
|
940
|
+
/** Cap on reconnect attempts (default `5`; `0` means unlimited). */
|
|
941
|
+
maxReconnectAttempts?: number;
|
|
942
|
+
/**
|
|
943
|
+
* Max outbound frames buffered while disconnected. Sending past this caps
|
|
944
|
+
* throws synchronously. Default `100`.
|
|
945
|
+
*/
|
|
946
|
+
maxQueueSize?: number;
|
|
947
|
+
/**
|
|
948
|
+
* Heartbeat interval in ms. While connected, a `pingFrame` is sent every
|
|
949
|
+
* interval to keep the socket warm (long-lived WebSockets get dropped by CF
|
|
950
|
+
* / intermediaries when idle) and to detect a silently-dead connection.
|
|
951
|
+
* Default `30000`. Set `0` to disable the heartbeat entirely.
|
|
952
|
+
*/
|
|
953
|
+
heartbeatInterval?: number;
|
|
954
|
+
/**
|
|
955
|
+
* How long (ms) to wait for a `pongFrame` after sending a ping before
|
|
956
|
+
* treating the connection as dead and forcing a reconnect. Should be well
|
|
957
|
+
* under `heartbeatInterval`. Default `10000`.
|
|
958
|
+
*/
|
|
959
|
+
heartbeatTimeout?: number;
|
|
960
|
+
/** Outbound keep-alive frame. Default `"ping"`. */
|
|
961
|
+
pingFrame?: string;
|
|
962
|
+
/**
|
|
963
|
+
* Inbound keep-alive ack. Matching frames are swallowed (never surfaced as a
|
|
964
|
+
* `MESSAGE`) and reset the liveness timer. Must match the server's
|
|
965
|
+
* auto-response. Default `"pong"`.
|
|
966
|
+
*/
|
|
967
|
+
pongFrame?: string;
|
|
968
|
+
}
|
|
969
|
+
declare class WSTransport extends EventCore {
|
|
970
|
+
/**
|
|
971
|
+
* Per-instance event scope. `EventCore` is a process-global static emitter,
|
|
972
|
+
* so without namespacing every WSTransport would hear every other one's
|
|
973
|
+
* frames. We prefix each event with this transport's `id` and expose
|
|
974
|
+
* `on`/`off`/`emit` that scope transparently — callers still use the plain
|
|
975
|
+
* `EventCoreEvents` enum and only ever see their own connection's events.
|
|
976
|
+
*/
|
|
977
|
+
readonly id: string;
|
|
978
|
+
readonly emit: (event: EventCoreEvents, data: unknown) => void;
|
|
979
|
+
readonly on: (event: EventCoreEvents, handler: EventListener | CallableFunction) => void;
|
|
980
|
+
readonly off: (event: EventCoreEvents, handler: EventListener | CallableFunction) => void;
|
|
981
|
+
private scopedEvent;
|
|
982
|
+
private url;
|
|
983
|
+
private urlProvider?;
|
|
984
|
+
private socket;
|
|
985
|
+
private _isConnected;
|
|
986
|
+
private _isConnecting;
|
|
987
|
+
private autoReconnect;
|
|
988
|
+
private reconnectDelay;
|
|
989
|
+
private maxReconnectAttempts;
|
|
990
|
+
private reconnectAttempts;
|
|
991
|
+
private reconnectTimeout;
|
|
992
|
+
private outboundQueue;
|
|
993
|
+
private maxQueueSize;
|
|
994
|
+
private heartbeatInterval;
|
|
995
|
+
private heartbeatTimeout;
|
|
996
|
+
private pingFrame;
|
|
997
|
+
private pongFrame;
|
|
998
|
+
private heartbeatTimer;
|
|
999
|
+
private pongTimer;
|
|
1000
|
+
constructor(opts: WSTransportOptions);
|
|
1001
|
+
/** Open the socket. Resolves when CONNECTED fires. Rejects on first error. */
|
|
1002
|
+
connect(): Promise<void>;
|
|
1003
|
+
/** Close the socket and stop reconnecting. */
|
|
1004
|
+
disconnect(): void;
|
|
1005
|
+
/**
|
|
1006
|
+
* Send a raw frame. If currently disconnected, the frame is queued and sent
|
|
1007
|
+
* after the next successful reconnect (up to `maxQueueSize`).
|
|
1008
|
+
*/
|
|
1009
|
+
send(frame: string): void;
|
|
1010
|
+
isConnected(): boolean;
|
|
1011
|
+
isConnecting(): boolean;
|
|
1012
|
+
queuedFrames(): number;
|
|
1013
|
+
reconnectAttemptCount(): number;
|
|
1014
|
+
/** Begin the keep-alive loop (no-op when disabled). */
|
|
1015
|
+
private startHeartbeat;
|
|
1016
|
+
/** Stop the keep-alive loop and clear any pending liveness deadline. */
|
|
1017
|
+
private stopHeartbeat;
|
|
1018
|
+
/**
|
|
1019
|
+
* Send one ping and arm a liveness deadline. If no `pongFrame` arrives before
|
|
1020
|
+
* `heartbeatTimeout`, the connection is considered dead and we force a
|
|
1021
|
+
* reconnect — this is what catches a silently-dropped TCP connection that
|
|
1022
|
+
* never fired `onclose` (the failure mode that "loses connection with the
|
|
1023
|
+
* Accelerator" on long sessions).
|
|
1024
|
+
*/
|
|
1025
|
+
private sendHeartbeat;
|
|
1026
|
+
/**
|
|
1027
|
+
* Tear down a connection we believe is dead and kick off reconnect, without
|
|
1028
|
+
* waiting on the OS/browser to eventually fire `onclose` (which a half-open
|
|
1029
|
+
* socket may never do). We detach the handlers first so the normal close path
|
|
1030
|
+
* can't double-fire the reconnect.
|
|
1031
|
+
*/
|
|
1032
|
+
private forceReconnect;
|
|
1033
|
+
private scheduleReconnect;
|
|
1034
|
+
private flushQueue;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/**
|
|
1038
|
+
* BroadcastChannel — a turnkey end-to-end encrypted channel for multi-peer
|
|
1039
|
+
* "room"-style apps (e.g. a chat). Wires together:
|
|
1040
|
+
*
|
|
1041
|
+
* - `WSTransport` for socket lifecycle, reconnect, and offline frame queue
|
|
1042
|
+
* - `EncryptedSession` for per-peer Double Ratchet state, handshake
|
|
1043
|
+
* reciprocation, fan-out encryption, and recipient filtering
|
|
1044
|
+
*
|
|
1045
|
+
* The host app only deals with high-level events and method calls; it never
|
|
1046
|
+
* touches `new WebSocket(...)` or builds cipherMessage frames by hand.
|
|
1047
|
+
*
|
|
1048
|
+
* Wire protocol (kept compatible with the existing chat protocol so this
|
|
1049
|
+
* drops into the deployed Accelerator without server changes):
|
|
1050
|
+
*
|
|
1051
|
+
* outbound: `{ keyExchange: {...} }` — produced by us on `announce()`
|
|
1052
|
+
* `{ cipherMessage: {...} }` — produced by `send(text)`,
|
|
1053
|
+
* one frame per peer
|
|
1054
|
+
* arbitrary JSON — sent via `sendRaw(obj)`
|
|
1055
|
+
*
|
|
1056
|
+
* inbound: `{ keyExchange: {...} }` — routed into EncryptedSession;
|
|
1057
|
+
* emits `peer_handshake`
|
|
1058
|
+
* `{ cipherMessage: {...} }` — decrypted; emits `message`
|
|
1059
|
+
* anything else — passes through as `raw_frame`
|
|
1060
|
+
* (the app decides what to do)
|
|
1061
|
+
*
|
|
1062
|
+
* **Event scoping.** BroadcastChannel uses a per-instance
|
|
1063
|
+
* `EventTarget`, so two channels (e.g. two rooms in the same app) don't
|
|
1064
|
+
* cross-contaminate each other's events.
|
|
1065
|
+
*
|
|
1066
|
+
* Note: this class shadows the `BroadcastChannel` Web API global (cross-tab
|
|
1067
|
+
* messaging) only when imported by name. They're unrelated.
|
|
1068
|
+
*/
|
|
1069
|
+
|
|
1070
|
+
/**
|
|
1071
|
+
* Event names BroadcastChannel emits. Lifecycle events reuse the
|
|
1072
|
+
* `EventCoreEvents` string values (`connected`, `disconnected`, etc.) so
|
|
1073
|
+
* apps can use shared constants if they want. Channel-specific events are
|
|
1074
|
+
* prefixed `channel:` to avoid colliding with other event systems in the
|
|
1075
|
+
* same app.
|
|
1076
|
+
*/
|
|
1077
|
+
declare const BroadcastChannelEvents: {
|
|
1078
|
+
readonly CONNECTED: EventCoreEvents.CONNECTED;
|
|
1079
|
+
readonly DISCONNECTED: EventCoreEvents.DISCONNECTED;
|
|
1080
|
+
readonly RECONNECTING: EventCoreEvents.RECONNECTING;
|
|
1081
|
+
readonly ERROR: EventCoreEvents.ERROR;
|
|
1082
|
+
/** A peer's ratchet is now ready. `detail.peerId` is the peer's id. */
|
|
1083
|
+
readonly PEER_HANDSHAKE: "channel:peer_handshake";
|
|
1084
|
+
/** Decrypted message. `detail` is `{ from, text, cipherMessage }`. */
|
|
1085
|
+
readonly MESSAGE: "channel:message";
|
|
1086
|
+
/** Inbound frame BroadcastChannel didn't recognize. App handles it. */
|
|
1087
|
+
readonly RAW_FRAME: "channel:raw_frame";
|
|
1088
|
+
};
|
|
1089
|
+
type BroadcastChannelEvent = (typeof BroadcastChannelEvents)[keyof typeof BroadcastChannelEvents];
|
|
1090
|
+
interface BroadcastChannelOptions extends WSTransportOptions {
|
|
1091
|
+
/** This client's identifier (username, DID, public-key string, etc.). */
|
|
1092
|
+
myId: string;
|
|
1093
|
+
/**
|
|
1094
|
+
* If true (default false), automatically send our keyExchange as soon as
|
|
1095
|
+
* the socket is connected. For chat-style apps that send a `{name}` frame
|
|
1096
|
+
* first, leave this false and call `announce()` after that handshake.
|
|
1097
|
+
*/
|
|
1098
|
+
autoAnnounce?: boolean;
|
|
1099
|
+
}
|
|
1100
|
+
declare class BroadcastChannel {
|
|
1101
|
+
readonly transport: WSTransport;
|
|
1102
|
+
readonly session: EncryptedSession;
|
|
1103
|
+
readonly myId: string;
|
|
1104
|
+
private autoAnnounce;
|
|
1105
|
+
private announced;
|
|
1106
|
+
/** Per-instance EventTarget — events do NOT leak across BroadcastChannels. */
|
|
1107
|
+
private events;
|
|
1108
|
+
constructor(opts: BroadcastChannelOptions);
|
|
1109
|
+
/** Subscribe to a channel event. Handler receives a CustomEvent. */
|
|
1110
|
+
on(event: BroadcastChannelEvent | string, handler: (e: CustomEvent) => void): void;
|
|
1111
|
+
/** Unsubscribe. */
|
|
1112
|
+
off(event: BroadcastChannelEvent | string, handler: (e: CustomEvent) => void): void;
|
|
1113
|
+
private emit;
|
|
1114
|
+
/** Generate our keys (idempotent) and open the WebSocket. */
|
|
1115
|
+
connect(): Promise<void>;
|
|
1116
|
+
/** Close the WebSocket and stop reconnecting. */
|
|
1117
|
+
disconnect(): void;
|
|
1118
|
+
/**
|
|
1119
|
+
* Send our `keyExchange` frame to the room. Idempotent — only sends on the
|
|
1120
|
+
* first call. Call this once your higher-level handshake (e.g. sending a
|
|
1121
|
+
* `{name}` identification frame) has completed.
|
|
1122
|
+
*/
|
|
1123
|
+
announce(): Promise<void>;
|
|
1124
|
+
/**
|
|
1125
|
+
* Encrypt `plaintext` to every peer we have a ratchet with, and send each
|
|
1126
|
+
* cipherMessage as its own WS frame. Returns the number of peers we sent to
|
|
1127
|
+
* (0 if no peers are handshaken yet — in which case the caller should
|
|
1128
|
+
* typically render the message locally for the sender and try again later).
|
|
1129
|
+
*/
|
|
1130
|
+
send(plaintext: string): Promise<number>;
|
|
1131
|
+
/**
|
|
1132
|
+
* Send an arbitrary JSON-serializable frame as-is. The app uses this for
|
|
1133
|
+
* any frame that isn't channel-managed (e.g. a chat's `{name}`, `{file}`,
|
|
1134
|
+
* or unencrypted `{message}`).
|
|
1135
|
+
*/
|
|
1136
|
+
sendRaw(frame: unknown): void;
|
|
1137
|
+
/** List of peer ids we have an established ratchet for. */
|
|
1138
|
+
peers(): string[];
|
|
1139
|
+
/** Drop a peer's ratchet (e.g. when they leave the room). */
|
|
1140
|
+
forgetPeer(peerId: string): void;
|
|
1141
|
+
isConnected(): boolean;
|
|
1142
|
+
private handleInboundFrame;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
/**
|
|
1146
|
+
* Cloudflare-Workers-compatible Groth16 ZK proof verifier.
|
|
1147
|
+
*
|
|
1148
|
+
* Drives the BN128 WASM module's exported pairing functions directly. Does NOT
|
|
1149
|
+
* use snarkjs/ffjavascript — those depend on runtime features (most notably
|
|
1150
|
+
* `URL.createObjectURL`) that CF Workers don't expose. See ROADMAP / CLAUDE.md.
|
|
1151
|
+
*
|
|
1152
|
+
* Two ways to initialize:
|
|
1153
|
+
* 1. `await initBn128Wasm()` — uses the bundled bn128.wasm (base64-inlined at
|
|
1154
|
+
* build time via `@rollup/plugin-wasm`). Works in any modern JS runtime
|
|
1155
|
+
* that allows runtime `WebAssembly.compile()` — Node, browsers, most
|
|
1156
|
+
* Workers configurations.
|
|
1157
|
+
* 2. `await initBn128Wasm(myWasmModule)` — accepts a pre-compiled
|
|
1158
|
+
* `WebAssembly.Module`. Lets a CF-Workers consumer keep the native pattern
|
|
1159
|
+
* of importing `.wasm` so wrangler precompiles it at deploy time and the
|
|
1160
|
+
* worker never pays for runtime compilation.
|
|
1161
|
+
*/
|
|
1162
|
+
|
|
1163
|
+
interface Bn128WasmInstance {
|
|
1164
|
+
instance: WebAssembly.Instance;
|
|
1165
|
+
memory: WebAssembly.Memory;
|
|
1166
|
+
initialPFree: number;
|
|
1167
|
+
}
|
|
1168
|
+
/**
|
|
1169
|
+
* Initialize the BN128 WASM module.
|
|
1170
|
+
*
|
|
1171
|
+
* @param wasmModule Optional pre-compiled `WebAssembly.Module`. If omitted, the
|
|
1172
|
+
* bundled bn128.wasm (base64-inlined by `@rollup/plugin-wasm` at build time)
|
|
1173
|
+
* is used. Passing your own module is the recommended path inside CF Workers,
|
|
1174
|
+
* where wrangler can precompile a `.wasm` import at deploy time and avoid the
|
|
1175
|
+
* runtime compile cost.
|
|
1176
|
+
*/
|
|
1177
|
+
declare function initBn128Wasm(wasmModule?: WebAssembly.Module): Promise<Bn128WasmInstance>;
|
|
1178
|
+
/**
|
|
1179
|
+
* Verify a Groth16 proof.
|
|
1180
|
+
*
|
|
1181
|
+
* Returns `false` (rather than throwing) for any structural problem — malformed
|
|
1182
|
+
* proof, out-of-range field elements, off-curve points, or a failed pairing.
|
|
1183
|
+
* Throws only for runtime/WASM faults.
|
|
1184
|
+
*/
|
|
1185
|
+
declare function verifyGroth16(wasmInstance: WebAssembly.Instance, memory: WebAssembly.Memory, initialPFree: number, verificationKey: VerificationKey, proof: Groth16Proof, publicSignals: string[]): Promise<boolean>;
|
|
1186
|
+
|
|
1187
|
+
export { AES_GCM_IV_BYTES, AES_GCM_KEY_BYTES, AES_GCM_TAG_BYTES, BroadcastChannel, BroadcastChannelEvents, ChunkCipher, DoubleRatchet, EncryptedSession, EventCore, EventCoreEvents, KeyStore, Message, Message as MessageDefault, PBKDF2_DEFAULT_ITERATIONS, PREIMAGE_POK_VERIFICATION_KEY, Packet, SerializeMessage, WSTransport, decorators, decryptAesGcm, deriveAesKeyFromPassphrase, deriveBitsHkdf, encryptAesGcm, getSubtle, initBn128Wasm, randomBytes, unwrapWithPassphrase, verifyGroth16, wrapWithPassphrase };
|
|
1188
|
+
export type { ACL, ACLEntry, AuthKeyPair, AuthType, Bn128WasmInstance, BroadcastChannelEvent, BroadcastChannelOptions, ChunkKeyMaterial, CipherFrame, CipherMessage, CipherMessageHeader, DehydratedKeypair, DehydratedKeys, ECDHCurve, Groth16Proof, IncomingFrame, KeyExchangeFrame, KeyPair, Keypair, MessageBody, MessageEvent, MessageHeaders, MessageOptions, OAuthProvider, PacketOptions, Participant, Permission, PermissionCheckRequest, PermissionCheckResult, PublicKeyJWK, RatchetState, ReceiveResult, Role, Session, SessionOptions, Subscription, UserIdentity, VerificationKey, WSMessage, WSMessageType, WSTransportOptions, WrappedPayload };
|