@muhkoo/connect 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +365 -0
- package/dist/browser/index.js +5636 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/connect.d.ts +2548 -0
- package/dist/server/index.js +5841 -0
- package/dist/server/index.js.map +1 -0
- package/dist/workers/index.js +2528 -0
- package/dist/workers/index.js.map +1 -0
- package/package.json +127 -0
|
@@ -0,0 +1,2548 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @public
|
|
3
|
+
* @remarks
|
|
4
|
+
* The body type is a generic type that can be any value. Internally we handle the serialization and deserialization of the body.
|
|
5
|
+
*/
|
|
6
|
+
type MessageBody = any;
|
|
7
|
+
/**
|
|
8
|
+
* @public
|
|
9
|
+
* @remarks
|
|
10
|
+
* 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.
|
|
11
|
+
*/
|
|
12
|
+
type MessageHeaders = Map<string, string>;
|
|
13
|
+
/**
|
|
14
|
+
* @public
|
|
15
|
+
* @remarks
|
|
16
|
+
* The message options type is a record object that contains the options for the message.
|
|
17
|
+
*/
|
|
18
|
+
interface MessageOptions {
|
|
19
|
+
id?: string;
|
|
20
|
+
body: MessageBody;
|
|
21
|
+
status?: "pending" | "processed" | "failed" | "delivered";
|
|
22
|
+
checksum?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* @public
|
|
26
|
+
* @remarks
|
|
27
|
+
* The message class is a simple class that represents a message object.
|
|
28
|
+
* @sealed
|
|
29
|
+
*/
|
|
30
|
+
declare class Message {
|
|
31
|
+
id: string;
|
|
32
|
+
timestamp: number;
|
|
33
|
+
status?: "pending" | "processed" | "failed" | "delivered";
|
|
34
|
+
checksum?: string;
|
|
35
|
+
private _body;
|
|
36
|
+
constructor(msg: MessageBody);
|
|
37
|
+
constructor(msg: MessageOptions);
|
|
38
|
+
get body(): MessageBody;
|
|
39
|
+
set body(value: MessageBody);
|
|
40
|
+
verifyChecksum: () => void;
|
|
41
|
+
private validatebodySize;
|
|
42
|
+
serialize: () => string;
|
|
43
|
+
static deserialize: (data: string) => Message;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type Address = string;
|
|
47
|
+
interface PacketOptions {
|
|
48
|
+
id?: string;
|
|
49
|
+
subject: string;
|
|
50
|
+
source: Address;
|
|
51
|
+
target: Address;
|
|
52
|
+
headers?: {
|
|
53
|
+
[key: string]: string | number | boolean | undefined;
|
|
54
|
+
};
|
|
55
|
+
message?: Message;
|
|
56
|
+
ttl?: number;
|
|
57
|
+
timestamp?: number;
|
|
58
|
+
signature?: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* @public
|
|
62
|
+
* @remarks
|
|
63
|
+
* This class represents a message object that can be sent over the network
|
|
64
|
+
* @sealed
|
|
65
|
+
*/
|
|
66
|
+
declare class Packet {
|
|
67
|
+
id: string;
|
|
68
|
+
subject: string;
|
|
69
|
+
source: Address;
|
|
70
|
+
target: Address;
|
|
71
|
+
headers?: PacketOptions['headers'];
|
|
72
|
+
message?: Message;
|
|
73
|
+
timestamp: number;
|
|
74
|
+
ttl?: number;
|
|
75
|
+
signature?: string;
|
|
76
|
+
constructor({ id, subject, source, target, headers, message, ttl, signature, timestamp }: PacketOptions);
|
|
77
|
+
serialize(): string;
|
|
78
|
+
static deserialize(data: string): Packet;
|
|
79
|
+
isExpired(): boolean;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
declare function SerializeMessage(target: any, propertyKey: string, descriptor: PropertyDescriptor): PropertyDescriptor;
|
|
83
|
+
|
|
84
|
+
declare const decorators: {
|
|
85
|
+
SerializeMessage: typeof SerializeMessage;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Shared cryptographic types for Connect and Accelerator
|
|
90
|
+
*/
|
|
91
|
+
/**
|
|
92
|
+
* Supported ECDH curves
|
|
93
|
+
*/
|
|
94
|
+
type ECDHCurve = 'P-256' | 'P-384';
|
|
95
|
+
/**
|
|
96
|
+
* JSON Web Key format for public keys
|
|
97
|
+
*/
|
|
98
|
+
interface PublicKeyJWK {
|
|
99
|
+
kty: 'EC';
|
|
100
|
+
crv: 'P-256' | 'P-384';
|
|
101
|
+
x: string;
|
|
102
|
+
y: string;
|
|
103
|
+
ext?: boolean;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Keypair with dehydrated public key (JWK) and CryptoKey private key
|
|
107
|
+
*/
|
|
108
|
+
interface Keypair {
|
|
109
|
+
publicKey: PublicKeyJWK;
|
|
110
|
+
privateKey: CryptoKey;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Dehydrated keypair (both keys as strings for transport/storage)
|
|
114
|
+
*/
|
|
115
|
+
interface DehydratedKeypair {
|
|
116
|
+
publicKey: string;
|
|
117
|
+
privateKey: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Shared identity types for Connect and Accelerator
|
|
122
|
+
*/
|
|
123
|
+
/**
|
|
124
|
+
* User identity - public key is the primary identifier
|
|
125
|
+
*/
|
|
126
|
+
interface UserIdentity {
|
|
127
|
+
publicKey: string;
|
|
128
|
+
accountType: 'self-sovereign' | 'custodial';
|
|
129
|
+
did?: string;
|
|
130
|
+
provider?: string;
|
|
131
|
+
createdAt: number;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Session
|
|
135
|
+
*/
|
|
136
|
+
interface Session {
|
|
137
|
+
sessionId: string;
|
|
138
|
+
publicKey: string;
|
|
139
|
+
appPublicKey: string;
|
|
140
|
+
createdAt: number;
|
|
141
|
+
expiresAt: number;
|
|
142
|
+
sharedSecret?: string;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Authentication types
|
|
146
|
+
*/
|
|
147
|
+
type AuthType = 'self-sovereign' | 'custodial';
|
|
148
|
+
/**
|
|
149
|
+
* OAuth provider types
|
|
150
|
+
*/
|
|
151
|
+
type OAuthProvider = 'google' | 'github' | 'discord' | 'twitter';
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Shared permission types for Connect and Accelerator
|
|
155
|
+
*/
|
|
156
|
+
/**
|
|
157
|
+
* Permission types for resources
|
|
158
|
+
*/
|
|
159
|
+
type Permission = 'read' | 'write' | 'delete' | 'admin' | 'share' | 'invite';
|
|
160
|
+
/**
|
|
161
|
+
* Role types
|
|
162
|
+
*/
|
|
163
|
+
type Role = 'owner' | 'admin' | 'editor' | 'viewer' | 'member';
|
|
164
|
+
/**
|
|
165
|
+
* ACL Entry - grants permissions to a user (identified by public key)
|
|
166
|
+
*/
|
|
167
|
+
interface ACLEntry {
|
|
168
|
+
subject: string;
|
|
169
|
+
permissions: Permission[];
|
|
170
|
+
role?: Role;
|
|
171
|
+
expiresAt?: number;
|
|
172
|
+
conditions?: {
|
|
173
|
+
ipAllowlist?: string[];
|
|
174
|
+
ipBlocklist?: string[];
|
|
175
|
+
validAfter?: number;
|
|
176
|
+
validBefore?: number;
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Access Control List for a resource
|
|
181
|
+
*/
|
|
182
|
+
interface ACL {
|
|
183
|
+
resource: string;
|
|
184
|
+
entries: Record<string, ACLEntry>;
|
|
185
|
+
defaultPermissions?: Permission[];
|
|
186
|
+
publicPermissions?: Permission[];
|
|
187
|
+
inheritFromParent?: boolean;
|
|
188
|
+
createdAt: number;
|
|
189
|
+
updatedAt: number;
|
|
190
|
+
createdBy: string;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Participant in a shared space
|
|
194
|
+
*/
|
|
195
|
+
interface Participant {
|
|
196
|
+
publicKey: string;
|
|
197
|
+
role: Role;
|
|
198
|
+
addedAt: number;
|
|
199
|
+
addedBy: string;
|
|
200
|
+
customPermissions?: Permission[];
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Permission check request
|
|
204
|
+
*/
|
|
205
|
+
interface PermissionCheckRequest {
|
|
206
|
+
publicKey: string;
|
|
207
|
+
resource: string;
|
|
208
|
+
requiredPermissions: Permission | Permission[];
|
|
209
|
+
context?: {
|
|
210
|
+
timestamp?: number;
|
|
211
|
+
ipAddress?: string;
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Permission check result
|
|
216
|
+
*/
|
|
217
|
+
interface PermissionCheckResult {
|
|
218
|
+
granted: boolean;
|
|
219
|
+
reason?: string;
|
|
220
|
+
grantedBy?: string;
|
|
221
|
+
permissions: Permission[];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Shared messaging types for Connect and Accelerator
|
|
226
|
+
*/
|
|
227
|
+
/**
|
|
228
|
+
* Message event for pub/sub messaging
|
|
229
|
+
*/
|
|
230
|
+
interface MessageEvent {
|
|
231
|
+
type: string;
|
|
232
|
+
topic: string;
|
|
233
|
+
data: any;
|
|
234
|
+
senderPublicKey?: string;
|
|
235
|
+
timestamp: number;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Message subscription
|
|
239
|
+
*/
|
|
240
|
+
interface Subscription {
|
|
241
|
+
connectionId: string;
|
|
242
|
+
publicKey: string;
|
|
243
|
+
topic: string;
|
|
244
|
+
filters?: any;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* WebSocket message types
|
|
248
|
+
*/
|
|
249
|
+
type WSMessageType = 'connect' | 'connected' | 'subscribe' | 'subscribed' | 'unsubscribe' | 'publish' | 'event' | 'message' | 'error';
|
|
250
|
+
/**
|
|
251
|
+
* WebSocket message
|
|
252
|
+
*/
|
|
253
|
+
interface WSMessage {
|
|
254
|
+
type: WSMessageType;
|
|
255
|
+
topic?: string;
|
|
256
|
+
data?: any;
|
|
257
|
+
publicKey?: string;
|
|
258
|
+
connectionId?: string;
|
|
259
|
+
timestamp?: number;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Shared zero-knowledge types and circuit constants.
|
|
264
|
+
*
|
|
265
|
+
* Used by both the browser/server builds (where snarkjs handles proof generation
|
|
266
|
+
* and verification) and the workers build (where verification runs through
|
|
267
|
+
* `bn128.wasm` via `src/workers/groth16-verifier.ts`). Keeping these here ensures
|
|
268
|
+
* proof shapes and verification keys stay identical across runtimes.
|
|
269
|
+
*/
|
|
270
|
+
/**
|
|
271
|
+
* Groth16 proof structure (BN128).
|
|
272
|
+
*
|
|
273
|
+
* - `pi_a`: G1 point as `[x, y]` or projective `[x, y, z]`.
|
|
274
|
+
* - `pi_b`: G2 point as `[[x0, x1], [y0, y1]]` or projective `[..., [z0, z1]]`.
|
|
275
|
+
* - `pi_c`: G1 point as `[x, y]` or projective `[x, y, z]`.
|
|
276
|
+
*
|
|
277
|
+
* Field elements are decimal-string-encoded BigInts (snarkjs convention).
|
|
278
|
+
*/
|
|
279
|
+
interface Groth16Proof {
|
|
280
|
+
pi_a: string[];
|
|
281
|
+
pi_b: string[][];
|
|
282
|
+
pi_c: string[];
|
|
283
|
+
protocol?: string;
|
|
284
|
+
curve?: string;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Groth16 verification key (BN128).
|
|
288
|
+
*
|
|
289
|
+
* Field elements are decimal-string-encoded BigInts (snarkjs convention).
|
|
290
|
+
*/
|
|
291
|
+
interface VerificationKey {
|
|
292
|
+
protocol: string;
|
|
293
|
+
curve: string;
|
|
294
|
+
nPublic: number;
|
|
295
|
+
vk_alpha_1: string[];
|
|
296
|
+
vk_beta_2: string[][];
|
|
297
|
+
vk_gamma_2: string[][];
|
|
298
|
+
vk_delta_2: string[][];
|
|
299
|
+
vk_alphabeta_12: string[][][];
|
|
300
|
+
IC: string[][];
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Verification key for the `preimagePoK` circuit (Poseidon-commitment
|
|
304
|
+
* proof-of-knowledge with three public signals: commitment, nonce, ecdsaPubHash).
|
|
305
|
+
*
|
|
306
|
+
* Pinned here so worker-side and SDK-side verification agree on the key being
|
|
307
|
+
* checked. If the circuit is recompiled, regenerate this constant and
|
|
308
|
+
* `public/circuits/build/preimagePoK_verification_key.json` together.
|
|
309
|
+
*/
|
|
310
|
+
declare const PREIMAGE_POK_VERIFICATION_KEY: VerificationKey;
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* WebCrypto subtle-interface resolver.
|
|
314
|
+
*
|
|
315
|
+
* Available natively in every runtime this SDK targets (modern browsers,
|
|
316
|
+
* Node 16+, CF Workers). Centralized here so the runtime check + error
|
|
317
|
+
* message are consistent across every primitive — and so a future polyfill
|
|
318
|
+
* shim has one place to plug in.
|
|
319
|
+
*/
|
|
320
|
+
declare function getSubtle(): SubtleCrypto;
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Cryptographically secure random bytes.
|
|
324
|
+
*
|
|
325
|
+
* Wraps `crypto.getRandomValues` with two small affordances every caller
|
|
326
|
+
* needs:
|
|
327
|
+
* - Allocates over a fresh, non-shared `ArrayBuffer` so the returned
|
|
328
|
+
* `Uint8Array` satisfies WebCrypto's `BufferSource` type under strict
|
|
329
|
+
* TS lib settings (where `Uint8Array<ArrayBufferLike>` isn't directly
|
|
330
|
+
* assignable).
|
|
331
|
+
* - Chunks requests larger than 65 KiB, the per-call cap browsers enforce
|
|
332
|
+
* on `getRandomValues`.
|
|
333
|
+
*/
|
|
334
|
+
declare function randomBytes(n: number): Uint8Array;
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* AES-256-GCM encryption / decryption with raw key bytes.
|
|
338
|
+
*
|
|
339
|
+
* The two functions here are the single canonical AES-GCM call site in the
|
|
340
|
+
* SDK. Earlier copies of this logic existed inline in `DoubleRatchet`, in the
|
|
341
|
+
* storage `ChunkCipher`, and in the passphrase-wrap helpers — they all now
|
|
342
|
+
* route through here so the algorithm, IV size, and tag handling stay in one
|
|
343
|
+
* place.
|
|
344
|
+
*
|
|
345
|
+
* Conventions:
|
|
346
|
+
* - 256-bit keys only. Passing 16- or 24-byte keys throws; callers that
|
|
347
|
+
* need AES-128 should add a dedicated primitive.
|
|
348
|
+
* - 96-bit IVs. WebCrypto's recommended GCM IV length; trades the 4-byte
|
|
349
|
+
* IV economy of 128-bit for compatibility with every spec-compliant
|
|
350
|
+
* implementation.
|
|
351
|
+
* - The 16-byte GCM authentication tag is appended to the ciphertext by
|
|
352
|
+
* WebCrypto's default behavior. Callers don't manage it separately.
|
|
353
|
+
*
|
|
354
|
+
* If a future need arises for AAD (associated data), add an optional `aad`
|
|
355
|
+
* parameter to both functions rather than introducing a parallel primitive.
|
|
356
|
+
*/
|
|
357
|
+
/** Length of the AES key in bytes. */
|
|
358
|
+
declare const AES_GCM_KEY_BYTES = 32;
|
|
359
|
+
/** Length of the AES-GCM IV in bytes (WebCrypto-recommended). */
|
|
360
|
+
declare const AES_GCM_IV_BYTES = 12;
|
|
361
|
+
/** Length of the AES-GCM authentication tag in bytes (appended to ciphertext). */
|
|
362
|
+
declare const AES_GCM_TAG_BYTES = 16;
|
|
363
|
+
/**
|
|
364
|
+
* Encrypt `plaintext` under a 32-byte AES-256 key with the supplied 12-byte
|
|
365
|
+
* IV. Returns ciphertext with the 16-byte GCM tag appended (so the output
|
|
366
|
+
* length is `plaintext.length + 16`).
|
|
367
|
+
*
|
|
368
|
+
* The IV must be unique per (key, message). Callers that generate the IV
|
|
369
|
+
* randomly should grab it via `randomBytes(AES_GCM_IV_BYTES)`; callers
|
|
370
|
+
* managing a counter pass their own.
|
|
371
|
+
*/
|
|
372
|
+
declare function encryptAesGcm(key: Uint8Array, iv: Uint8Array, plaintext: Uint8Array): Promise<Uint8Array>;
|
|
373
|
+
/**
|
|
374
|
+
* Inverse of {@link encryptAesGcm}. Throws if the GCM auth tag fails — this
|
|
375
|
+
* captures "wrong key", "wrong IV", or "ciphertext was tampered with",
|
|
376
|
+
* indistinguishably. Callers that care about distinguishing those should
|
|
377
|
+
* keep separate provenance bookkeeping, not invent a parallel MAC.
|
|
378
|
+
*/
|
|
379
|
+
declare function decryptAesGcm(key: Uint8Array, iv: Uint8Array, ciphertext: Uint8Array): Promise<Uint8Array>;
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Key-derivation primitives — PBKDF2 (for passphrase → key) and HKDF (for
|
|
383
|
+
* symmetric key material → expanded key material).
|
|
384
|
+
*
|
|
385
|
+
* PBKDF2 is the slow, salted KDF the passphrase-wrap helper uses to harden
|
|
386
|
+
* user passwords against offline guessing. HKDF is the fast, deterministic
|
|
387
|
+
* KDF the Double Ratchet uses to derive root + chain + message keys from
|
|
388
|
+
* shared secrets.
|
|
389
|
+
*
|
|
390
|
+
* Both are SHA-256-backed everywhere in this codebase — keep it that way
|
|
391
|
+
* unless there's a concrete reason to diverge.
|
|
392
|
+
*/
|
|
393
|
+
/**
|
|
394
|
+
* Iteration count default for PBKDF2 passphrase hardening. Tuned for
|
|
395
|
+
* 2024–2026 hardware: slow enough to deter offline guessing of weak
|
|
396
|
+
* passphrases (~100ms on commodity laptops), fast enough that interactive
|
|
397
|
+
* unlock stays sub-second.
|
|
398
|
+
*/
|
|
399
|
+
declare const PBKDF2_DEFAULT_ITERATIONS = 200000;
|
|
400
|
+
/**
|
|
401
|
+
* Derive a 256-bit AES-GCM key from `passphrase + salt` via PBKDF2-SHA256.
|
|
402
|
+
*
|
|
403
|
+
* Returned as raw bytes (not a `CryptoKey`) so callers can hand it straight
|
|
404
|
+
* to {@link encryptAesGcm} — that's the convention everywhere else in the
|
|
405
|
+
* primitive layer. If a future caller needs the non-extractable
|
|
406
|
+
* `CryptoKey` form, add a parallel `deriveCryptoKeyFromPassphrase` variant
|
|
407
|
+
* rather than overloading this one.
|
|
408
|
+
*/
|
|
409
|
+
declare function deriveAesKeyFromPassphrase(passphrase: string, salt: Uint8Array, iterations?: number): Promise<Uint8Array>;
|
|
410
|
+
/**
|
|
411
|
+
* HKDF-Expand with SHA-256, returning `lengthBytes` raw bytes of key material.
|
|
412
|
+
*
|
|
413
|
+
* `info` is the per-purpose label (e.g. `"DoubleRatchetMsg"`). Optional
|
|
414
|
+
* `salt` defaults to an empty buffer, matching the way the Double Ratchet
|
|
415
|
+
* code has always used it.
|
|
416
|
+
*
|
|
417
|
+
* Note: this is intentionally a one-shot Extract-then-Expand call rather than
|
|
418
|
+
* exposing Extract and Expand separately — every call site in this codebase
|
|
419
|
+
* wants the same shape, and a richer API can be added later if needed.
|
|
420
|
+
*/
|
|
421
|
+
declare function deriveBitsHkdf(inputKeyMaterial: Uint8Array, info: string, lengthBytes: number, salt?: Uint8Array): Promise<Uint8Array>;
|
|
422
|
+
|
|
423
|
+
interface KeyPair {
|
|
424
|
+
privateKey: CryptoKey | null;
|
|
425
|
+
publicKey: CryptoKey;
|
|
426
|
+
}
|
|
427
|
+
interface AuthKeyPair {
|
|
428
|
+
privateKey: CryptoKey | null;
|
|
429
|
+
publicKey: CryptoKey;
|
|
430
|
+
}
|
|
431
|
+
interface DehydratedKeys {
|
|
432
|
+
ecdhPub: string;
|
|
433
|
+
ecdhPriv: string;
|
|
434
|
+
ecdsaPub: string;
|
|
435
|
+
ecdsaPriv: string;
|
|
436
|
+
}
|
|
437
|
+
declare class KeyStore {
|
|
438
|
+
private static instance;
|
|
439
|
+
private _keys;
|
|
440
|
+
private _authKeys;
|
|
441
|
+
private constructor();
|
|
442
|
+
static getInstance(): KeyStore;
|
|
443
|
+
get keys(): Map<string, KeyPair>;
|
|
444
|
+
get authKeys(): Map<string, AuthKeyPair>;
|
|
445
|
+
generateOwnKeyPair(id: string): Promise<KeyPair>;
|
|
446
|
+
storeRemotePublicKeys(id: string, ecdhPublicKey: CryptoKey, ecdsaPublicKey: CryptoKey): Promise<void>;
|
|
447
|
+
hydrateKeyPair(id: string, dehydrated: DehydratedKeys): Promise<void>;
|
|
448
|
+
getKeyPair(id: string): KeyPair | null;
|
|
449
|
+
getAuthKeyPair(id: string): AuthKeyPair | null;
|
|
450
|
+
dehydrateKeyPair(id: string): Promise<DehydratedKeys>;
|
|
451
|
+
getRawEcdsaPublicKey(id: string): Promise<Uint8Array | null>;
|
|
452
|
+
packDehydratedKeys(id: string): Promise<string>;
|
|
453
|
+
hydrateFromPacked(id: string, masterKey: string): Promise<void>;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
interface CipherMessageHeader {
|
|
457
|
+
dhPub: string;
|
|
458
|
+
prevChainLength: number;
|
|
459
|
+
messageNumber: number;
|
|
460
|
+
sessionId: string;
|
|
461
|
+
sessionType: 'global' | 'specific';
|
|
462
|
+
senderId: string;
|
|
463
|
+
recipientId: string;
|
|
464
|
+
timestamp: number;
|
|
465
|
+
signature: string;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
interface CipherMessage {
|
|
469
|
+
header: CipherMessageHeader;
|
|
470
|
+
ciphertext: string; // Hex-encoded
|
|
471
|
+
nonce: string; // Hex-encoded
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// interface RatchetState {
|
|
475
|
+
// clientDhPriv: Uint8Array;
|
|
476
|
+
// clientDhPub: Uint8Array;
|
|
477
|
+
// serverDhPriv: Uint8Array;
|
|
478
|
+
// serverDhPub: Uint8Array;
|
|
479
|
+
// rootKey: Uint8Array | null;
|
|
480
|
+
// sendChainKey: Uint8Array | null;
|
|
481
|
+
// recvChainKey: Uint8Array | null;
|
|
482
|
+
// sendCount: number;
|
|
483
|
+
// recvCount: number;
|
|
484
|
+
// prevChainLength: number;
|
|
485
|
+
// skippedKeys: Map<number, Uint8Array>;
|
|
486
|
+
// }
|
|
487
|
+
|
|
488
|
+
// interface RatchetState {
|
|
489
|
+
// clientDhPriv: CryptoKey | null; // ECDH private key
|
|
490
|
+
// clientDhPub: CryptoKey; //
|
|
491
|
+
// serverDhPriv: CryptoKey | null; // ECDH private key
|
|
492
|
+
// serverDhPub: CryptoKey; // ECDH public key
|
|
493
|
+
// rootKey: CryptoKey | null; // AES-GCM key
|
|
494
|
+
// sendChainKey: CryptoKey | null; // AES-GCM key for sending
|
|
495
|
+
// recvChainKey: CryptoKey | null; // AES-GCM key for receiving
|
|
496
|
+
// sendCount: number; // Number of messages sent
|
|
497
|
+
// recvCount: number; // Number of messages received
|
|
498
|
+
// prevChainLength: number; // Length of the previous chain
|
|
499
|
+
// skippedKeys: Map<number, CryptoKey>; // Skipped keys for messages
|
|
500
|
+
// }
|
|
501
|
+
|
|
502
|
+
interface RatchetState {
|
|
503
|
+
clientDhPriv: CryptoKey | null;
|
|
504
|
+
clientDhPub: CryptoKey ;
|
|
505
|
+
serverDhPriv: CryptoKey | null;
|
|
506
|
+
serverDhPub: CryptoKey ;
|
|
507
|
+
// Symmetric key material (root key + chain keys + skipped message keys).
|
|
508
|
+
// Stored as Uint8Array rather than Node's Buffer so this type works in the
|
|
509
|
+
// browser/Workers builds.
|
|
510
|
+
rootKey: Uint8Array | null;
|
|
511
|
+
sendChainKey: Uint8Array | null;
|
|
512
|
+
recvChainKey: Uint8Array | null;
|
|
513
|
+
sendCount: number;
|
|
514
|
+
recvCount: number;
|
|
515
|
+
prevChainLength: number;
|
|
516
|
+
currentSkippedKeys: Map<number, Uint8Array>;
|
|
517
|
+
oldSkippedMessageKeys: Map<string, { skips: Map<number, Uint8Array>; created: number }>;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
declare class DoubleRatchet {
|
|
521
|
+
private state;
|
|
522
|
+
private sessionType;
|
|
523
|
+
private maxSkip;
|
|
524
|
+
private overlapPeriod;
|
|
525
|
+
private windowSize;
|
|
526
|
+
constructor(senderId: string, recipientId: string, sessionType: 'global' | 'specific', isClient?: boolean);
|
|
527
|
+
private hkdf;
|
|
528
|
+
initializeSession(isClient?: boolean): Promise<void>;
|
|
529
|
+
private dhRatchet;
|
|
530
|
+
private symmetricRatchet;
|
|
531
|
+
encrypt(plaintext: string, newDhKey: boolean | undefined, senderId: string, recipientId: string, sessionId: string, sessionType: 'global' | 'specific'): Promise<CipherMessage>;
|
|
532
|
+
private cleanOldSkippedKeys;
|
|
533
|
+
decrypt(message: CipherMessage, isClient?: boolean): Promise<string>;
|
|
534
|
+
getState(): RatchetState;
|
|
535
|
+
setState(state: RatchetState): void;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
type SnarkProof = Groth16Proof;
|
|
539
|
+
|
|
540
|
+
interface ZkCompiled {
|
|
541
|
+
verificationKey: VerificationKey;
|
|
542
|
+
}
|
|
543
|
+
declare class AuthPublicInput {
|
|
544
|
+
commitment: string;
|
|
545
|
+
nonce: string;
|
|
546
|
+
ecdsaPubHash: string;
|
|
547
|
+
constructor(commitment: string, nonce: string, ecdsaPubHash: string);
|
|
548
|
+
toPublicSignals(): string[];
|
|
549
|
+
}
|
|
550
|
+
declare class Field {
|
|
551
|
+
private value;
|
|
552
|
+
static readonly FIELD_SIZE: bigint;
|
|
553
|
+
constructor(value: bigint | string | number);
|
|
554
|
+
toBigInt(): bigint;
|
|
555
|
+
toString(): string;
|
|
556
|
+
toHex(): string;
|
|
557
|
+
equals(other: Field): boolean;
|
|
558
|
+
static fromHex(hex: string): Field;
|
|
559
|
+
}
|
|
560
|
+
declare class Poseidon {
|
|
561
|
+
static hash(inputs: Field[]): Promise<Field>;
|
|
562
|
+
}
|
|
563
|
+
declare function encodeToHex(field: Field): string;
|
|
564
|
+
declare function decodeFromHex(hexStr: string): Field;
|
|
565
|
+
interface CircuitBufferConfig {
|
|
566
|
+
wasm: Uint8Array;
|
|
567
|
+
zkey: Uint8Array;
|
|
568
|
+
verificationKey: VerificationKey;
|
|
569
|
+
}
|
|
570
|
+
declare class HashKnowledge {
|
|
571
|
+
private static wasmBuffer?;
|
|
572
|
+
private static zkeyBuffer?;
|
|
573
|
+
private static verificationKey?;
|
|
574
|
+
static setCircuitBuffers(wasm: Uint8Array, zkey: Uint8Array, verificationKey: VerificationKey): void;
|
|
575
|
+
static compile(): Promise<ZkCompiled>;
|
|
576
|
+
static prove(secret: Field): Promise<{
|
|
577
|
+
proof: SnarkProof;
|
|
578
|
+
publicSignals: string[];
|
|
579
|
+
}>;
|
|
580
|
+
static verify(proof: SnarkProof, publicSignals?: string[]): Promise<boolean>;
|
|
581
|
+
}
|
|
582
|
+
declare class PreimagePoK {
|
|
583
|
+
private static wasmBuffer?;
|
|
584
|
+
private static zkeyBuffer?;
|
|
585
|
+
private static verificationKey?;
|
|
586
|
+
static setCircuitBuffers(wasm: Uint8Array, zkey: Uint8Array, verificationKey: VerificationKey): void;
|
|
587
|
+
static compile(): Promise<ZkCompiled>;
|
|
588
|
+
static prove(publicInput: AuthPublicInput, secret: Field, salt: Field, ecdsaPub: Field): Promise<{
|
|
589
|
+
proof: SnarkProof;
|
|
590
|
+
publicSignals: string[];
|
|
591
|
+
}>;
|
|
592
|
+
static verify(proof: SnarkProof, publicSignals?: string[]): Promise<boolean>;
|
|
593
|
+
}
|
|
594
|
+
declare function verifyHashKnowledge(proof: SnarkProof, expectedOutput?: Field, publicSignals?: string[]): Promise<boolean>;
|
|
595
|
+
declare function verifyPreimagePoK(proof: SnarkProof, publicSignals?: string[]): Promise<boolean>;
|
|
596
|
+
declare function verify(programType: 'HashKnowledge' | 'PreimagePoK', proof: SnarkProof, options?: {
|
|
597
|
+
expectedOutput?: Field;
|
|
598
|
+
publicSignals?: string[];
|
|
599
|
+
}): Promise<boolean>;
|
|
600
|
+
declare function quickVerify(program: typeof HashKnowledge | typeof PreimagePoK, proof: SnarkProof, publicSignals?: string[]): Promise<boolean>;
|
|
601
|
+
declare function initializeCircuits(config: {
|
|
602
|
+
hashKnowledge?: CircuitBufferConfig;
|
|
603
|
+
preimagePoK?: CircuitBufferConfig;
|
|
604
|
+
}): void;
|
|
605
|
+
declare function compilePrograms(): Promise<{
|
|
606
|
+
hashKnowledge: ZkCompiled;
|
|
607
|
+
preimagePoK: ZkCompiled;
|
|
608
|
+
}>;
|
|
609
|
+
|
|
610
|
+
interface AuthToken {
|
|
611
|
+
peerId: string;
|
|
612
|
+
timestamp: number;
|
|
613
|
+
signature: string;
|
|
614
|
+
}
|
|
615
|
+
declare class Authenticator {
|
|
616
|
+
private trustedKeys;
|
|
617
|
+
private zkVerificationKey;
|
|
618
|
+
initializeZK(): Promise<void>;
|
|
619
|
+
getZKVerificationKey(): VerificationKey | null;
|
|
620
|
+
addTrustedServer(serverId: string, publicKey: CryptoKey): void;
|
|
621
|
+
generateAuthToken(peerId: string, privateKey: CryptoKey): Promise<AuthToken>;
|
|
622
|
+
verifyAuthToken(token: AuthToken, clientPublicKey: CryptoKey): Promise<boolean>;
|
|
623
|
+
verifyZKProof(proof: any, publicInput: AuthPublicInput, storedCommitment: Field, ecdsaPub: CryptoKey): Promise<boolean>;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
declare class DoubleRatchetManager {
|
|
627
|
+
private sessions;
|
|
628
|
+
private sessionStates;
|
|
629
|
+
private keyStore;
|
|
630
|
+
private _authenticator;
|
|
631
|
+
private registeredUsers;
|
|
632
|
+
private id;
|
|
633
|
+
constructor(id: string);
|
|
634
|
+
get authenticator(): Authenticator;
|
|
635
|
+
addTrustedServer(serverId: string, publicKey: CryptoKey): Promise<void>;
|
|
636
|
+
registerZK(clientId: string, secret: Field, salt: Field, ecdsaPub: CryptoKey): Promise<void>;
|
|
637
|
+
performHandshake(senderId: string, recipientId: string, zkProof: any, publicInput: AuthPublicInput, clientPublicKey: CryptoKey, // ECDH pub for session
|
|
638
|
+
clientAuthPublicKey: CryptoKey, // ECDSA pub for auth
|
|
639
|
+
authToken: AuthToken): Promise<void>;
|
|
640
|
+
initializeSession(senderId: string, recipientId: string, isClient?: boolean, sessionType?: 'global' | 'specific', sessionId?: string, zkProof?: any, publicInput?: AuthPublicInput, clientPublicKey?: CryptoKey, clientAuthPublicKey?: CryptoKey, clientEcdhPrivateKey?: CryptoKey, clientAuthPrivateKey?: CryptoKey): Promise<string>;
|
|
641
|
+
encrypt(senderId: string, recipientId: string, sessionId: string, plaintext: string, newDhKey?: boolean, sessionType?: 'global' | 'specific'): Promise<CipherMessage>;
|
|
642
|
+
decrypt(message: CipherMessage, isClient?: boolean): Promise<string>;
|
|
643
|
+
getSessionSharedSecret(sessionId: string): Promise<Uint8Array | null>;
|
|
644
|
+
private saveState;
|
|
645
|
+
private loadState;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Per-chunk AES-256-GCM encryption with random keys and IVs.
|
|
650
|
+
*
|
|
651
|
+
* Each chunk of a stored file gets its own freshly generated 256-bit AES key
|
|
652
|
+
* and 96-bit IV. The (key, IV) pair lives in the file manifest, which the
|
|
653
|
+
* application is responsible for storing under whatever access-control regime
|
|
654
|
+
* it uses (in this codebase: the `SharedSpaceClient` writes manifests into
|
|
655
|
+
* ZK-gated multi-user spaces; the shard store itself is open).
|
|
656
|
+
*
|
|
657
|
+
* Per-chunk keys (rather than a per-file DEK) lets future versions revoke or
|
|
658
|
+
* re-share individual chunks without re-encrypting the whole file. The
|
|
659
|
+
* additional manifest size is dominated by the shard hash list anyway.
|
|
660
|
+
*
|
|
661
|
+
* The cipher itself delegates to the canonical `encryptAesGcm` / `decryptAesGcm`
|
|
662
|
+
* primitives — this class only wraps them with key-material generation and
|
|
663
|
+
* the JSON-friendly base64 serialization the manifest needs.
|
|
664
|
+
*/
|
|
665
|
+
/**
|
|
666
|
+
* Per-chunk key material, base64-encoded for JSON-friendly storage in the
|
|
667
|
+
* manifest. The IV is unique per chunk; the key is unique per chunk too in
|
|
668
|
+
* this codebase, though that's a stricter property than AES-GCM strictly
|
|
669
|
+
* requires (which is "unique IV per key").
|
|
670
|
+
*/
|
|
671
|
+
interface ChunkKeyMaterial {
|
|
672
|
+
/** Base64-encoded 32-byte AES-256 key. */
|
|
673
|
+
key: string;
|
|
674
|
+
/** Base64-encoded 12-byte AES-GCM IV. */
|
|
675
|
+
iv: string;
|
|
676
|
+
}
|
|
677
|
+
declare class ChunkCipher {
|
|
678
|
+
/**
|
|
679
|
+
* Generate a fresh random (key, iv) pair encoded for the manifest.
|
|
680
|
+
* Callers should generate one of these per chunk before calling
|
|
681
|
+
* {@link encryptChunk}.
|
|
682
|
+
*/
|
|
683
|
+
generateKeyMaterial(): ChunkKeyMaterial;
|
|
684
|
+
/**
|
|
685
|
+
* Encrypt `plaintext` under the supplied key + IV. Returns the AES-GCM
|
|
686
|
+
* ciphertext with the 16-byte auth tag appended (WebCrypto's default
|
|
687
|
+
* GCM behavior).
|
|
688
|
+
*/
|
|
689
|
+
encryptChunk(plaintext: Uint8Array, material: ChunkKeyMaterial): Promise<Uint8Array>;
|
|
690
|
+
/**
|
|
691
|
+
* Inverse of {@link encryptChunk}. Throws if the auth tag fails — that
|
|
692
|
+
* means the ciphertext was tampered with, the key/iv was wrong, or the
|
|
693
|
+
* shards were reassembled incorrectly.
|
|
694
|
+
*/
|
|
695
|
+
decryptChunk(ciphertext: Uint8Array, material: ChunkKeyMaterial): Promise<Uint8Array>;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Passphrase-based wrap / unwrap helpers using WebCrypto.
|
|
700
|
+
*
|
|
701
|
+
* Encrypt a payload with a user-supplied passphrase before persisting it
|
|
702
|
+
* somewhere the server can see (e.g. the manifest blob in a ZK-gated
|
|
703
|
+
* SharedSpace, or chat-key material in a PersonalSpace). The server never
|
|
704
|
+
* sees the passphrase or the plaintext — both wrap and unwrap run entirely
|
|
705
|
+
* client-side.
|
|
706
|
+
*
|
|
707
|
+
* Algorithm (the only one this codebase has ever shipped):
|
|
708
|
+
* - PBKDF2-SHA256 with 200_000 iterations derives a 256-bit AES key from
|
|
709
|
+
* `passphrase || salt`.
|
|
710
|
+
* - AES-256-GCM encrypts the plaintext with a fresh random 12-byte IV.
|
|
711
|
+
* - The auth tag is appended to the ciphertext by WebCrypto's default
|
|
712
|
+
* GCM behaviour, so {@link WrappedPayload.ciphertext} carries it implicitly.
|
|
713
|
+
*
|
|
714
|
+
* The returned {@link WrappedPayload} is JSON-friendly (all base64 fields), so
|
|
715
|
+
* it can be `JSON.stringify`-ed straight into a KV layer.
|
|
716
|
+
*/
|
|
717
|
+
/**
|
|
718
|
+
* The persisted, JSON-friendly representation of a passphrase-wrapped blob.
|
|
719
|
+
*
|
|
720
|
+
* The `alg` field is a self-describing marker — if we ever rotate KDF/cipher,
|
|
721
|
+
* unwrap can switch on this value to keep older payloads readable.
|
|
722
|
+
*/
|
|
723
|
+
interface WrappedPayload {
|
|
724
|
+
/** Random per-wrap salt for PBKDF2, base64. */
|
|
725
|
+
salt: string;
|
|
726
|
+
/** AES-GCM IV, base64. */
|
|
727
|
+
iv: string;
|
|
728
|
+
/** AES-GCM ciphertext (includes 16-byte GCM auth tag), base64. */
|
|
729
|
+
ciphertext: string;
|
|
730
|
+
/** Algorithm marker so future readers know what they're looking at. */
|
|
731
|
+
alg: "PBKDF2-SHA256/AES-256-GCM";
|
|
732
|
+
/** PBKDF2 iteration count used. */
|
|
733
|
+
iter: number;
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Encrypt `plaintext` under a key derived from `passphrase`. The returned
|
|
737
|
+
* payload bundles the salt, IV, and ciphertext so unwrap only needs the
|
|
738
|
+
* passphrase to reverse the operation.
|
|
739
|
+
*/
|
|
740
|
+
declare function wrapWithPassphrase(passphrase: string, plaintext: Uint8Array): Promise<WrappedPayload>;
|
|
741
|
+
/**
|
|
742
|
+
* Decrypt a {@link WrappedPayload} produced by {@link wrapWithPassphrase}.
|
|
743
|
+
* Throws if the passphrase is wrong, the payload is tampered with, or the
|
|
744
|
+
* algorithm marker is unrecognised.
|
|
745
|
+
*/
|
|
746
|
+
declare function unwrapWithPassphrase(passphrase: string, wrapped: WrappedPayload): Promise<Uint8Array>;
|
|
747
|
+
|
|
748
|
+
declare enum EventCoreEvents {
|
|
749
|
+
CONNECTED = "connected",
|
|
750
|
+
DISCONNECTED = "disconnected",
|
|
751
|
+
RECONNECTING = "reconnecting",
|
|
752
|
+
ERROR = "error",
|
|
753
|
+
DATA_RECEIVED = "data_received",
|
|
754
|
+
DATA_SENT = "data_sent",
|
|
755
|
+
MESSAGE = "message",
|
|
756
|
+
GET_HISTORY = "get_history",
|
|
757
|
+
RECEIVED_HISTORY = "received_history"
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* @public
|
|
761
|
+
* EventCore is a static event emitter with singleton functionality for global event handling.
|
|
762
|
+
*/
|
|
763
|
+
declare class EventCore {
|
|
764
|
+
private static eventTarget;
|
|
765
|
+
private static events;
|
|
766
|
+
/**
|
|
767
|
+
* @remarks Listen to an event.
|
|
768
|
+
* @example EventCore.on('event', (e) =\> appLogger.debug(e));
|
|
769
|
+
*/
|
|
770
|
+
static on(event: EventCoreEvents, handler: EventListener | CallableFunction): void;
|
|
771
|
+
/**
|
|
772
|
+
* @remarks Stop listening to an event.
|
|
773
|
+
* @example EventCore.off('event', handler);
|
|
774
|
+
*/
|
|
775
|
+
static off(event: EventCoreEvents, handler: EventListener | CallableFunction): void;
|
|
776
|
+
/**
|
|
777
|
+
* @remarks Emit an event.
|
|
778
|
+
* @example EventCore.emit('event', data);
|
|
779
|
+
*/
|
|
780
|
+
static emit(event: EventCoreEvents, data: any): void;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* An end-to-end-encrypted communication session for one local identity
|
|
785
|
+
* (`myId`) talking to an arbitrary set of peers. Wraps `KeyStore` +
|
|
786
|
+
* per-pair `DoubleRatchet` instances and owns the protocol bookkeeping
|
|
787
|
+
* (handshake reciprocation, dedup, role assignment, broadcast fan-out,
|
|
788
|
+
* recipient filtering) so application code doesn't have to.
|
|
789
|
+
*
|
|
790
|
+
* Transport-agnostic by design: the Session never touches a WebSocket
|
|
791
|
+
* (or HTTP, or anything else). Application code:
|
|
792
|
+
* - calls `session.encrypt(text)` and gets back an array of frames it
|
|
793
|
+
* ships over whatever transport it has;
|
|
794
|
+
* - calls `session.receive(frame)` with each inbound frame and acts on
|
|
795
|
+
* the typed result (plaintext, handshake-done, ignored).
|
|
796
|
+
*
|
|
797
|
+
* Frames are plain JSON objects with the same shape the existing chat
|
|
798
|
+
* protocol uses (`{ keyExchange: {...} }` and `{ cipherMessage: {...} }`),
|
|
799
|
+
* so this can drop into a chat app without changing the wire format.
|
|
800
|
+
*/
|
|
801
|
+
|
|
802
|
+
interface SessionOptions {
|
|
803
|
+
/** This client's identifier (e.g. username / DID / public-key string). */
|
|
804
|
+
myId: string;
|
|
805
|
+
}
|
|
806
|
+
/** A handshake frame the application ships to peers (or receives from them). */
|
|
807
|
+
interface KeyExchangeFrame {
|
|
808
|
+
type: "handshake" | "update";
|
|
809
|
+
userId: string;
|
|
810
|
+
/** Base64-encoded JWK of the sender's ECDH public key. */
|
|
811
|
+
ecdhPublicKey: string;
|
|
812
|
+
/** Base64-encoded JWK of the sender's ECDSA public key. */
|
|
813
|
+
ecdsaPublicKey: string;
|
|
814
|
+
}
|
|
815
|
+
/** A ciphertext frame, addressed to exactly one peer. */
|
|
816
|
+
interface CipherFrame {
|
|
817
|
+
cipherMessage: CipherMessage;
|
|
818
|
+
}
|
|
819
|
+
/** Anything inbound the application might hand to `session.receive()`. */
|
|
820
|
+
type IncomingFrame = {
|
|
821
|
+
keyExchange: KeyExchangeFrame;
|
|
822
|
+
} | {
|
|
823
|
+
cipherMessage: CipherMessage;
|
|
824
|
+
} | Record<string, unknown>;
|
|
825
|
+
/** Outcome of feeding one frame through `session.receive()`. */
|
|
826
|
+
type ReceiveResult = {
|
|
827
|
+
kind: "plaintext";
|
|
828
|
+
/** The sender's id (from the cipherMessage header). */
|
|
829
|
+
from: string;
|
|
830
|
+
/** Decrypted message body. */
|
|
831
|
+
text: string;
|
|
832
|
+
/** The original cipherMessage, for callers that want it. */
|
|
833
|
+
cipherMessage: CipherMessage;
|
|
834
|
+
} | {
|
|
835
|
+
kind: "handshake";
|
|
836
|
+
/** The peer whose handshake was just processed. */
|
|
837
|
+
peerId: string;
|
|
838
|
+
/**
|
|
839
|
+
* If non-null, the application MUST send this frame back to the peer
|
|
840
|
+
* (e.g. broadcast on the room WebSocket). It's our reciprocation —
|
|
841
|
+
* giving the peer our public keys so they can verify our messages.
|
|
842
|
+
*
|
|
843
|
+
* The Session dedups internally: we only emit this on the first
|
|
844
|
+
* handshake we process for a given peer in this Session's lifetime.
|
|
845
|
+
*/
|
|
846
|
+
outbound: {
|
|
847
|
+
keyExchange: KeyExchangeFrame;
|
|
848
|
+
} | null;
|
|
849
|
+
} | {
|
|
850
|
+
kind: "ignored";
|
|
851
|
+
/** Why we ignored the frame (debugging aid; not for branching on). */
|
|
852
|
+
reason: string;
|
|
853
|
+
};
|
|
854
|
+
declare class EncryptedSession {
|
|
855
|
+
private readonly myId;
|
|
856
|
+
private readonly keyStore;
|
|
857
|
+
/** One DoubleRatchet per peer pair. */
|
|
858
|
+
private readonly ratchets;
|
|
859
|
+
/** Peers we've already sent our keyExchange to in this Session. */
|
|
860
|
+
private readonly sentHandshakeTo;
|
|
861
|
+
/** Resolved when own keys are generated and ready to use. */
|
|
862
|
+
private readonly _ready;
|
|
863
|
+
private _resolveReady;
|
|
864
|
+
private _initialized;
|
|
865
|
+
constructor(opts: SessionOptions);
|
|
866
|
+
/** A promise that resolves once `initialize()` has completed. */
|
|
867
|
+
get ready(): Promise<void>;
|
|
868
|
+
/** Our local identifier. */
|
|
869
|
+
get id(): string;
|
|
870
|
+
/**
|
|
871
|
+
* Generate this client's ECDH + ECDSA keypair (idempotent — safe to call
|
|
872
|
+
* multiple times across reconnects; only the first call generates).
|
|
873
|
+
*/
|
|
874
|
+
initialize(): Promise<void>;
|
|
875
|
+
/**
|
|
876
|
+
* Build the keyExchange frame this client should send to peers when it
|
|
877
|
+
* comes online. Application is responsible for actually transmitting it
|
|
878
|
+
* (e.g. ws.send(JSON.stringify(frame))).
|
|
879
|
+
*/
|
|
880
|
+
getOwnKeyExchange(): Promise<{
|
|
881
|
+
keyExchange: KeyExchangeFrame;
|
|
882
|
+
}>;
|
|
883
|
+
/**
|
|
884
|
+
* Encrypt a plaintext to every peer we currently have a ratchet for, and
|
|
885
|
+
* return one cipherMessage frame per recipient. The application sends
|
|
886
|
+
* each frame over its transport.
|
|
887
|
+
*
|
|
888
|
+
* If there are no peers yet (or all peers are still mid-handshake), the
|
|
889
|
+
* returned array may be empty — the caller should typically render the
|
|
890
|
+
* outgoing message locally in that case (since there's no one to send
|
|
891
|
+
* it to yet).
|
|
892
|
+
*/
|
|
893
|
+
encrypt(plaintext: string): Promise<CipherFrame[]>;
|
|
894
|
+
/**
|
|
895
|
+
* Hand an inbound frame to the session. Returns a typed result the
|
|
896
|
+
* application acts on.
|
|
897
|
+
*
|
|
898
|
+
* - `kind: 'plaintext'` — decrypted; render `result.text`
|
|
899
|
+
* - `kind: 'handshake'` — a peer's keys are now stored, and if
|
|
900
|
+
* `result.outbound` is set, send it
|
|
901
|
+
* - `kind: 'ignored'` — not for us, malformed, duplicate, etc.
|
|
902
|
+
*
|
|
903
|
+
* Calling code should always handle `'ignored'` (no UI side effect).
|
|
904
|
+
*/
|
|
905
|
+
receive(frame: IncomingFrame): Promise<ReceiveResult>;
|
|
906
|
+
/** List of peer ids we have an established ratchet for. */
|
|
907
|
+
peers(): string[];
|
|
908
|
+
/** Has the handshake completed for this peer? */
|
|
909
|
+
hasRatchetFor(peerId: string): boolean;
|
|
910
|
+
/**
|
|
911
|
+
* Tear down a peer's ratchet (e.g. when they leave the room). The peer's
|
|
912
|
+
* stored keys remain in `KeyStore` so a fresh handshake can be set up
|
|
913
|
+
* later without re-fetching them, but the ratchet state is dropped.
|
|
914
|
+
*/
|
|
915
|
+
forgetPeer(peerId: string): void;
|
|
916
|
+
/** Deterministic per-pair session id — same string on both sides. */
|
|
917
|
+
private _sessionIdFor;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* Pure WebSocket transport — owns the socket lifecycle and nothing else.
|
|
922
|
+
*
|
|
923
|
+
* Hands raw frame strings up (via the `MESSAGE` event) and accepts raw frame
|
|
924
|
+
* strings down (via `send`). Anything frame-shape, encryption, or serialization
|
|
925
|
+
* related belongs in the layer above (e.g. `Network` for client↔server
|
|
926
|
+
* encrypted pipes, `BroadcastChannel` for multi-peer E2E rooms).
|
|
927
|
+
*
|
|
928
|
+
* Responsibilities:
|
|
929
|
+
* - Open / close the WebSocket
|
|
930
|
+
* - Auto-reconnect with linear backoff after an unexpected close
|
|
931
|
+
* - Buffer outbound frames while disconnected (capped queue)
|
|
932
|
+
* - Emit lifecycle events (CONNECTED / DISCONNECTED / RECONNECTING / ERROR)
|
|
933
|
+
* plus raw inbound MESSAGE events
|
|
934
|
+
*/
|
|
935
|
+
|
|
936
|
+
interface WSTransportOptions {
|
|
937
|
+
/** WebSocket URL, e.g. `ws://localhost:8787/foo`. */
|
|
938
|
+
url: string;
|
|
939
|
+
/** Reconnect automatically after unexpected disconnect (default `true`). */
|
|
940
|
+
autoReconnect?: boolean;
|
|
941
|
+
/** Delay between reconnect attempts in ms (default `3000`). */
|
|
942
|
+
reconnectDelay?: number;
|
|
943
|
+
/** Cap on reconnect attempts (default `5`; `0` means unlimited). */
|
|
944
|
+
maxReconnectAttempts?: number;
|
|
945
|
+
/**
|
|
946
|
+
* Max outbound frames buffered while disconnected. Sending past this caps
|
|
947
|
+
* throws synchronously. Default `100`.
|
|
948
|
+
*/
|
|
949
|
+
maxQueueSize?: number;
|
|
950
|
+
}
|
|
951
|
+
declare class WSTransport extends EventCore {
|
|
952
|
+
emit: typeof EventCore.emit;
|
|
953
|
+
on: typeof EventCore.on;
|
|
954
|
+
off: typeof EventCore.off;
|
|
955
|
+
private url;
|
|
956
|
+
private socket;
|
|
957
|
+
private _isConnected;
|
|
958
|
+
private _isConnecting;
|
|
959
|
+
private autoReconnect;
|
|
960
|
+
private reconnectDelay;
|
|
961
|
+
private maxReconnectAttempts;
|
|
962
|
+
private reconnectAttempts;
|
|
963
|
+
private reconnectTimeout;
|
|
964
|
+
private outboundQueue;
|
|
965
|
+
private maxQueueSize;
|
|
966
|
+
constructor(opts: WSTransportOptions);
|
|
967
|
+
/** Open the socket. Resolves when CONNECTED fires. Rejects on first error. */
|
|
968
|
+
connect(): Promise<void>;
|
|
969
|
+
/** Close the socket and stop reconnecting. */
|
|
970
|
+
disconnect(): void;
|
|
971
|
+
/**
|
|
972
|
+
* Send a raw frame. If currently disconnected, the frame is queued and sent
|
|
973
|
+
* after the next successful reconnect (up to `maxQueueSize`).
|
|
974
|
+
*/
|
|
975
|
+
send(frame: string): void;
|
|
976
|
+
isConnected(): boolean;
|
|
977
|
+
isConnecting(): boolean;
|
|
978
|
+
queuedFrames(): number;
|
|
979
|
+
reconnectAttemptCount(): number;
|
|
980
|
+
private scheduleReconnect;
|
|
981
|
+
private flushQueue;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/**
|
|
985
|
+
* BroadcastChannel — a turnkey end-to-end encrypted channel for multi-peer
|
|
986
|
+
* "room"-style apps (e.g. a chat). Wires together:
|
|
987
|
+
*
|
|
988
|
+
* - `WSTransport` for socket lifecycle, reconnect, and offline frame queue
|
|
989
|
+
* - `EncryptedSession` for per-peer Double Ratchet state, handshake
|
|
990
|
+
* reciprocation, fan-out encryption, and recipient filtering
|
|
991
|
+
*
|
|
992
|
+
* The host app only deals with high-level events and method calls; it never
|
|
993
|
+
* touches `new WebSocket(...)` or builds cipherMessage frames by hand.
|
|
994
|
+
*
|
|
995
|
+
* Wire protocol (kept compatible with the existing chat protocol so this
|
|
996
|
+
* drops into the deployed Accelerator without server changes):
|
|
997
|
+
*
|
|
998
|
+
* outbound: `{ keyExchange: {...} }` — produced by us on `announce()`
|
|
999
|
+
* `{ cipherMessage: {...} }` — produced by `send(text)`,
|
|
1000
|
+
* one frame per peer
|
|
1001
|
+
* arbitrary JSON — sent via `sendRaw(obj)`
|
|
1002
|
+
*
|
|
1003
|
+
* inbound: `{ keyExchange: {...} }` — routed into EncryptedSession;
|
|
1004
|
+
* emits `peer_handshake`
|
|
1005
|
+
* `{ cipherMessage: {...} }` — decrypted; emits `message`
|
|
1006
|
+
* anything else — passes through as `raw_frame`
|
|
1007
|
+
* (the app decides what to do)
|
|
1008
|
+
*
|
|
1009
|
+
* **Event scoping.** Unlike `Network`, BroadcastChannel uses a per-instance
|
|
1010
|
+
* `EventTarget`, so two channels (e.g. two rooms in the same app) don't
|
|
1011
|
+
* cross-contaminate each other's events.
|
|
1012
|
+
*
|
|
1013
|
+
* Note: this class shadows the `BroadcastChannel` Web API global (cross-tab
|
|
1014
|
+
* messaging) only when imported by name. They're unrelated.
|
|
1015
|
+
*/
|
|
1016
|
+
|
|
1017
|
+
/**
|
|
1018
|
+
* Event names BroadcastChannel emits. Lifecycle events reuse the
|
|
1019
|
+
* `EventCoreEvents` string values (`connected`, `disconnected`, etc.) so
|
|
1020
|
+
* apps can use shared constants if they want. Channel-specific events are
|
|
1021
|
+
* prefixed `channel:` to avoid colliding with other event systems in the
|
|
1022
|
+
* same app.
|
|
1023
|
+
*/
|
|
1024
|
+
declare const BroadcastChannelEvents: {
|
|
1025
|
+
readonly CONNECTED: EventCoreEvents.CONNECTED;
|
|
1026
|
+
readonly DISCONNECTED: EventCoreEvents.DISCONNECTED;
|
|
1027
|
+
readonly RECONNECTING: EventCoreEvents.RECONNECTING;
|
|
1028
|
+
readonly ERROR: EventCoreEvents.ERROR;
|
|
1029
|
+
/** A peer's ratchet is now ready. `detail.peerId` is the peer's id. */
|
|
1030
|
+
readonly PEER_HANDSHAKE: "channel:peer_handshake";
|
|
1031
|
+
/** Decrypted message. `detail` is `{ from, text, cipherMessage }`. */
|
|
1032
|
+
readonly MESSAGE: "channel:message";
|
|
1033
|
+
/** Inbound frame BroadcastChannel didn't recognize. App handles it. */
|
|
1034
|
+
readonly RAW_FRAME: "channel:raw_frame";
|
|
1035
|
+
};
|
|
1036
|
+
type BroadcastChannelEvent = (typeof BroadcastChannelEvents)[keyof typeof BroadcastChannelEvents];
|
|
1037
|
+
interface BroadcastChannelOptions extends WSTransportOptions {
|
|
1038
|
+
/** This client's identifier (username, DID, public-key string, etc.). */
|
|
1039
|
+
myId: string;
|
|
1040
|
+
/**
|
|
1041
|
+
* If true (default false), automatically send our keyExchange as soon as
|
|
1042
|
+
* the socket is connected. For chat-style apps that send a `{name}` frame
|
|
1043
|
+
* first, leave this false and call `announce()` after that handshake.
|
|
1044
|
+
*/
|
|
1045
|
+
autoAnnounce?: boolean;
|
|
1046
|
+
}
|
|
1047
|
+
declare class BroadcastChannel {
|
|
1048
|
+
readonly transport: WSTransport;
|
|
1049
|
+
readonly session: EncryptedSession;
|
|
1050
|
+
readonly myId: string;
|
|
1051
|
+
private autoAnnounce;
|
|
1052
|
+
private announced;
|
|
1053
|
+
/** Per-instance EventTarget — events do NOT leak across BroadcastChannels. */
|
|
1054
|
+
private events;
|
|
1055
|
+
constructor(opts: BroadcastChannelOptions);
|
|
1056
|
+
/** Subscribe to a channel event. Handler receives a CustomEvent. */
|
|
1057
|
+
on(event: BroadcastChannelEvent | string, handler: (e: CustomEvent) => void): void;
|
|
1058
|
+
/** Unsubscribe. */
|
|
1059
|
+
off(event: BroadcastChannelEvent | string, handler: (e: CustomEvent) => void): void;
|
|
1060
|
+
private emit;
|
|
1061
|
+
/** Generate our keys (idempotent) and open the WebSocket. */
|
|
1062
|
+
connect(): Promise<void>;
|
|
1063
|
+
/** Close the WebSocket and stop reconnecting. */
|
|
1064
|
+
disconnect(): void;
|
|
1065
|
+
/**
|
|
1066
|
+
* Send our `keyExchange` frame to the room. Idempotent — only sends on the
|
|
1067
|
+
* first call. Call this once your higher-level handshake (e.g. sending a
|
|
1068
|
+
* `{name}` identification frame) has completed.
|
|
1069
|
+
*/
|
|
1070
|
+
announce(): Promise<void>;
|
|
1071
|
+
/**
|
|
1072
|
+
* Encrypt `plaintext` to every peer we have a ratchet with, and send each
|
|
1073
|
+
* cipherMessage as its own WS frame. Returns the number of peers we sent to
|
|
1074
|
+
* (0 if no peers are handshaken yet — in which case the caller should
|
|
1075
|
+
* typically render the message locally for the sender and try again later).
|
|
1076
|
+
*/
|
|
1077
|
+
send(plaintext: string): Promise<number>;
|
|
1078
|
+
/**
|
|
1079
|
+
* Send an arbitrary JSON-serializable frame as-is. The app uses this for
|
|
1080
|
+
* any frame that isn't channel-managed (e.g. a chat's `{name}`, `{file}`,
|
|
1081
|
+
* or unencrypted `{message}`).
|
|
1082
|
+
*/
|
|
1083
|
+
sendRaw(frame: unknown): void;
|
|
1084
|
+
/** List of peer ids we have an established ratchet for. */
|
|
1085
|
+
peers(): string[];
|
|
1086
|
+
/** Drop a peer's ratchet (e.g. when they leave the room). */
|
|
1087
|
+
forgetPeer(peerId: string): void;
|
|
1088
|
+
isConnected(): boolean;
|
|
1089
|
+
private handleInboundFrame;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
/**
|
|
1093
|
+
* Cloudflare-Workers-compatible Groth16 ZK proof verifier.
|
|
1094
|
+
*
|
|
1095
|
+
* Drives the BN128 WASM module's exported pairing functions directly. Does NOT
|
|
1096
|
+
* use snarkjs/ffjavascript — those depend on runtime features (most notably
|
|
1097
|
+
* `URL.createObjectURL`) that CF Workers don't expose. See ROADMAP / CLAUDE.md.
|
|
1098
|
+
*
|
|
1099
|
+
* Two ways to initialize:
|
|
1100
|
+
* 1. `await initBn128Wasm()` — uses the bundled bn128.wasm (base64-inlined at
|
|
1101
|
+
* build time via `@rollup/plugin-wasm`). Works in any modern JS runtime
|
|
1102
|
+
* that allows runtime `WebAssembly.compile()` — Node, browsers, most
|
|
1103
|
+
* Workers configurations.
|
|
1104
|
+
* 2. `await initBn128Wasm(myWasmModule)` — accepts a pre-compiled
|
|
1105
|
+
* `WebAssembly.Module`. Lets a CF-Workers consumer keep the native pattern
|
|
1106
|
+
* of importing `.wasm` so wrangler precompiles it at deploy time and the
|
|
1107
|
+
* worker never pays for runtime compilation.
|
|
1108
|
+
*/
|
|
1109
|
+
|
|
1110
|
+
interface Bn128WasmInstance {
|
|
1111
|
+
instance: WebAssembly.Instance;
|
|
1112
|
+
memory: WebAssembly.Memory;
|
|
1113
|
+
initialPFree: number;
|
|
1114
|
+
}
|
|
1115
|
+
/**
|
|
1116
|
+
* Initialize the BN128 WASM module.
|
|
1117
|
+
*
|
|
1118
|
+
* @param wasmModule Optional pre-compiled `WebAssembly.Module`. If omitted, the
|
|
1119
|
+
* bundled bn128.wasm (base64-inlined by `@rollup/plugin-wasm` at build time)
|
|
1120
|
+
* is used. Passing your own module is the recommended path inside CF Workers,
|
|
1121
|
+
* where wrangler can precompile a `.wasm` import at deploy time and avoid the
|
|
1122
|
+
* runtime compile cost.
|
|
1123
|
+
*/
|
|
1124
|
+
declare function initBn128Wasm(wasmModule?: WebAssembly.Module): Promise<Bn128WasmInstance>;
|
|
1125
|
+
/**
|
|
1126
|
+
* Verify a Groth16 proof.
|
|
1127
|
+
*
|
|
1128
|
+
* Returns `false` (rather than throwing) for any structural problem — malformed
|
|
1129
|
+
* proof, out-of-range field elements, off-curve points, or a failed pairing.
|
|
1130
|
+
* Throws only for runtime/WASM faults.
|
|
1131
|
+
*/
|
|
1132
|
+
declare function verifyGroth16(wasmInstance: WebAssembly.Instance, memory: WebAssembly.Memory, initialPFree: number, verificationKey: VerificationKey, proof: Groth16Proof, publicSignals: string[]): Promise<boolean>;
|
|
1133
|
+
|
|
1134
|
+
/**
|
|
1135
|
+
* `PersonalSpaceClient` — the client-side counterpart to the accelerator's
|
|
1136
|
+
* `PersonalSpaceDO`. Wraps the `/api/personal/:commitment/*` HTTP protocol so
|
|
1137
|
+
* callers can `put` / `get` / `delete` / `list` per-user KV blobs without
|
|
1138
|
+
* thinking about challenges, nonces, or Groth16 proof generation.
|
|
1139
|
+
*
|
|
1140
|
+
* Every gated operation:
|
|
1141
|
+
* 1. Requests a one-shot challenge from the DO (`POST /challenge`).
|
|
1142
|
+
* 2. Converts the returned hex nonce into a BN254 field element.
|
|
1143
|
+
* 3. Generates a fresh Groth16 proof for the `preimagePoK` circuit using
|
|
1144
|
+
* snarkjs, with the user's secret/salt/ecdsaPub as private inputs and
|
|
1145
|
+
* `{commitment, nonce, ecdsaPubHash}` as public signals.
|
|
1146
|
+
* 4. POSTs the proof + publicSignals + (optional) value to the gated endpoint.
|
|
1147
|
+
*
|
|
1148
|
+
* snarkjs is loaded lazily (dynamic `import`) the first time a proof is
|
|
1149
|
+
* generated — it's an optional peer dependency that only the ZK-proof path
|
|
1150
|
+
* needs, so importing this module (or constructing a `Client`) never requires
|
|
1151
|
+
* it. Consumers using the proof path install `snarkjs`, or provide it via an
|
|
1152
|
+
* import map in the browser (see the README).
|
|
1153
|
+
*
|
|
1154
|
+
* Encryption of the stored values is the caller's responsibility — the DO
|
|
1155
|
+
* sees only opaque JSON. The companion `wrap.ts` helpers in this folder are
|
|
1156
|
+
* a convenient way to passphrase-encrypt a payload before putting it.
|
|
1157
|
+
*/
|
|
1158
|
+
/**
|
|
1159
|
+
* Constructor options for {@link PersonalSpaceClient}.
|
|
1160
|
+
*
|
|
1161
|
+
* All of `secret`, `salt`, `ecdsaPub`, and `ecdsaPubHash` are decimal-encoded
|
|
1162
|
+
* BigInt strings (snarkjs convention). `commitment` is likewise decimal — it's
|
|
1163
|
+
* the Poseidon hash of `(secret, salt, ecdsaPubHash)`.
|
|
1164
|
+
*/
|
|
1165
|
+
interface PersonalSpaceClientOptions {
|
|
1166
|
+
/** Base URL of the accelerator, e.g. `http://localhost:8787`. */
|
|
1167
|
+
baseUrl: string;
|
|
1168
|
+
/** The user's Poseidon commitment as a decimal string (~78 digits). */
|
|
1169
|
+
commitment: string;
|
|
1170
|
+
/** ZK identity material — private inputs to the proof. Decimal BigInt string. */
|
|
1171
|
+
secret: string;
|
|
1172
|
+
/** ZK identity salt. Decimal BigInt string. */
|
|
1173
|
+
salt: string;
|
|
1174
|
+
/** Raw ECDSA public-key field representation. Decimal BigInt string. */
|
|
1175
|
+
ecdsaPub: string;
|
|
1176
|
+
/** Poseidon hash of `ecdsaPub`, used as a public signal. Decimal BigInt string. */
|
|
1177
|
+
ecdsaPubHash: string;
|
|
1178
|
+
/** Where the runtime can fetch the compiled circuit assets. */
|
|
1179
|
+
circuits: {
|
|
1180
|
+
/** URL of the `preimagePoK.wasm` witness generator. */
|
|
1181
|
+
wasmUrl: string;
|
|
1182
|
+
/** URL of the `preimagePoK_0001.zkey` proving key. */
|
|
1183
|
+
zkeyUrl: string;
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
declare class PersonalSpaceClient {
|
|
1187
|
+
private readonly baseUrl;
|
|
1188
|
+
private readonly commitment;
|
|
1189
|
+
private readonly secret;
|
|
1190
|
+
private readonly salt;
|
|
1191
|
+
private readonly ecdsaPub;
|
|
1192
|
+
private readonly ecdsaPubHash;
|
|
1193
|
+
private readonly wasmUrl;
|
|
1194
|
+
private readonly zkeyUrl;
|
|
1195
|
+
constructor(opts: PersonalSpaceClientOptions);
|
|
1196
|
+
/** Store `value` under `key`, replacing any prior value. */
|
|
1197
|
+
put(key: string, value: unknown): Promise<void>;
|
|
1198
|
+
/** Fetch the value stored under `key`. Returns `null` if absent. */
|
|
1199
|
+
get<T = unknown>(key: string): Promise<T | null>;
|
|
1200
|
+
/** Remove `key` from the user's space. Returns whether the key existed. */
|
|
1201
|
+
delete(key: string): Promise<boolean>;
|
|
1202
|
+
/** List every key currently stored in the user's space. */
|
|
1203
|
+
list(): Promise<string[]>;
|
|
1204
|
+
/**
|
|
1205
|
+
* Run the full pre-flight: ask the DO for a challenge, reduce the nonce to
|
|
1206
|
+
* a field element, and generate a fresh Groth16 proof. Returns everything
|
|
1207
|
+
* the gated endpoints need in their body.
|
|
1208
|
+
*/
|
|
1209
|
+
private proveFreshChallenge;
|
|
1210
|
+
/** Hit `POST /challenge` and return the issued nonce + ID. */
|
|
1211
|
+
private requestChallenge;
|
|
1212
|
+
/**
|
|
1213
|
+
* Thin wrapper over `globalThis.fetch` so all methods share a single set of
|
|
1214
|
+
* headers and body-encoding behaviour. `globalThis.fetch` is available in
|
|
1215
|
+
* modern browsers, Node 18+, Workers, and Bun.
|
|
1216
|
+
*/
|
|
1217
|
+
private fetch;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
/**
|
|
1221
|
+
* Wire types for the storage layer.
|
|
1222
|
+
*
|
|
1223
|
+
* The `FileManifest` is the single piece of metadata the system needs to
|
|
1224
|
+
* reassemble a file: per-chunk encryption material, RS parameters, and the
|
|
1225
|
+
* SHA-256 content hashes of every shard that belongs to the chunk. With the
|
|
1226
|
+
* manifest in hand, a reader can pull each shard from the open content-
|
|
1227
|
+
* addressed shard store, decode, decrypt, and concatenate to recover the
|
|
1228
|
+
* original bytes.
|
|
1229
|
+
*
|
|
1230
|
+
* The manifest itself lives inside a `SharedSpaceDO` (Phase B) which gates
|
|
1231
|
+
* access by ZK proof + ACL. Shards are stored in the open content-addressed
|
|
1232
|
+
* `ShardStoreDO` — anyone can put/get any shard, but without the manifest
|
|
1233
|
+
* (which has the keys) the ciphertext is just noise.
|
|
1234
|
+
*/
|
|
1235
|
+
|
|
1236
|
+
/**
|
|
1237
|
+
* One chunk's metadata. A file is the concatenation of `chunks` in order.
|
|
1238
|
+
*
|
|
1239
|
+
* - `originalSize` is the chunk's plaintext length, before AES-GCM expanded
|
|
1240
|
+
* it by 16 bytes for the auth tag and before RS padded it for shard
|
|
1241
|
+
* alignment.
|
|
1242
|
+
* - `ciphertextSize` is the AES-GCM output length (plaintext + 16). This is
|
|
1243
|
+
* what the RS decoder trims back to.
|
|
1244
|
+
* - `shardHashes` lists every shard's SHA-256 in encoding order — the first
|
|
1245
|
+
* `dataShards` are data shards, the trailing `parityShards` are parity.
|
|
1246
|
+
*/
|
|
1247
|
+
interface ChunkManifest {
|
|
1248
|
+
/** Stable per-chunk ID (uuid-like). Useful for client-side resume / dedup. */
|
|
1249
|
+
id: string;
|
|
1250
|
+
/** Position of this chunk in the file (0-indexed). */
|
|
1251
|
+
chunkIndex: number;
|
|
1252
|
+
/** Plaintext byte length of the chunk before encryption. */
|
|
1253
|
+
originalSize: number;
|
|
1254
|
+
/** AES-GCM ciphertext byte length (originalSize + 16 for the tag). */
|
|
1255
|
+
ciphertextSize: number;
|
|
1256
|
+
/** Number of RS data shards. */
|
|
1257
|
+
dataShards: number;
|
|
1258
|
+
/** Number of RS parity shards. */
|
|
1259
|
+
parityShards: number;
|
|
1260
|
+
/** Length in bytes of every shard (data and parity are equal). */
|
|
1261
|
+
shardSize: number;
|
|
1262
|
+
/** SHA-256 (hex) of each shard's ciphertext bytes, in shard order. */
|
|
1263
|
+
shardHashes: string[];
|
|
1264
|
+
/** Per-chunk AES-256-GCM key + IV. Plain in the manifest — gating happens at the SpaceDO. */
|
|
1265
|
+
cipher: ChunkKeyMaterial;
|
|
1266
|
+
}
|
|
1267
|
+
/**
|
|
1268
|
+
* Top-level manifest for a single stored file. Persisted in the user's
|
|
1269
|
+
* SharedSpace by file id; readable / writable per the space's ACL.
|
|
1270
|
+
*/
|
|
1271
|
+
interface FileManifest {
|
|
1272
|
+
/** Stable file ID. Assigned by the SDK at write time; used to read or delete. */
|
|
1273
|
+
id: string;
|
|
1274
|
+
/** Human-readable filename (display only — not used by the shard store). */
|
|
1275
|
+
name: string;
|
|
1276
|
+
/** Total plaintext byte length. Sum of chunk `originalSize`s. */
|
|
1277
|
+
size: number;
|
|
1278
|
+
/** MIME type the application supplied at write time. Opaque to storage. */
|
|
1279
|
+
type: string;
|
|
1280
|
+
/** Optional virtual path / folder hint for the application. */
|
|
1281
|
+
path?: string;
|
|
1282
|
+
/** When the file was last modified per the application (ms epoch). */
|
|
1283
|
+
lastModified: number;
|
|
1284
|
+
/** When this manifest was created on the SDK side (ms epoch). */
|
|
1285
|
+
createdAt: number;
|
|
1286
|
+
/** Ordered list of chunks. Concatenate them in index order to rebuild the file. */
|
|
1287
|
+
chunks: ChunkManifest[];
|
|
1288
|
+
/** SDK schema version. Bump on breaking manifest-shape changes. */
|
|
1289
|
+
version: 1;
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Lightweight summary returned by `FileStorage.writeFile` and surfaced in
|
|
1293
|
+
* directory-style listings. The full manifest is only fetched when actually
|
|
1294
|
+
* reassembling the file.
|
|
1295
|
+
*/
|
|
1296
|
+
interface FileStat {
|
|
1297
|
+
id: string;
|
|
1298
|
+
name: string;
|
|
1299
|
+
size: number;
|
|
1300
|
+
type: string;
|
|
1301
|
+
path?: string;
|
|
1302
|
+
lastModified: number;
|
|
1303
|
+
createdAt: number;
|
|
1304
|
+
chunkCount: number;
|
|
1305
|
+
shardCount: number;
|
|
1306
|
+
}
|
|
1307
|
+
/** Distil a `FileManifest` into a `FileStat`. */
|
|
1308
|
+
declare function manifestToStat(manifest: FileManifest): FileStat;
|
|
1309
|
+
|
|
1310
|
+
/**
|
|
1311
|
+
* HTTP client for the open content-addressed shard store.
|
|
1312
|
+
*
|
|
1313
|
+
* Each shard is named by the lowercase hex SHA-256 of its ciphertext bytes.
|
|
1314
|
+
* The server verifies the hash on PUT, so the address authenticates the
|
|
1315
|
+
* contents — there's no benefit to ACLing PUT/GET, and making them open
|
|
1316
|
+
* enables cross-user dedup down the line.
|
|
1317
|
+
*
|
|
1318
|
+
* Wire protocol:
|
|
1319
|
+
* PUT /api/shards/:hash body: shard bytes (application/octet-stream)
|
|
1320
|
+
* 200 { ok: true, hash, size, dedup: boolean }
|
|
1321
|
+
* 400 if hash doesn't match body
|
|
1322
|
+
*
|
|
1323
|
+
* GET /api/shards/:hash 200 with shard bytes (application/octet-stream)
|
|
1324
|
+
* 404 if absent
|
|
1325
|
+
*
|
|
1326
|
+
* HEAD /api/shards/:hash 200 if present (no body) — used by callers to
|
|
1327
|
+
* skip a PUT they'd otherwise dedup-rewrite. The
|
|
1328
|
+
* server can ignore the optimization if it wants.
|
|
1329
|
+
*/
|
|
1330
|
+
interface ShardClientOptions {
|
|
1331
|
+
/** Base URL of the accelerator (e.g. `https://muhkoo-accelerator.workers.dev`). */
|
|
1332
|
+
baseUrl: string;
|
|
1333
|
+
/**
|
|
1334
|
+
* Path prefix for the shard endpoints, joined to `baseUrl` before the
|
|
1335
|
+
* hash. Defaults to `/api/shards`, which targets a global shard store
|
|
1336
|
+
* (e.g. a dedicated `ShardStoreDO`). Pass a room-scoped value like
|
|
1337
|
+
* `/api/spaces/{roomId}/shards` to keep shards bound to a specific
|
|
1338
|
+
* `SharedSpaceDO` — that's the path the chat application uses today.
|
|
1339
|
+
*
|
|
1340
|
+
* Leading slash optional; trailing slash stripped. The final URL is
|
|
1341
|
+
* always `${baseUrl}${normalizedPrefix}/${encodeURIComponent(hash)}`.
|
|
1342
|
+
*/
|
|
1343
|
+
pathPrefix?: string;
|
|
1344
|
+
/**
|
|
1345
|
+
* Optional custom fetch implementation. Defaults to `globalThis.fetch`.
|
|
1346
|
+
* Provided so callers can inject retries / auth / instrumentation.
|
|
1347
|
+
*/
|
|
1348
|
+
fetch?: typeof fetch;
|
|
1349
|
+
}
|
|
1350
|
+
/**
|
|
1351
|
+
* Compute the lowercase hex SHA-256 of `bytes`. Available via WebCrypto in
|
|
1352
|
+
* every runtime this SDK targets.
|
|
1353
|
+
*/
|
|
1354
|
+
declare function shardHash(bytes: Uint8Array): Promise<string>;
|
|
1355
|
+
declare class ShardClient {
|
|
1356
|
+
private readonly baseUrl;
|
|
1357
|
+
private readonly pathPrefix;
|
|
1358
|
+
private readonly fetchFn;
|
|
1359
|
+
constructor(opts: ShardClientOptions);
|
|
1360
|
+
/** Build the URL for a given shard hash — exposed for callers that want to log or pre-resolve. */
|
|
1361
|
+
urlForHash(hash: string): string;
|
|
1362
|
+
/**
|
|
1363
|
+
* Upload a shard. The server verifies SHA-256(body) matches `:hash` and
|
|
1364
|
+
* rejects with 400 if not, so the caller is the source of truth on
|
|
1365
|
+
* naming. Re-uploading an existing shard is idempotent — the server
|
|
1366
|
+
* returns `dedup: true` in that case.
|
|
1367
|
+
*/
|
|
1368
|
+
putShard(hash: string, bytes: Uint8Array): Promise<{
|
|
1369
|
+
dedup: boolean;
|
|
1370
|
+
}>;
|
|
1371
|
+
/**
|
|
1372
|
+
* Fetch a shard by hash. Throws if the server returns a non-200; returns
|
|
1373
|
+
* `null` for 404 so callers can distinguish "missing" from "errored".
|
|
1374
|
+
*/
|
|
1375
|
+
getShard(hash: string): Promise<Uint8Array | null>;
|
|
1376
|
+
/**
|
|
1377
|
+
* Check whether a shard exists. Lets the caller skip a PUT they'd otherwise
|
|
1378
|
+
* dedup-rewrite. The server can implement this as a HEAD or return a small
|
|
1379
|
+
* JSON body — we don't depend on either.
|
|
1380
|
+
*/
|
|
1381
|
+
hasShard(hash: string): Promise<boolean>;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
/**
|
|
1385
|
+
* HTTP client for the multi-user `SharedSpaceDO` (Phase B).
|
|
1386
|
+
*
|
|
1387
|
+
* Mirrors the proven shape of `PersonalSpaceClient` — every gated call:
|
|
1388
|
+
* 1. POSTs `/challenge` to get a fresh nonce keyed to the caller's commitment
|
|
1389
|
+
* 2. Generates a Groth16 proof for the `preimagePoK` circuit with the nonce
|
|
1390
|
+
* reduced into a BN254 field element as the public signal
|
|
1391
|
+
* 3. POSTs the operation with `{ challengeId, proof, publicSignals, ... }`
|
|
1392
|
+
*
|
|
1393
|
+
* What's different from `PersonalSpaceClient`:
|
|
1394
|
+
* - Addressed by space ID, not user commitment. Multiple users can be
|
|
1395
|
+
* members of the same space.
|
|
1396
|
+
* - The space carries a participants list + ACL (shared types from
|
|
1397
|
+
* `src/types/permissions.ts`). The server checks that the proving
|
|
1398
|
+
* commitment is a participant with the requested permission before each
|
|
1399
|
+
* mutation.
|
|
1400
|
+
* - Stores file manifests (not generic KV blobs). The shape is constrained
|
|
1401
|
+
* by `FileManifest` from `../types`.
|
|
1402
|
+
*
|
|
1403
|
+
* Wire protocol (this client implements the client side; the server side is
|
|
1404
|
+
* the next slice of work in accelerator):
|
|
1405
|
+
*
|
|
1406
|
+
* POST /api/spaces create a new space (gated by caller proof)
|
|
1407
|
+
* body: { challengeId, proof, publicSignals, initialAcl? }
|
|
1408
|
+
* returns: { spaceId }
|
|
1409
|
+
*
|
|
1410
|
+
* POST /api/spaces/:spaceId/challenge issue a nonce for the caller
|
|
1411
|
+
* body: { commitment }
|
|
1412
|
+
* returns: { challengeId, nonce }
|
|
1413
|
+
*
|
|
1414
|
+
* POST /api/spaces/:spaceId/files write a file manifest
|
|
1415
|
+
* body: { challengeId, proof, publicSignals, manifest }
|
|
1416
|
+
* returns: { ok: true, fileId }
|
|
1417
|
+
*
|
|
1418
|
+
* POST /api/spaces/:spaceId/files/:fileId/get read a file manifest
|
|
1419
|
+
* body: { challengeId, proof, publicSignals }
|
|
1420
|
+
* returns: { manifest }
|
|
1421
|
+
*
|
|
1422
|
+
* DELETE /api/spaces/:spaceId/files/:fileId delete a file manifest
|
|
1423
|
+
* body: { challengeId, proof, publicSignals }
|
|
1424
|
+
* returns: { ok: true, existed: boolean }
|
|
1425
|
+
*
|
|
1426
|
+
* POST /api/spaces/:spaceId/files/list list manifests in space
|
|
1427
|
+
* body: { challengeId, proof, publicSignals }
|
|
1428
|
+
* returns: { files: FileStat[] }
|
|
1429
|
+
*
|
|
1430
|
+
* POST /api/spaces/:spaceId/participants add a participant
|
|
1431
|
+
* body: { challengeId, proof, publicSignals, participant: Participant }
|
|
1432
|
+
* returns: { ok: true }
|
|
1433
|
+
*
|
|
1434
|
+
* DELETE /api/spaces/:spaceId/participants/:commitment remove a participant
|
|
1435
|
+
* body: { challengeId, proof, publicSignals }
|
|
1436
|
+
* returns: { ok: true, existed: boolean }
|
|
1437
|
+
*
|
|
1438
|
+
* POST /api/spaces/:spaceId/acl set / replace an ACL entry
|
|
1439
|
+
* body: { challengeId, proof, publicSignals, entry: ACLEntry }
|
|
1440
|
+
* returns: { ok: true }
|
|
1441
|
+
*/
|
|
1442
|
+
|
|
1443
|
+
interface SharedSpaceClientOptions {
|
|
1444
|
+
/** Base URL of the accelerator. */
|
|
1445
|
+
baseUrl: string;
|
|
1446
|
+
/** This caller's Poseidon commitment (decimal string). */
|
|
1447
|
+
commitment: string;
|
|
1448
|
+
/** Private witnesses for the `preimagePoK` circuit. */
|
|
1449
|
+
secret: string;
|
|
1450
|
+
salt: string;
|
|
1451
|
+
ecdsaPub: string;
|
|
1452
|
+
/** Public signal: Poseidon hash of `ecdsaPub`. */
|
|
1453
|
+
ecdsaPubHash: string;
|
|
1454
|
+
/** Compiled circuit assets — same files PersonalSpaceClient uses. */
|
|
1455
|
+
circuits: {
|
|
1456
|
+
wasmUrl: string;
|
|
1457
|
+
zkeyUrl: string;
|
|
1458
|
+
};
|
|
1459
|
+
/** Optional custom fetch (defaults to `globalThis.fetch`). */
|
|
1460
|
+
fetch?: typeof fetch;
|
|
1461
|
+
}
|
|
1462
|
+
declare class SharedSpaceClient {
|
|
1463
|
+
private readonly baseUrl;
|
|
1464
|
+
private readonly commitment;
|
|
1465
|
+
private readonly secret;
|
|
1466
|
+
private readonly salt;
|
|
1467
|
+
private readonly ecdsaPub;
|
|
1468
|
+
private readonly ecdsaPubHash;
|
|
1469
|
+
private readonly wasmUrl;
|
|
1470
|
+
private readonly zkeyUrl;
|
|
1471
|
+
private readonly fetchFn;
|
|
1472
|
+
constructor(opts: SharedSpaceClientOptions);
|
|
1473
|
+
/**
|
|
1474
|
+
* Create a brand new space. The caller becomes the owner; `initialAcl`
|
|
1475
|
+
* adds any additional ACL entries up front (e.g. inviting a co-owner at
|
|
1476
|
+
* creation time).
|
|
1477
|
+
*/
|
|
1478
|
+
createSpace(initialAcl?: ACLEntry[]): Promise<{
|
|
1479
|
+
spaceId: string;
|
|
1480
|
+
}>;
|
|
1481
|
+
/** Write a file manifest into the space. Requires `write` permission. */
|
|
1482
|
+
writeFileManifest(spaceId: string, manifest: FileManifest): Promise<void>;
|
|
1483
|
+
/** Read a file manifest by id. Requires `read` permission. */
|
|
1484
|
+
readFileManifest(spaceId: string, fileId: string): Promise<FileManifest>;
|
|
1485
|
+
/** Delete a file manifest by id. Requires `delete` permission. */
|
|
1486
|
+
deleteFileManifest(spaceId: string, fileId: string): Promise<boolean>;
|
|
1487
|
+
/** List file summaries in the space. Requires `read` permission. */
|
|
1488
|
+
listFiles(spaceId: string): Promise<FileStat[]>;
|
|
1489
|
+
/** Add a participant. Requires `invite` or `admin`. */
|
|
1490
|
+
addParticipant(spaceId: string, participant: Participant): Promise<void>;
|
|
1491
|
+
/** Remove a participant by their public-key identity. Requires `admin`. */
|
|
1492
|
+
removeParticipant(spaceId: string, participantCommitment: string): Promise<boolean>;
|
|
1493
|
+
/** Set / replace an ACL entry. Requires `admin`. */
|
|
1494
|
+
setACLEntry(spaceId: string, entry: ACLEntry): Promise<void>;
|
|
1495
|
+
/** Request a challenge for `spaceId` and generate the matching proof. */
|
|
1496
|
+
private proveFreshChallenge;
|
|
1497
|
+
/**
|
|
1498
|
+
* Hit `/challenge`. The body includes the caller's commitment so the
|
|
1499
|
+
* server can scope the issued nonce to this principal.
|
|
1500
|
+
*/
|
|
1501
|
+
private requestChallenge;
|
|
1502
|
+
private jsonRequest;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
/**
|
|
1506
|
+
* `FileStorage` — top-level entry point for chunked, encrypted, erasure-coded
|
|
1507
|
+
* file storage.
|
|
1508
|
+
*
|
|
1509
|
+
* Pipeline for writing a file:
|
|
1510
|
+
* 1. Split the plaintext into fixed-size chunks (default 4 MiB).
|
|
1511
|
+
* 2. For each chunk:
|
|
1512
|
+
* a. Generate a fresh AES-256-GCM key + 12-byte IV (`ChunkCipher`).
|
|
1513
|
+
* b. Encrypt the chunk → ciphertext (plaintext.length + 16 bytes tag).
|
|
1514
|
+
* c. Reed–Solomon encode the ciphertext into `dataShards + parityShards`
|
|
1515
|
+
* equal-length shards (`ReedSolomonCodec`).
|
|
1516
|
+
* d. PUT every shard to the content-addressed shard store, keyed by
|
|
1517
|
+
* SHA-256 of the shard bytes (`ShardClient`).
|
|
1518
|
+
* 3. Build a `FileManifest` listing every chunk's keys, IVs, sizes, and
|
|
1519
|
+
* the SHA-256 of every shard in order.
|
|
1520
|
+
*
|
|
1521
|
+
* Two ways to use the manifest:
|
|
1522
|
+
* - {@link FileStorage.writeFileToShards} returns it to the caller, who is
|
|
1523
|
+
* then responsible for delivering it to readers (e.g. by E2E-encrypting
|
|
1524
|
+
* it as a chat message). The chat application takes this path.
|
|
1525
|
+
* - {@link FileStorage.writeFile} additionally POSTs the manifest to a
|
|
1526
|
+
* gated multi-user space (`SharedSpaceClient`) so any participant of the
|
|
1527
|
+
* space can read the file by manifest id later.
|
|
1528
|
+
*
|
|
1529
|
+
* Reading is the inverse: {@link FileStorage.readFileFromShards} takes a
|
|
1530
|
+
* manifest and reassembles the plaintext; {@link FileStorage.readFile} first
|
|
1531
|
+
* fetches the manifest from the space.
|
|
1532
|
+
*/
|
|
1533
|
+
|
|
1534
|
+
interface FileStorageOptions {
|
|
1535
|
+
/** Talks to the content-addressed shard store. Always required. */
|
|
1536
|
+
shards: ShardClient;
|
|
1537
|
+
/**
|
|
1538
|
+
* Talks to the gated multi-user space holding manifests + ACLs.
|
|
1539
|
+
* Optional — only required by {@link FileStorage.writeFile} /
|
|
1540
|
+
* {@link FileStorage.readFile} / {@link FileStorage.deleteFile} /
|
|
1541
|
+
* {@link FileStorage.listFiles} / {@link FileStorage.readManifest}.
|
|
1542
|
+
*
|
|
1543
|
+
* Callers that ship the manifest themselves (e.g. via an encrypted chat
|
|
1544
|
+
* message) can omit `space` and use the `*FromShards` / `*ToShards`
|
|
1545
|
+
* methods directly.
|
|
1546
|
+
*/
|
|
1547
|
+
space?: SharedSpaceClient;
|
|
1548
|
+
/**
|
|
1549
|
+
* Plaintext chunk size in bytes. After encryption each chunk gains 16
|
|
1550
|
+
* bytes (GCM tag); after RS encoding the size is rounded up to a multiple
|
|
1551
|
+
* of `dataShards` per chunk. Default 4 MiB — a balance between
|
|
1552
|
+
* round-trip cost and parallelism on the upload path.
|
|
1553
|
+
*/
|
|
1554
|
+
chunkSize?: number;
|
|
1555
|
+
/** RS data shards per chunk (default 4). */
|
|
1556
|
+
dataShards?: number;
|
|
1557
|
+
/** RS parity shards per chunk (default 2). */
|
|
1558
|
+
parityShards?: number;
|
|
1559
|
+
/**
|
|
1560
|
+
* Cap on concurrent shard uploads / downloads. Tune for the network. The
|
|
1561
|
+
* default of 8 saturates most home connections without overwhelming the
|
|
1562
|
+
* server's per-IP rate limiter.
|
|
1563
|
+
*/
|
|
1564
|
+
concurrency?: number;
|
|
1565
|
+
/**
|
|
1566
|
+
* Optional pre-compiled RS WebAssembly module. Defaults to the bundled
|
|
1567
|
+
* loader. Pass this in environments where the bundled loader path isn't
|
|
1568
|
+
* available (vitest, deploy-time-precompiled CF Workers).
|
|
1569
|
+
*/
|
|
1570
|
+
rsWasmModule?: WebAssembly.Module;
|
|
1571
|
+
}
|
|
1572
|
+
/**
|
|
1573
|
+
* Input to {@link FileStorage.writeFile} (which writes the manifest to a
|
|
1574
|
+
* `SharedSpaceClient`). Carries the destination `spaceId`.
|
|
1575
|
+
*/
|
|
1576
|
+
interface WriteFileInput extends WriteToShardsInput {
|
|
1577
|
+
/** Space to write the manifest to — must allow the caller `write`. */
|
|
1578
|
+
spaceId: string;
|
|
1579
|
+
}
|
|
1580
|
+
/**
|
|
1581
|
+
* Input to {@link FileStorage.writeFileToShards} (which doesn't write a
|
|
1582
|
+
* manifest anywhere — that's the caller's job).
|
|
1583
|
+
*/
|
|
1584
|
+
interface WriteToShardsInput {
|
|
1585
|
+
/** Raw data, a Blob, or a File. */
|
|
1586
|
+
data: Uint8Array | Blob | File;
|
|
1587
|
+
/** Application-supplied display metadata. */
|
|
1588
|
+
metadata: {
|
|
1589
|
+
name: string;
|
|
1590
|
+
type: string;
|
|
1591
|
+
path?: string;
|
|
1592
|
+
lastModified?: number;
|
|
1593
|
+
};
|
|
1594
|
+
/**
|
|
1595
|
+
* Optional pre-allocated file id. Defaults to a fresh one. Useful for
|
|
1596
|
+
* resumable uploads: the same id always maps to the same manifest slot.
|
|
1597
|
+
*/
|
|
1598
|
+
fileId?: string;
|
|
1599
|
+
}
|
|
1600
|
+
declare class FileStorage {
|
|
1601
|
+
private readonly space;
|
|
1602
|
+
private readonly shards;
|
|
1603
|
+
private readonly chunkSize;
|
|
1604
|
+
private readonly dataShards;
|
|
1605
|
+
private readonly parityShards;
|
|
1606
|
+
private readonly concurrency;
|
|
1607
|
+
private readonly cipher;
|
|
1608
|
+
private readonly codec;
|
|
1609
|
+
private readonly rsWasmModule?;
|
|
1610
|
+
constructor(opts: FileStorageOptions);
|
|
1611
|
+
/**
|
|
1612
|
+
* Chunk → encrypt → encode → upload-shards. Returns the resulting manifest
|
|
1613
|
+
* along with a `FileStat` summary. The caller is responsible for storing
|
|
1614
|
+
* or transmitting the manifest however it likes.
|
|
1615
|
+
*/
|
|
1616
|
+
writeFileToShards(input: WriteToShardsInput): Promise<{
|
|
1617
|
+
manifest: FileManifest;
|
|
1618
|
+
stat: FileStat;
|
|
1619
|
+
}>;
|
|
1620
|
+
/**
|
|
1621
|
+
* Inverse of {@link writeFileToShards}. Takes a manifest the caller
|
|
1622
|
+
* obtained somehow (chat message, share link, etc.) and reassembles the
|
|
1623
|
+
* plaintext. Throws if too many shards are missing or any chunk's auth
|
|
1624
|
+
* tag fails.
|
|
1625
|
+
*/
|
|
1626
|
+
readFileFromShards(manifest: FileManifest): Promise<{
|
|
1627
|
+
data: Uint8Array;
|
|
1628
|
+
stat: FileStat;
|
|
1629
|
+
}>;
|
|
1630
|
+
/** As {@link writeFileToShards}, plus POSTs the manifest to `input.spaceId`. */
|
|
1631
|
+
writeFile(input: WriteFileInput): Promise<FileStat>;
|
|
1632
|
+
/** As {@link readFileFromShards}, but fetches the manifest from a space first. */
|
|
1633
|
+
readFile(spaceId: string, fileId: string): Promise<{
|
|
1634
|
+
data: Uint8Array;
|
|
1635
|
+
stat: FileStat;
|
|
1636
|
+
}>;
|
|
1637
|
+
/**
|
|
1638
|
+
* Delete the manifest from a space. **Does not GC the shards** — those
|
|
1639
|
+
* are content-addressed and may be referenced by other manifests.
|
|
1640
|
+
* Garbage collection is a separate server-side concern (Phase D / D-ish).
|
|
1641
|
+
*/
|
|
1642
|
+
deleteFile(spaceId: string, fileId: string): Promise<boolean>;
|
|
1643
|
+
/** Convenience pass-through. */
|
|
1644
|
+
listFiles(spaceId: string): Promise<FileStat[]>;
|
|
1645
|
+
/** Read a manifest verbatim — useful for "share file" / export flows. */
|
|
1646
|
+
readManifest(spaceId: string, fileId: string): Promise<FileManifest>;
|
|
1647
|
+
private requireSpace;
|
|
1648
|
+
/** Encrypt + encode + upload one chunk; return its manifest entry. */
|
|
1649
|
+
private writeChunk;
|
|
1650
|
+
/** Inverse of `writeChunk`. */
|
|
1651
|
+
private readChunk;
|
|
1652
|
+
/** Run `tasks` with at most `this.concurrency` in flight. */
|
|
1653
|
+
private runWithConcurrency;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
/**
|
|
1657
|
+
* Reed–Solomon erasure-coding codec.
|
|
1658
|
+
*
|
|
1659
|
+
* Wraps the WASM-backed `encode` / `reconstruct` primitives in `wasm/rs.ts`
|
|
1660
|
+
* with shard-padding bookkeeping so callers can hand it any-length input and
|
|
1661
|
+
* get back uniform-size shards.
|
|
1662
|
+
*
|
|
1663
|
+
* Usage:
|
|
1664
|
+
* const codec = new ReedSolomonCodec();
|
|
1665
|
+
* await codec.ready();
|
|
1666
|
+
* const { shards, shardSize, originalSize } = codec.encode(plaintext, 4, 2);
|
|
1667
|
+
* // 6 shards (4 data + 2 parity), each `shardSize` bytes
|
|
1668
|
+
* // …ship them, lose up to 2, then …
|
|
1669
|
+
* const recovered = codec.decode(shards, 2, deadIndexes, originalSize);
|
|
1670
|
+
*
|
|
1671
|
+
* Why the explicit `originalSize`: RS pads input to make every data shard the
|
|
1672
|
+
* same length. The decoder must trim that padding back off after recovery.
|
|
1673
|
+
* The caller is responsible for stashing `originalSize` somewhere durable
|
|
1674
|
+
* (the file manifest, in this codebase).
|
|
1675
|
+
*/
|
|
1676
|
+
interface EncodeResult {
|
|
1677
|
+
/** All `dataShards + parityShards` shards. All have length `shardSize`. */
|
|
1678
|
+
shards: Uint8Array[];
|
|
1679
|
+
/** Length of every shard (data and parity). */
|
|
1680
|
+
shardSize: number;
|
|
1681
|
+
/** Length of the original input before padding. Needed to trim on decode. */
|
|
1682
|
+
originalSize: number;
|
|
1683
|
+
/** How many of the leading shards are data (the rest are parity). */
|
|
1684
|
+
dataShards: number;
|
|
1685
|
+
/** How many parity shards trail the data shards. */
|
|
1686
|
+
parityShards: number;
|
|
1687
|
+
}
|
|
1688
|
+
declare class ReedSolomonCodec {
|
|
1689
|
+
/**
|
|
1690
|
+
* Resolves once the WASM module is loaded; safe to call repeatedly.
|
|
1691
|
+
*
|
|
1692
|
+
* Pass `wasmModule` to use a pre-compiled module (e.g. in tests where the
|
|
1693
|
+
* rollup-inlined loader path isn't available, or in CF Workers where you
|
|
1694
|
+
* want to load the module at deploy time). Default path uses the bundled
|
|
1695
|
+
* base64-inlined wasm — works in Node, browsers, and Workers.
|
|
1696
|
+
*/
|
|
1697
|
+
ready(wasmModule?: WebAssembly.Module): Promise<void>;
|
|
1698
|
+
/**
|
|
1699
|
+
* Split `data` into `dataShards` equal-size chunks (zero-padded to fit),
|
|
1700
|
+
* compute `parityShards` parity shards, and return all of them.
|
|
1701
|
+
*/
|
|
1702
|
+
encode(data: Uint8Array, dataShards: number, parityShards: number): EncodeResult;
|
|
1703
|
+
/**
|
|
1704
|
+
* Reconstruct the original `data` from `shards`. Up to `parityShards`
|
|
1705
|
+
* shards may be missing; for each missing shard, pass an empty
|
|
1706
|
+
* `Uint8Array` placeholder at its position and list the position in
|
|
1707
|
+
* `deadShardIndexes`.
|
|
1708
|
+
*
|
|
1709
|
+
* `originalSize` is required so the trailing zero-padding can be trimmed.
|
|
1710
|
+
*/
|
|
1711
|
+
decode(shards: Uint8Array[], parityShards: number, deadShardIndexes: number[], originalSize: number): Uint8Array;
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
/**
|
|
1715
|
+
* Universal wasm-bindgen glue for `wasm-reed-solomon-erasure` (v0.2.2 — Rust
|
|
1716
|
+
* Reed–Solomon erasure-coding library compiled with `wasm-pack --target nodejs`).
|
|
1717
|
+
*
|
|
1718
|
+
* The upstream npm package ships `wasm_reed_solomon_erasure.js` which uses
|
|
1719
|
+
* `require('fs').readFileSync()` to load the WASM file at module load time —
|
|
1720
|
+
* that hard-codes Node and breaks browsers / CF Workers. This file is a port
|
|
1721
|
+
* of that glue with the Node-only pieces removed:
|
|
1722
|
+
*
|
|
1723
|
+
* - `TextDecoder` is taken from the global (available in Node 12+, every
|
|
1724
|
+
* modern browser, and Workers — no `require('util')`).
|
|
1725
|
+
* - The WASM module is loaded via the rollup-inlined `.wasm` import, the
|
|
1726
|
+
* same `targetEnv: 'auto-inline'` path the bn128 verifier uses. Works in
|
|
1727
|
+
* Node, browser, and Workers without runtime fs access.
|
|
1728
|
+
*
|
|
1729
|
+
* Only the two public functions consumers actually need (`encode` and
|
|
1730
|
+
* `reconstruct`) are exported. The rest is the wbindgen heap-table dance.
|
|
1731
|
+
*
|
|
1732
|
+
* Encoding:
|
|
1733
|
+
* `encode(dataShards: Uint8Array[], parityShard: number)` → array of
|
|
1734
|
+
* `dataShards.length + parityShard` Uint8Arrays. The first N are the input
|
|
1735
|
+
* shards unchanged; the trailing M are the freshly computed parity shards.
|
|
1736
|
+
* All shards are the same length — caller is responsible for padding the
|
|
1737
|
+
* data into equal-size data shards.
|
|
1738
|
+
*
|
|
1739
|
+
* Reconstruction:
|
|
1740
|
+
* `reconstruct(corruptedShards, parityShard, deadIndexes)` → fully recovered
|
|
1741
|
+
* array. Pass placeholders (empty Uint8Arrays) at the dead indexes; the
|
|
1742
|
+
* reconstructor fills them in. Up to `parityShard` shards may be missing.
|
|
1743
|
+
*/
|
|
1744
|
+
/**
|
|
1745
|
+
* Load and instantiate the WASM module. Idempotent — repeated calls await the
|
|
1746
|
+
* same one-shot promise.
|
|
1747
|
+
*
|
|
1748
|
+
* Two ways to initialize, matching the pattern used by `workers/groth16-verifier`:
|
|
1749
|
+
* 1. `await initRsWasm()` — uses the bundled .wasm (base64-inlined at build
|
|
1750
|
+
* time by `@rollup/plugin-wasm`). Works in any runtime that allows
|
|
1751
|
+
* runtime `WebAssembly.compile()`.
|
|
1752
|
+
* 2. `await initRsWasm(myWasmModule)` — accepts a pre-compiled
|
|
1753
|
+
* `WebAssembly.Module`. Useful when the bundled loader path isn't
|
|
1754
|
+
* available (vitest, CF Workers wanting deploy-time precompile, etc.).
|
|
1755
|
+
*/
|
|
1756
|
+
declare function initRsWasm(wasmModule?: WebAssembly.Module): Promise<void>;
|
|
1757
|
+
|
|
1758
|
+
/**
|
|
1759
|
+
* HTTP client for the accelerator's `/api/auth/*` endpoints.
|
|
1760
|
+
*
|
|
1761
|
+
* Owns the wire shapes for register / challenge / authenticate / verify so
|
|
1762
|
+
* the React SPA (and any future consumers) don't reimplement them. The ZK
|
|
1763
|
+
* identity derivation + Groth16 proof generation stay on the application
|
|
1764
|
+
* side — this client handles only the round-trips.
|
|
1765
|
+
*
|
|
1766
|
+
* Mirrors `PersonalSpaceClient` / `ShardClient` / `SharedSpaceClient`: takes
|
|
1767
|
+
* a `baseUrl`, optional custom `fetch`, throws typed `Error`s on failure
|
|
1768
|
+
* (with the worker's message inlined where available).
|
|
1769
|
+
*/
|
|
1770
|
+
|
|
1771
|
+
interface ZkRegisterRequest {
|
|
1772
|
+
username: string;
|
|
1773
|
+
/** Decimal-string Poseidon commitment. */
|
|
1774
|
+
commitment: string;
|
|
1775
|
+
/** Base64-encoded raw uncompressed ECDH public key (SEC1 0x04‖X‖Y). */
|
|
1776
|
+
ecdhPublicKey: string;
|
|
1777
|
+
/** Base64-encoded raw uncompressed ECDSA public key. */
|
|
1778
|
+
ecdsaPublicKey: string;
|
|
1779
|
+
email?: string | null;
|
|
1780
|
+
}
|
|
1781
|
+
interface ZkRegisterResponse {
|
|
1782
|
+
success: boolean;
|
|
1783
|
+
}
|
|
1784
|
+
interface ZkChallengeResponse {
|
|
1785
|
+
challengeId: string;
|
|
1786
|
+
/** Hex-encoded random nonce — caller reduces into a BN254 field element for the proof. */
|
|
1787
|
+
nonce: string;
|
|
1788
|
+
/** Decimal-string Poseidon commitment the server has on file for this user. */
|
|
1789
|
+
commitment: string;
|
|
1790
|
+
}
|
|
1791
|
+
interface ZkAuthenticateRequest {
|
|
1792
|
+
challengeId: string;
|
|
1793
|
+
proof: {
|
|
1794
|
+
commitment: string;
|
|
1795
|
+
/** Original hex nonce from the challenge (NOT the field-reduced version). */
|
|
1796
|
+
nonce: string;
|
|
1797
|
+
response: {
|
|
1798
|
+
proof: Groth16Proof;
|
|
1799
|
+
publicSignals: string[];
|
|
1800
|
+
};
|
|
1801
|
+
/** Base64 ECDSA-SHA256 signature over `JSON.stringify(response.proof)`. */
|
|
1802
|
+
signature: string;
|
|
1803
|
+
};
|
|
1804
|
+
rememberMe?: boolean;
|
|
1805
|
+
}
|
|
1806
|
+
interface AuthSuccess {
|
|
1807
|
+
token: string;
|
|
1808
|
+
username: string;
|
|
1809
|
+
}
|
|
1810
|
+
interface AuthClientOptions {
|
|
1811
|
+
/** Absolute URL of the accelerator worker (no trailing slash required). */
|
|
1812
|
+
baseUrl: string;
|
|
1813
|
+
/** Optional custom fetch — defaults to `globalThis.fetch`. */
|
|
1814
|
+
fetch?: typeof fetch;
|
|
1815
|
+
}
|
|
1816
|
+
declare class AuthClient {
|
|
1817
|
+
private readonly baseUrl;
|
|
1818
|
+
private readonly fetchFn;
|
|
1819
|
+
constructor(opts: AuthClientOptions);
|
|
1820
|
+
/** Submit ZK registration. Caller has already derived the identity + commitment. */
|
|
1821
|
+
register(req: ZkRegisterRequest): Promise<ZkRegisterResponse>;
|
|
1822
|
+
/** Request an auth challenge for `username` (server issues a fresh nonce). */
|
|
1823
|
+
getChallenge(username: string): Promise<ZkChallengeResponse>;
|
|
1824
|
+
/** Trade a challenge + proof for a session token. */
|
|
1825
|
+
authenticate(req: ZkAuthenticateRequest): Promise<AuthSuccess>;
|
|
1826
|
+
/**
|
|
1827
|
+
* Validate a previously stored session token. Returns `{ username }` on
|
|
1828
|
+
* success; throws on 401/404 — caller uses that to drop a stale token
|
|
1829
|
+
* when restoring a session after a reload.
|
|
1830
|
+
*/
|
|
1831
|
+
verify(token: string): Promise<{
|
|
1832
|
+
username: string;
|
|
1833
|
+
}>;
|
|
1834
|
+
private json;
|
|
1835
|
+
private parse;
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
/**
|
|
1839
|
+
* Deterministic ZK identity derivation from `(username, password)`.
|
|
1840
|
+
*
|
|
1841
|
+
* Same inputs on ANY device produce the same secret, salt, and P-256
|
|
1842
|
+
* ECDSA + ECDH keypairs — which is what makes federated login from a
|
|
1843
|
+
* fresh browser possible. There is no localStorage involvement: every
|
|
1844
|
+
* register and login call re-derives the identity on demand.
|
|
1845
|
+
*
|
|
1846
|
+
* Pipeline:
|
|
1847
|
+
* master_seed = PBKDF2-SHA256(password, "muhkoo-zk-v1:" || username, 200k iters)
|
|
1848
|
+
* secret = HKDF-Expand-SHA256(master_seed, "zk-secret", 32 bytes) → hex
|
|
1849
|
+
* salt = HKDF-Expand-SHA256(master_seed, "zk-salt", 32 bytes) → hex
|
|
1850
|
+
* ecdsa_priv = HKDF-Expand-SHA256(master_seed, "ecdsa-priv", 32 bytes)
|
|
1851
|
+
* ecdh_priv = HKDF-Expand-SHA256(master_seed, "ecdh-priv", 32 bytes)
|
|
1852
|
+
*
|
|
1853
|
+
* P-256 public points are computed via `@noble/curves` (WebCrypto has no
|
|
1854
|
+
* derive-public-from-private-scalar API), then both keypairs are imported
|
|
1855
|
+
* into SubtleCrypto in JWK form so the rest of the crypto stack can keep
|
|
1856
|
+
* using `crypto.subtle.sign` / `crypto.subtle.deriveBits`.
|
|
1857
|
+
*
|
|
1858
|
+
* Originally lived in `accelerator/public/js/zk-identity.js` and was ported
|
|
1859
|
+
* to `muhkoo/web/src/zk/identity.ts`; consolidated here so the SDK owns
|
|
1860
|
+
* identity derivation end-to-end.
|
|
1861
|
+
*/
|
|
1862
|
+
interface ZkIdentity {
|
|
1863
|
+
/** 32-byte secret, hex-encoded. Private witness for the ZK circuit. */
|
|
1864
|
+
secretHex: string;
|
|
1865
|
+
/** 32-byte salt, hex-encoded. Private witness for the ZK circuit. */
|
|
1866
|
+
saltHex: string;
|
|
1867
|
+
/** P-256 ECDSA keypair, used to sign auth proofs. */
|
|
1868
|
+
ecdsaKeyPair: CryptoKeyPair;
|
|
1869
|
+
/** P-256 ECDH keypair, used by the chat ratchet. */
|
|
1870
|
+
ecdhKeyPair: CryptoKeyPair;
|
|
1871
|
+
}
|
|
1872
|
+
/**
|
|
1873
|
+
* Derive a deterministic ZK identity from `(username, password)`.
|
|
1874
|
+
*
|
|
1875
|
+
* Same inputs always produce the same identity, on any device — that's
|
|
1876
|
+
* what makes federated login work without dragging encrypted state across
|
|
1877
|
+
* devices.
|
|
1878
|
+
*/
|
|
1879
|
+
declare function deriveIdentity(username: string, password: string): Promise<ZkIdentity>;
|
|
1880
|
+
|
|
1881
|
+
/**
|
|
1882
|
+
* Poseidon hash wrapper used by the ZK auth flow.
|
|
1883
|
+
*
|
|
1884
|
+
* Backed by `circomlibjs`'s `buildPoseidon`. Builder is lazy and cached
|
|
1885
|
+
* because the JS-side Babyjubjub field reps take ~100ms to construct, and
|
|
1886
|
+
* we'd rather not pay that on every page load.
|
|
1887
|
+
*
|
|
1888
|
+
* Returns hashes as decimal-string BigInts — same format snarkjs and the
|
|
1889
|
+
* `preimagePoK` circuit expect for inputs.
|
|
1890
|
+
*
|
|
1891
|
+
* Note: circomlibjs is `optionalDependencies` in connect's package.json.
|
|
1892
|
+
* Consumers that use the ZK auth flow (browser/server, not workers) need
|
|
1893
|
+
* it installed. The workers build does not import this module.
|
|
1894
|
+
*/
|
|
1895
|
+
/**
|
|
1896
|
+
* Poseidon-hash `inputs` and return the result as a decimal-string BigInt.
|
|
1897
|
+
* Inputs are converted to BigInt — pass either bigints, numbers, or numeric
|
|
1898
|
+
* strings.
|
|
1899
|
+
*/
|
|
1900
|
+
declare function poseidonHash(inputs: Array<bigint | string | number>): Promise<string>;
|
|
1901
|
+
|
|
1902
|
+
/**
|
|
1903
|
+
* Groth16 proof generation for the `preimagePoK` circuit.
|
|
1904
|
+
*
|
|
1905
|
+
* The circuit proves knowledge of `(secret, salt, ecdsaPub)` such that
|
|
1906
|
+
* commitment = Poseidon(secret, salt, Poseidon(ecdsaPub))
|
|
1907
|
+
* while only revealing `(commitment, nonce, ecdsaPubHash)` as public signals.
|
|
1908
|
+
*
|
|
1909
|
+
* Circuit assets (wasm + zkey) are served by the accelerator worker. Their
|
|
1910
|
+
* URLs are a required argument of {@link generateAuthProof} — there is no
|
|
1911
|
+
* default. The web app constructs them relative to its configured worker
|
|
1912
|
+
* `baseUrl` so the SPA can target either the local wrangler dev or a deployed
|
|
1913
|
+
* worker depending on `VITE_WORKER_URL`.
|
|
1914
|
+
*/
|
|
1915
|
+
|
|
1916
|
+
interface CircuitUrls {
|
|
1917
|
+
/** preimagePoK.wasm — the circuit's witness generator. */
|
|
1918
|
+
wasmUrl: string;
|
|
1919
|
+
/** preimagePoK_0001.zkey — the proving key. */
|
|
1920
|
+
zkeyUrl: string;
|
|
1921
|
+
}
|
|
1922
|
+
/**
|
|
1923
|
+
* Canonical {@link CircuitUrls} for the `preimagePoK` login circuit, anchored
|
|
1924
|
+
* at a base URL where the accelerator serves the circuit assets out of
|
|
1925
|
+
* `circuits/build/`. The filenames here are fixed by the circom build output
|
|
1926
|
+
* (the witness generator lands inside a `preimagePoK_js/` directory); callers
|
|
1927
|
+
* shouldn't hard-code them. `Client` uses this to default its `circuits`
|
|
1928
|
+
* option, and `PersonalSpaceClient` consumers can pass `defaultCircuitUrls(baseUrl)`.
|
|
1929
|
+
*/
|
|
1930
|
+
declare function defaultCircuitUrls(baseUrl: string): CircuitUrls;
|
|
1931
|
+
/**
|
|
1932
|
+
* Reduce any hex/decimal/byte input to a decimal-string field element in
|
|
1933
|
+
* `[0, FIELD_SIZE)`. snarkjs and the `preimagePoK` circuit both want decimal
|
|
1934
|
+
* strings; everything else gets normalized here.
|
|
1935
|
+
*
|
|
1936
|
+
* **Behavior is intentionally byte-for-byte compatible with the legacy
|
|
1937
|
+
* `accelerator/public/js/zk-snarkjs-client.js#toField`.** Diverging produces
|
|
1938
|
+
* different commitments than accounts registered via the legacy flow,
|
|
1939
|
+
* which locks those users out forever — the worker stores whatever
|
|
1940
|
+
* commitment the client sent at register time and compares it as a string
|
|
1941
|
+
* at login time.
|
|
1942
|
+
*
|
|
1943
|
+
* In particular: a 32+ char string of hex chars is always treated as hex,
|
|
1944
|
+
* even if every character happens to be 0-9 with no a-f letters. An
|
|
1945
|
+
* earlier version of this function gated the hex branch on "has at least
|
|
1946
|
+
* one letter" — that broke the digit-only-hex edge case (rare in practice,
|
|
1947
|
+
* but real). DO NOT add that letter check back.
|
|
1948
|
+
*/
|
|
1949
|
+
declare function toField(value: string | bigint | number): string;
|
|
1950
|
+
/**
|
|
1951
|
+
* Build the user's Poseidon commitment binding `(secret, salt, ecdsaPubHash)`.
|
|
1952
|
+
* Same formula the worker verifies against on register and authenticate.
|
|
1953
|
+
*/
|
|
1954
|
+
declare function buildCommitment(secretHex: string, saltHex: string, ecdsaPubHex: string): Promise<string>;
|
|
1955
|
+
/**
|
|
1956
|
+
* Generate a Groth16 proof for the `preimagePoK` circuit. Public signals end
|
|
1957
|
+
* up `[commitment, nonce, ecdsaPubHash]` — the worker checks the commitment
|
|
1958
|
+
* matches the stored one and the nonce matches the issued challenge nonce
|
|
1959
|
+
* reduced into the field.
|
|
1960
|
+
*
|
|
1961
|
+
* Throws if circuit URLs are unreachable or snarkjs decides the inputs are
|
|
1962
|
+
* malformed.
|
|
1963
|
+
*/
|
|
1964
|
+
declare function generateAuthProof(args: {
|
|
1965
|
+
secretHex: string;
|
|
1966
|
+
saltHex: string;
|
|
1967
|
+
ecdsaPubHex: string;
|
|
1968
|
+
nonceHex: string;
|
|
1969
|
+
/** Required — caller supplies URLs scoped to the worker they want to talk to. */
|
|
1970
|
+
circuits: CircuitUrls;
|
|
1971
|
+
}): Promise<{
|
|
1972
|
+
proof: Groth16Proof;
|
|
1973
|
+
publicSignals: string[];
|
|
1974
|
+
commitment: string;
|
|
1975
|
+
nonceField: string;
|
|
1976
|
+
ecdsaPubHash: string;
|
|
1977
|
+
}>;
|
|
1978
|
+
|
|
1979
|
+
/**
|
|
1980
|
+
* Conversions between SubtleCrypto `CryptoKey`s and the wire formats the
|
|
1981
|
+
* accelerator's auth endpoints + `preimagePoK` circuit expect.
|
|
1982
|
+
*
|
|
1983
|
+
* Two outputs we need from an ECDSA `CryptoKey`:
|
|
1984
|
+
* - **base64 raw** — passed to `/api/auth/zk-register` as
|
|
1985
|
+
* `ecdsaPublicKey` / `ecdhPublicKey`. (Name is historical;
|
|
1986
|
+
* the legacy client called it `exportPublicKeyToBase58`
|
|
1987
|
+
* but actually used base64 — kept as base64.)
|
|
1988
|
+
* - **hex raw** — fed to the proof as `ecdsaPub` input; the circuit
|
|
1989
|
+
* reduces it into a field element.
|
|
1990
|
+
*
|
|
1991
|
+
* Both go through `crypto.subtle.exportKey("raw", ...)` which yields the
|
|
1992
|
+
* uncompressed SEC1 form (`0x04 || X || Y`).
|
|
1993
|
+
*/
|
|
1994
|
+
/** Raw uncompressed (`0x04 || X || Y`) public key bytes, as lowercase hex. */
|
|
1995
|
+
declare function exportPublicKeyHex(publicKey: CryptoKey): Promise<string>;
|
|
1996
|
+
/** Raw uncompressed public key as base64 — wire format for the auth endpoints. */
|
|
1997
|
+
declare function exportPublicKeyBase64(publicKey: CryptoKey): Promise<string>;
|
|
1998
|
+
/** Sign a UTF-8 `message` with an ECDSA private key. Returns base64. */
|
|
1999
|
+
declare function signMessage(message: string, privateKey: CryptoKey): Promise<string>;
|
|
2000
|
+
|
|
2001
|
+
/**
|
|
2002
|
+
* `SessionState` — the in-memory source of truth for "who is signed in" on a
|
|
2003
|
+
* {@link Client}.
|
|
2004
|
+
*
|
|
2005
|
+
* It separates two concerns that have different lifetimes and security
|
|
2006
|
+
* properties:
|
|
2007
|
+
*
|
|
2008
|
+
* - **session** (`{ token, username, commitment }`) — proves the user is
|
|
2009
|
+
* authenticated to the accelerator. Cheap to persist (the token is an
|
|
2010
|
+
* opaque server-side handle) and enough to authorize token-gated REST
|
|
2011
|
+
* calls. Restored from a {@link SessionStore} across reloads.
|
|
2012
|
+
*
|
|
2013
|
+
* - **identity** ({@link ZkIdentity}) — the derived secret + keypairs. Lets
|
|
2014
|
+
* the client *prove* ownership (ZK), derive at-rest encryption keys, and
|
|
2015
|
+
* run the messaging ratchet. It is derived from `(username, password)` and
|
|
2016
|
+
* is deliberately **never** persisted by default — a restored session has
|
|
2017
|
+
* a valid token but no identity until the user re-derives it
|
|
2018
|
+
* (`client.auth.zk.unlock(password)`).
|
|
2019
|
+
*
|
|
2020
|
+
* This split is what makes `restore()` (authenticated, can't decrypt) and
|
|
2021
|
+
* `unlock()` (now can decrypt) two distinct, intentional states.
|
|
2022
|
+
*/
|
|
2023
|
+
|
|
2024
|
+
/** The persistable part of a session — safe-ish to keep across reloads. */
|
|
2025
|
+
interface StoredSession {
|
|
2026
|
+
token: string;
|
|
2027
|
+
username: string;
|
|
2028
|
+
/** Decimal-string Poseidon commitment — the user's stable id. */
|
|
2029
|
+
commitment: string;
|
|
2030
|
+
}
|
|
2031
|
+
/**
|
|
2032
|
+
* Pluggable persistence for the session token. Browser apps typically back
|
|
2033
|
+
* this with `localStorage`; the default is in-memory (lost on reload).
|
|
2034
|
+
*
|
|
2035
|
+
* Identity material is intentionally not part of this interface — see the
|
|
2036
|
+
* class docs. Apps that want federated session restore without re-prompting
|
|
2037
|
+
* for a password can persist it themselves and call `unlock()` on boot.
|
|
2038
|
+
*/
|
|
2039
|
+
interface SessionStore {
|
|
2040
|
+
load(): StoredSession | null | Promise<StoredSession | null>;
|
|
2041
|
+
save(session: StoredSession): void | Promise<void>;
|
|
2042
|
+
clear(): void | Promise<void>;
|
|
2043
|
+
}
|
|
2044
|
+
/** Default in-memory store — holds the session for the process lifetime only. */
|
|
2045
|
+
declare class MemorySessionStore implements SessionStore {
|
|
2046
|
+
private current;
|
|
2047
|
+
load(): StoredSession | null;
|
|
2048
|
+
save(session: StoredSession): void;
|
|
2049
|
+
clear(): void;
|
|
2050
|
+
}
|
|
2051
|
+
/**
|
|
2052
|
+
* A {@link SessionStore} backed by the browser's `localStorage`, so a session
|
|
2053
|
+
* survives page reloads. This is the store most browser apps want — pass it to
|
|
2054
|
+
* `new Client({ sessionStore: new LocalStorageSessionStore() })`.
|
|
2055
|
+
*
|
|
2056
|
+
* Browser-only: construction throws if `localStorage` is unavailable (Node,
|
|
2057
|
+
* Workers, or SSR before hydration) — use {@link MemorySessionStore} or a
|
|
2058
|
+
* custom store there. Keys are namespaced under `keyPrefix` so multiple apps
|
|
2059
|
+
* on the same origin don't collide.
|
|
2060
|
+
*/
|
|
2061
|
+
declare class LocalStorageSessionStore implements SessionStore {
|
|
2062
|
+
private readonly tokenKey;
|
|
2063
|
+
private readonly usernameKey;
|
|
2064
|
+
private readonly commitmentKey;
|
|
2065
|
+
constructor(keyPrefix?: string);
|
|
2066
|
+
load(): StoredSession | null;
|
|
2067
|
+
save(session: StoredSession): void;
|
|
2068
|
+
clear(): void;
|
|
2069
|
+
}
|
|
2070
|
+
/**
|
|
2071
|
+
* The {@link SessionStore} a {@link Client} uses when none is supplied: a
|
|
2072
|
+
* {@link LocalStorageSessionStore} in the browser (so sessions survive
|
|
2073
|
+
* reloads), falling back to {@link MemorySessionStore} anywhere `localStorage`
|
|
2074
|
+
* is unavailable or unusable (Node, Workers, SSR, or private-mode browsers
|
|
2075
|
+
* that throw on access). Pass an explicit store to override.
|
|
2076
|
+
*/
|
|
2077
|
+
declare function defaultSessionStore(keyPrefix?: string): SessionStore;
|
|
2078
|
+
declare class SessionState {
|
|
2079
|
+
private session;
|
|
2080
|
+
private _identity;
|
|
2081
|
+
private readonly store;
|
|
2082
|
+
constructor(store?: SessionStore);
|
|
2083
|
+
/** True once a token-bearing session exists (regardless of identity). */
|
|
2084
|
+
get isAuthenticated(): boolean;
|
|
2085
|
+
/** True once identity material is available (login or unlock happened). */
|
|
2086
|
+
get isUnlocked(): boolean;
|
|
2087
|
+
get token(): string | null;
|
|
2088
|
+
get username(): string | null;
|
|
2089
|
+
get commitment(): string | null;
|
|
2090
|
+
get identity(): ZkIdentity | null;
|
|
2091
|
+
/** Identity material, throwing a clear error when the client is locked. */
|
|
2092
|
+
requireIdentity(): ZkIdentity;
|
|
2093
|
+
/** Persist + hold a freshly authenticated session. */
|
|
2094
|
+
setSession(session: StoredSession): Promise<void>;
|
|
2095
|
+
/** Attach derived identity material to the current session. */
|
|
2096
|
+
setIdentity(identity: ZkIdentity): void;
|
|
2097
|
+
/** Load a persisted session (used by `restore()`); identity stays null. */
|
|
2098
|
+
loadPersisted(): Promise<StoredSession | null>;
|
|
2099
|
+
/** Drop all session + identity state and clear persistence. */
|
|
2100
|
+
clear(): Promise<void>;
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
/**
|
|
2104
|
+
* `client.auth` — user authentication, exposed today through the `zk`
|
|
2105
|
+
* sub-namespace (`client.auth.zk.login(...)`). Leaving room under `auth` for
|
|
2106
|
+
* other strategies later (e.g. `auth.oauth`) without reshaping the surface.
|
|
2107
|
+
*
|
|
2108
|
+
* `ZkAuth` absorbs the full ZK register/login dance that used to live in the
|
|
2109
|
+
* web app's `AuthContext`:
|
|
2110
|
+
*
|
|
2111
|
+
* register: derive identity → commitment → POST /api/auth/zk-register
|
|
2112
|
+
* login: derive identity → challenge → Groth16 proof → sign →
|
|
2113
|
+
* POST /api/auth/zk-authenticate → session token
|
|
2114
|
+
*
|
|
2115
|
+
* The derived {@link ZkIdentity} (secret + ECDSA/ECDH keypairs) is held in
|
|
2116
|
+
* {@link SessionState} so the storage + message namespaces can reuse it for
|
|
2117
|
+
* at-rest encryption and the messaging ratchet without re-deriving.
|
|
2118
|
+
*/
|
|
2119
|
+
|
|
2120
|
+
/** What the auth methods resolve to — the stable, non-secret user facts. */
|
|
2121
|
+
interface AuthUser {
|
|
2122
|
+
username: string;
|
|
2123
|
+
/** Decimal-string Poseidon commitment — the user's stable id. */
|
|
2124
|
+
commitment: string;
|
|
2125
|
+
}
|
|
2126
|
+
interface RegisterParams {
|
|
2127
|
+
username: string;
|
|
2128
|
+
password: string;
|
|
2129
|
+
email?: string | null;
|
|
2130
|
+
/** Sign in immediately after registering (default: true). */
|
|
2131
|
+
login?: boolean;
|
|
2132
|
+
}
|
|
2133
|
+
interface LoginOptions {
|
|
2134
|
+
/** Ask the accelerator for a long-lived (30d) session (default: false). */
|
|
2135
|
+
rememberMe?: boolean;
|
|
2136
|
+
}
|
|
2137
|
+
interface ZkAuthDeps {
|
|
2138
|
+
auth: AuthClient;
|
|
2139
|
+
circuits: CircuitUrls;
|
|
2140
|
+
session: SessionState;
|
|
2141
|
+
}
|
|
2142
|
+
/**
|
|
2143
|
+
* ZK identity auth. One instance per {@link Client}; reads/writes the shared
|
|
2144
|
+
* {@link SessionState}.
|
|
2145
|
+
*/
|
|
2146
|
+
declare class ZkAuth {
|
|
2147
|
+
private readonly deps;
|
|
2148
|
+
constructor(deps: ZkAuthDeps);
|
|
2149
|
+
/**
|
|
2150
|
+
* Register a new user, then (by default) sign them in. The identity is
|
|
2151
|
+
* derived deterministically from `(username, password)` — the same inputs
|
|
2152
|
+
* reproduce it on any device, so there's no key material to ship around.
|
|
2153
|
+
*/
|
|
2154
|
+
register(params: RegisterParams): Promise<AuthUser>;
|
|
2155
|
+
/**
|
|
2156
|
+
* Sign in: re-derive the identity, prove knowledge of it against a fresh
|
|
2157
|
+
* server challenge, and trade the proof for a session token. On success
|
|
2158
|
+
* both the session and the identity are stored on the client.
|
|
2159
|
+
*/
|
|
2160
|
+
login(username: string, password: string, opts?: LoginOptions): Promise<AuthUser>;
|
|
2161
|
+
/**
|
|
2162
|
+
* Restore a persisted session on boot. Validates the stored token with the
|
|
2163
|
+
* accelerator; returns the user when valid, or `null` (and clears the
|
|
2164
|
+
* stale session) otherwise. Identity stays locked — call {@link unlock}
|
|
2165
|
+
* to re-enable encryption/messaging without a full re-login.
|
|
2166
|
+
*/
|
|
2167
|
+
restore(): Promise<AuthUser | null>;
|
|
2168
|
+
/**
|
|
2169
|
+
* Re-derive identity material for an already-authenticated (restored)
|
|
2170
|
+
* session, so the client can decrypt storage and run the ratchet. Verifies
|
|
2171
|
+
* the password by checking the derived commitment matches the session's.
|
|
2172
|
+
*/
|
|
2173
|
+
unlock(password: string): Promise<void>;
|
|
2174
|
+
/** Sign out: clears the session, identity, and persisted token. */
|
|
2175
|
+
logout(): Promise<void>;
|
|
2176
|
+
/** The signed-in user, or `null`. Synchronous read of current state. */
|
|
2177
|
+
get user(): AuthUser | null;
|
|
2178
|
+
/**
|
|
2179
|
+
* The derived identity material for the current session, or `null` when
|
|
2180
|
+
* locked. Exposed so apps that still drive lower-level primitives (e.g. a
|
|
2181
|
+
* chat ratchet keyed off the same identity) can reuse it without
|
|
2182
|
+
* re-deriving from the password.
|
|
2183
|
+
*/
|
|
2184
|
+
get identity(): ZkIdentity | null;
|
|
2185
|
+
/** The current session token, or `null` when signed out. */
|
|
2186
|
+
get token(): string | null;
|
|
2187
|
+
/** Poseidon commitment for an identity — the value the server stores. */
|
|
2188
|
+
private commitmentFor;
|
|
2189
|
+
}
|
|
2190
|
+
/** `client.auth` — the auth namespace. Strategies hang off here. */
|
|
2191
|
+
declare class AuthNamespace {
|
|
2192
|
+
readonly zk: ZkAuth;
|
|
2193
|
+
constructor(deps: ZkAuthDeps);
|
|
2194
|
+
/** Convenience pass-through: the currently signed-in user, or `null`. */
|
|
2195
|
+
get user(): AuthUser | null;
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
/**
|
|
2199
|
+
* `HttpClient` — the one place the unified {@link Client} talks to the
|
|
2200
|
+
* accelerator over HTTP.
|
|
2201
|
+
*
|
|
2202
|
+
* Every request carries two independent credentials:
|
|
2203
|
+
* - the **app key** (`mk_…`) on `X-Muhkoo-Key` — resolves the app /
|
|
2204
|
+
* developer / env context the accelerator uses for routing + billing;
|
|
2205
|
+
* - the **user session token** on `X-Muhkoo-Session` (when a user is
|
|
2206
|
+
* logged in) — the accelerator validates it and injects a trusted
|
|
2207
|
+
* `X-Muhkoo-User-Context` downstream so per-user spaces can authorize
|
|
2208
|
+
* the request without a fresh Groth16 proof.
|
|
2209
|
+
*
|
|
2210
|
+
* `X-Muhkoo-Key` is used (rather than `Authorization: Bearer`) because it is
|
|
2211
|
+
* a non-forbidden header that skips the CORS preflight in browsers.
|
|
2212
|
+
*
|
|
2213
|
+
* The class exposes a header-injecting {@link fetch} that can be handed to
|
|
2214
|
+
* lower-level building blocks (`AuthClient`, `PersonalSpaceClient`) so they
|
|
2215
|
+
* keep owning their own wire shapes while still getting the credentials, plus
|
|
2216
|
+
* convenience JSON helpers (`get`/`post`/`del`) for code that talks to the
|
|
2217
|
+
* accelerator directly.
|
|
2218
|
+
*/
|
|
2219
|
+
interface HttpClientOptions {
|
|
2220
|
+
/** Absolute URL of the accelerator worker (trailing slash optional). */
|
|
2221
|
+
baseUrl: string;
|
|
2222
|
+
/**
|
|
2223
|
+
* App / publishable key, e.g. `mk_test_pk_…`. Sent on every request as
|
|
2224
|
+
* `X-Muhkoo-Key` when present. Transitionally optional (auth + personal
|
|
2225
|
+
* storage still function without it), but the product direction is to
|
|
2226
|
+
* REQUIRE it — it attributes traffic for billing/metering and authorizes
|
|
2227
|
+
* shared-space (messaging) websockets.
|
|
2228
|
+
*/
|
|
2229
|
+
apiKey?: string;
|
|
2230
|
+
/** Returns the current user session token, or `null` when logged out. */
|
|
2231
|
+
getSessionToken?: () => string | null;
|
|
2232
|
+
/** Custom fetch — defaults to `globalThis.fetch`. */
|
|
2233
|
+
fetch?: typeof fetch;
|
|
2234
|
+
}
|
|
2235
|
+
/** Thrown for any non-2xx accelerator response. Carries the HTTP status. */
|
|
2236
|
+
declare class HttpError extends Error {
|
|
2237
|
+
readonly status: number;
|
|
2238
|
+
readonly body?: unknown | undefined;
|
|
2239
|
+
constructor(message: string, status: number, body?: unknown | undefined);
|
|
2240
|
+
}
|
|
2241
|
+
declare class HttpClient {
|
|
2242
|
+
readonly baseUrl: string;
|
|
2243
|
+
private readonly apiKey;
|
|
2244
|
+
private readonly getSessionToken;
|
|
2245
|
+
private readonly fetchFn;
|
|
2246
|
+
constructor(opts: HttpClientOptions);
|
|
2247
|
+
/**
|
|
2248
|
+
* A `fetch`-compatible function that injects the app key + session token
|
|
2249
|
+
* headers. Hand this to `AuthClient` / `PersonalSpaceClient` so they pick
|
|
2250
|
+
* up credentials without knowing about them.
|
|
2251
|
+
*/
|
|
2252
|
+
readonly fetch: typeof fetch;
|
|
2253
|
+
get<T = unknown>(path: string): Promise<T>;
|
|
2254
|
+
post<T = unknown>(path: string, body?: unknown): Promise<T>;
|
|
2255
|
+
del<T = unknown>(path: string, body?: unknown): Promise<T>;
|
|
2256
|
+
private request;
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
/**
|
|
2260
|
+
* `client.storage` — per-user persistent storage backed by the accelerator's
|
|
2261
|
+
* personal space.
|
|
2262
|
+
*
|
|
2263
|
+
* await client.storage.set('todos', id, { title, completed })
|
|
2264
|
+
* const todo = await client.storage.get('todos', id)
|
|
2265
|
+
* const ids = await client.storage.list('todos')
|
|
2266
|
+
* await client.storage.delete('todos', id)
|
|
2267
|
+
* const off = client.storage.on('change', e => …) // realtime, cross-device
|
|
2268
|
+
*
|
|
2269
|
+
* A *collection* is a key namespace within the user's space; the stored key is
|
|
2270
|
+
* `${collection}/${id}`. Operations are authorized by the user's session token
|
|
2271
|
+
* (stamped on every request by {@link HttpClient}) — no per-op Groth16 proof.
|
|
2272
|
+
*
|
|
2273
|
+
* Values are encrypted at rest by default ({@link StorageCipher}, an AES-GCM
|
|
2274
|
+
* key derived from the ZK identity); pass `{ encrypt: false }` to store
|
|
2275
|
+
* plaintext. The realtime feed rides the personal space's own websocket, so no
|
|
2276
|
+
* separate transport is involved.
|
|
2277
|
+
*
|
|
2278
|
+
* `query()` is intentionally deferred — see the SDK overhaul plan.
|
|
2279
|
+
*/
|
|
2280
|
+
|
|
2281
|
+
interface StorageNamespaceDeps {
|
|
2282
|
+
http: HttpClient;
|
|
2283
|
+
session: SessionState;
|
|
2284
|
+
/** WebSocket base (ws/wss) for the change feed; derived from baseUrl. */
|
|
2285
|
+
wsBaseUrl: string;
|
|
2286
|
+
}
|
|
2287
|
+
interface SetOptions {
|
|
2288
|
+
/** Encrypt the value at rest (default: true). */
|
|
2289
|
+
encrypt?: boolean;
|
|
2290
|
+
}
|
|
2291
|
+
interface StorageChangeEvent<T = unknown> {
|
|
2292
|
+
collection: string;
|
|
2293
|
+
id: string;
|
|
2294
|
+
type: "set" | "delete";
|
|
2295
|
+
/** The decrypted value for `set`; `null` for `delete`. */
|
|
2296
|
+
data: T | null;
|
|
2297
|
+
}
|
|
2298
|
+
declare class StorageNamespace {
|
|
2299
|
+
private readonly deps;
|
|
2300
|
+
private cipher;
|
|
2301
|
+
private cipherIdentity;
|
|
2302
|
+
constructor(deps: StorageNamespaceDeps);
|
|
2303
|
+
/** Store `value` under `collection`/`id` (encrypted at rest by default). */
|
|
2304
|
+
set<T = unknown>(collection: string, id: string, value: T, opts?: SetOptions): Promise<void>;
|
|
2305
|
+
/** Fetch the value at `collection`/`id`, or `null` if absent. */
|
|
2306
|
+
get<T = unknown>(collection: string, id: string): Promise<T | null>;
|
|
2307
|
+
/** Delete `collection`/`id`. Returns whether the key existed. */
|
|
2308
|
+
delete(collection: string, id: string): Promise<boolean>;
|
|
2309
|
+
/** List the ids present in `collection`. */
|
|
2310
|
+
list(collection: string): Promise<string[]>;
|
|
2311
|
+
/** Reserved — server-side query is deferred (see the overhaul plan). */
|
|
2312
|
+
query(_collection: string, _filter?: unknown): Promise<never>;
|
|
2313
|
+
/**
|
|
2314
|
+
* Subscribe to realtime changes to this user's data across devices. Opens
|
|
2315
|
+
* the personal space's websocket and decrypts each change before handing
|
|
2316
|
+
* it to `handler`. Returns an unsubscribe function.
|
|
2317
|
+
*
|
|
2318
|
+
* Requires a `WebSocket` global (browsers; Node 22+ or a `ws` shim).
|
|
2319
|
+
*/
|
|
2320
|
+
on<T = unknown>(event: "change", handler: (e: StorageChangeEvent<T>) => void): () => void;
|
|
2321
|
+
private dispatchChange;
|
|
2322
|
+
/** Decode a stored value: decrypt envelopes, pass plaintext through. */
|
|
2323
|
+
private decode;
|
|
2324
|
+
private commitment;
|
|
2325
|
+
private spacePath;
|
|
2326
|
+
private kvPath;
|
|
2327
|
+
/** The at-rest cipher for the current identity (re-derived if it changes). */
|
|
2328
|
+
private getCipher;
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
/**
|
|
2332
|
+
* `Room` — a client handle for one shared space (a chat-style room).
|
|
2333
|
+
*
|
|
2334
|
+
* A shared space carries two things, and `Room` exposes both:
|
|
2335
|
+
* - **realtime E2E messaging** over a {@link BroadcastChannel} (group
|
|
2336
|
+
* handshake, ratchet send/receive, roster frames);
|
|
2337
|
+
* - **room-scoped file storage** via {@link FileStorage} over the room's
|
|
2338
|
+
* shard endpoint.
|
|
2339
|
+
*
|
|
2340
|
+
* Obtained from `client.message.room(name)`. The websocket isn't opened until
|
|
2341
|
+
* `connect()` — and `putFile`/`getFile` don't need it at all (they're plain
|
|
2342
|
+
* shard HTTP), so reading a shared file never opens a socket. The upgrade
|
|
2343
|
+
* ticket + identity are resolved lazily inside `connect()`.
|
|
2344
|
+
*
|
|
2345
|
+
* `Room` intentionally mirrors the `BroadcastChannel` surface the chat hook
|
|
2346
|
+
* already uses (`on`/`off`/`send`/`sendRaw`/`announce`/`peers`/`isConnected`)
|
|
2347
|
+
* so adopting it is a drop-in swap for `new BroadcastChannel(...)`.
|
|
2348
|
+
*/
|
|
2349
|
+
|
|
2350
|
+
type Listener = (e: CustomEvent) => void;
|
|
2351
|
+
interface RoomFileMetadata {
|
|
2352
|
+
name: string;
|
|
2353
|
+
type: string;
|
|
2354
|
+
path?: string;
|
|
2355
|
+
lastModified?: number;
|
|
2356
|
+
}
|
|
2357
|
+
interface RoomDeps {
|
|
2358
|
+
name: string;
|
|
2359
|
+
wsBaseUrl: string;
|
|
2360
|
+
httpBaseUrl: string;
|
|
2361
|
+
/** Header-injecting fetch from the client's HttpClient. */
|
|
2362
|
+
fetch: typeof fetch;
|
|
2363
|
+
/** Resolves the user id used for the ratchet (the signed-in username). */
|
|
2364
|
+
myId: () => string;
|
|
2365
|
+
/** Resolves a short-lived WS upgrade ticket (or null when keyless). */
|
|
2366
|
+
fetchTicket: () => Promise<string | null>;
|
|
2367
|
+
}
|
|
2368
|
+
declare class Room {
|
|
2369
|
+
private readonly deps;
|
|
2370
|
+
readonly name: string;
|
|
2371
|
+
private channel;
|
|
2372
|
+
private readonly pending;
|
|
2373
|
+
constructor(deps: RoomDeps);
|
|
2374
|
+
/** Open the room websocket (resolves the ticket + identity lazily). */
|
|
2375
|
+
connect(): Promise<void>;
|
|
2376
|
+
disconnect(): void;
|
|
2377
|
+
/** Subscribe to a channel event. Buffered until `connect()` if early. */
|
|
2378
|
+
on(event: string, handler: Listener): void;
|
|
2379
|
+
off(event: string, handler: Listener): void;
|
|
2380
|
+
/** Advertise our keyExchange to the room (idempotent per connection). */
|
|
2381
|
+
announce(): Promise<void>;
|
|
2382
|
+
/** E2E-encrypt `text` to every peer; returns the recipient count. */
|
|
2383
|
+
send(text: string): Promise<number>;
|
|
2384
|
+
/** Send an arbitrary JSON frame (e.g. the `{name}` handshake). */
|
|
2385
|
+
sendRaw(frame: unknown): void;
|
|
2386
|
+
peers(): string[];
|
|
2387
|
+
isConnected(): boolean;
|
|
2388
|
+
/** The underlying channel, for advanced use. `null` before `connect()`. */
|
|
2389
|
+
get raw(): BroadcastChannel | null;
|
|
2390
|
+
/** Encrypt + erasure-code + upload `file` to this room's shards. */
|
|
2391
|
+
putFile(file: File | Blob | Uint8Array, metadata: RoomFileMetadata): Promise<{
|
|
2392
|
+
manifest: FileManifest;
|
|
2393
|
+
stat: FileStat;
|
|
2394
|
+
}>;
|
|
2395
|
+
/** Fetch + decode a file previously shared in this room. */
|
|
2396
|
+
getFile(manifest: FileManifest): Promise<{
|
|
2397
|
+
data: Uint8Array;
|
|
2398
|
+
stat: FileStat;
|
|
2399
|
+
}>;
|
|
2400
|
+
private fileStorage;
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
/**
|
|
2404
|
+
* `client.message` — realtime messaging over the shared-space primitive.
|
|
2405
|
+
*
|
|
2406
|
+
* const sub = client.message.subscribe('todos', e => …) // pub/sub feed
|
|
2407
|
+
* await client.message.publish('todos', data)
|
|
2408
|
+
* await client.message.send('user:abc', { text: 'Hello!' }) // E2E DM
|
|
2409
|
+
*
|
|
2410
|
+
* Two distinct modes, one transport (a shared-space room websocket via
|
|
2411
|
+
* {@link BroadcastChannel}):
|
|
2412
|
+
*
|
|
2413
|
+
* - **pub/sub** — `publish`/`subscribe(subject)` ride plaintext `pub` frames
|
|
2414
|
+
* the accelerator blind-relays to everyone in the room `pub:<subject>`.
|
|
2415
|
+
* - **direct messages** — `send('user:x', …)` is end-to-end encrypted via the
|
|
2416
|
+
* Double Ratchet, fanned out in the recipient's inbox room `inbox:x`;
|
|
2417
|
+
* the recipient receives them by `subscribe('user:x', …)` (their own id).
|
|
2418
|
+
*
|
|
2419
|
+
* Every room websocket is opened with a short-lived signed ticket fetched from
|
|
2420
|
+
* `POST /api/ws-ticket` (browsers can't set the app-key header on
|
|
2421
|
+
* `new WebSocket()`), then completes the room's `{name}` → `{ready}` handshake
|
|
2422
|
+
* before any frame is sent.
|
|
2423
|
+
*/
|
|
2424
|
+
|
|
2425
|
+
/** The subset of {@link BroadcastChannel} this namespace drives (test seam). */
|
|
2426
|
+
interface ChannelLike {
|
|
2427
|
+
on(event: string, handler: (e: CustomEvent) => void): void;
|
|
2428
|
+
off(event: string, handler: (e: CustomEvent) => void): void;
|
|
2429
|
+
connect(): Promise<void>;
|
|
2430
|
+
disconnect(): void;
|
|
2431
|
+
announce(): Promise<void>;
|
|
2432
|
+
send(plaintext: string): Promise<number>;
|
|
2433
|
+
sendRaw(frame: unknown): void;
|
|
2434
|
+
}
|
|
2435
|
+
interface MessageNamespaceDeps {
|
|
2436
|
+
http: HttpClient;
|
|
2437
|
+
session: SessionState;
|
|
2438
|
+
/** WebSocket base (ws/wss); derived from the accelerator baseUrl. */
|
|
2439
|
+
wsBaseUrl: string;
|
|
2440
|
+
/** Test seam — defaults to constructing a real {@link BroadcastChannel}. */
|
|
2441
|
+
createChannel?: (url: string, myId: string) => ChannelLike;
|
|
2442
|
+
}
|
|
2443
|
+
interface MuhkooMessageEvent<T = unknown> {
|
|
2444
|
+
subject: string;
|
|
2445
|
+
from: string;
|
|
2446
|
+
data: T;
|
|
2447
|
+
}
|
|
2448
|
+
/** Handle returned by `subscribe`; call `.unsubscribe()` to detach. */
|
|
2449
|
+
interface MessageSubscription {
|
|
2450
|
+
subject: string;
|
|
2451
|
+
unsubscribe(): void;
|
|
2452
|
+
}
|
|
2453
|
+
declare class MessageNamespace {
|
|
2454
|
+
private readonly deps;
|
|
2455
|
+
/** One connection per room, memoized. */
|
|
2456
|
+
private readonly rooms;
|
|
2457
|
+
constructor(deps: MessageNamespaceDeps);
|
|
2458
|
+
/**
|
|
2459
|
+
* Subscribe to a subject (pub/sub) or to this user's own id (`user:<me>`)
|
|
2460
|
+
* to receive direct messages. Returns an unsubscribe handle.
|
|
2461
|
+
*/
|
|
2462
|
+
subscribe<T = unknown>(subject: string, handler: (e: MuhkooMessageEvent<T>) => void): MessageSubscription;
|
|
2463
|
+
/** Publish `data` to every subscriber of `subject` (plaintext fan-out). */
|
|
2464
|
+
publish<T = unknown>(subject: string, data: T): Promise<void>;
|
|
2465
|
+
/**
|
|
2466
|
+
* Send an end-to-end-encrypted direct message to `user:<id>`. Delivered in
|
|
2467
|
+
* the recipient's inbox room; the recipient must be subscribed
|
|
2468
|
+
* (`subscribe('user:<id>')`) and online to receive it (E2E requires a live
|
|
2469
|
+
* ratchet between both parties).
|
|
2470
|
+
*/
|
|
2471
|
+
send<T = unknown>(target: string, payload: T): Promise<void>;
|
|
2472
|
+
/**
|
|
2473
|
+
* Mint a new private shared-space id, server-side. The id MUST come from
|
|
2474
|
+
* the accelerator: it's a 64-hex string that embeds the spaces DO class
|
|
2475
|
+
* hash, which the worker validates on every subsequent request — client-
|
|
2476
|
+
* generated ids would be rejected. Pass the returned id to {@link room}
|
|
2477
|
+
* (or share it) to open the space.
|
|
2478
|
+
*/
|
|
2479
|
+
createRoom(): Promise<string>;
|
|
2480
|
+
/**
|
|
2481
|
+
* Open a handle to a shared-space room — group E2E messaging plus
|
|
2482
|
+
* room-scoped file storage. The websocket isn't opened until
|
|
2483
|
+
* `room.connect()`; `room.putFile`/`getFile` work without it. This is the
|
|
2484
|
+
* primitive group chat is built on (`publish`/`subscribe`/`send` are the
|
|
2485
|
+
* higher-level conveniences layered over the same space transport).
|
|
2486
|
+
*/
|
|
2487
|
+
room(roomName: string): Room;
|
|
2488
|
+
/** Close every open room connection (e.g. on logout). */
|
|
2489
|
+
disconnect(): void;
|
|
2490
|
+
private getRoom;
|
|
2491
|
+
private openRoom;
|
|
2492
|
+
/** Fetch a short-lived WS upgrade ticket scoped to this app key. */
|
|
2493
|
+
private fetchTicket;
|
|
2494
|
+
private myId;
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
/** The hosted Muhkoo Accelerator — the default {@link ClientOptions.baseUrl}. */
|
|
2498
|
+
declare const DEFAULT_BASE_URL = "https://api.muhkoo.dev";
|
|
2499
|
+
interface ClientOptions {
|
|
2500
|
+
/**
|
|
2501
|
+
* App / publishable key issued by the accelerator (e.g. `mk_test_pk_…`).
|
|
2502
|
+
*
|
|
2503
|
+
* TRANSITIONALLY optional: auth + storage currently function without it so
|
|
2504
|
+
* the in-tree web app can migrate before a key is provisioned. The product
|
|
2505
|
+
* direction is that the app key becomes REQUIRED — it's how every app
|
|
2506
|
+
* authenticates to the Accelerator and gets attributed for metering/billing.
|
|
2507
|
+
* New integrations should always pass one.
|
|
2508
|
+
*/
|
|
2509
|
+
apiKey?: string;
|
|
2510
|
+
/**
|
|
2511
|
+
* Absolute URL of the accelerator (trailing slash optional). Defaults to
|
|
2512
|
+
* the hosted Muhkoo Accelerator ({@link DEFAULT_BASE_URL}); override to
|
|
2513
|
+
* point at staging, a local `wrangler dev`, or a self-hosted deployment.
|
|
2514
|
+
*/
|
|
2515
|
+
baseUrl?: string;
|
|
2516
|
+
/**
|
|
2517
|
+
* URLs of the `preimagePoK` circuit assets used for ZK login proofs.
|
|
2518
|
+
* Defaults to {@link defaultCircuitUrls} anchored at `baseUrl` — i.e.
|
|
2519
|
+
* `${baseUrl}/circuits/build/preimagePoK_js/preimagePoK.wasm` and
|
|
2520
|
+
* `…/preimagePoK_0001.zkey`, where the accelerator serves them.
|
|
2521
|
+
*/
|
|
2522
|
+
circuits?: CircuitUrls;
|
|
2523
|
+
/** Pluggable session-token persistence. Defaults to in-memory. */
|
|
2524
|
+
sessionStore?: SessionStore;
|
|
2525
|
+
/** Custom fetch — defaults to `globalThis.fetch`. */
|
|
2526
|
+
fetch?: typeof fetch;
|
|
2527
|
+
/** Optional log level for the SDK logger. */
|
|
2528
|
+
logLevel?: string;
|
|
2529
|
+
}
|
|
2530
|
+
declare class Client {
|
|
2531
|
+
readonly baseUrl: string;
|
|
2532
|
+
/** Authentication — `client.auth.zk.login(...)`, etc. */
|
|
2533
|
+
readonly auth: AuthNamespace;
|
|
2534
|
+
/** Per-user persistent storage — `client.storage.set(...)`, etc. */
|
|
2535
|
+
readonly storage: StorageNamespace;
|
|
2536
|
+
/** Realtime messaging — `client.message.subscribe(...)`, etc. */
|
|
2537
|
+
readonly message: MessageNamespace;
|
|
2538
|
+
private readonly session;
|
|
2539
|
+
private readonly http;
|
|
2540
|
+
constructor(options?: ClientOptions);
|
|
2541
|
+
/** The currently signed-in user, or `null`. */
|
|
2542
|
+
get user(): AuthUser | null;
|
|
2543
|
+
/** Whether a token-bearing session is active. */
|
|
2544
|
+
get isAuthenticated(): boolean;
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
export { AES_GCM_IV_BYTES, AES_GCM_KEY_BYTES, AES_GCM_TAG_BYTES, AuthClient, AuthNamespace, AuthPublicInput, Authenticator, BroadcastChannel, BroadcastChannelEvents, ChunkCipher, Client, DEFAULT_BASE_URL, DoubleRatchet, DoubleRatchetManager, EncryptedSession, EventCore, EventCoreEvents, Field, Field as FieldElement, FileStorage, HashKnowledge, HttpClient, HttpError, KeyStore, LocalStorageSessionStore, MemorySessionStore, Message, Message as MessageDefault, MessageNamespace, PBKDF2_DEFAULT_ITERATIONS, PREIMAGE_POK_VERIFICATION_KEY, Packet, PersonalSpaceClient, Poseidon, PreimagePoK, ReedSolomonCodec, Room, SerializeMessage, SessionState, ShardClient, SharedSpaceClient, StorageNamespace, WSTransport, ZkAuth, buildCommitment, compilePrograms, decodeFromHex, decorators, decryptAesGcm, defaultCircuitUrls, defaultSessionStore, deriveAesKeyFromPassphrase, deriveBitsHkdf, deriveIdentity, encodeToHex, encryptAesGcm, exportPublicKeyBase64, exportPublicKeyHex, generateAuthProof, getSubtle, initBn128Wasm, initRsWasm, initializeCircuits, manifestToStat, poseidonHash, quickVerify, randomBytes, shardHash, signMessage, toField, unwrapWithPassphrase, verify, verifyGroth16, verifyHashKnowledge, verifyPreimagePoK, wrapWithPassphrase };
|
|
2548
|
+
export type { ACL, ACLEntry, AuthClientOptions, AuthKeyPair, AuthSuccess, AuthToken, AuthType, AuthUser, Bn128WasmInstance, BroadcastChannelEvent, BroadcastChannelOptions, ChannelLike, ChunkKeyMaterial, ChunkManifest, CipherFrame, CipherMessage, CipherMessageHeader, CircuitBufferConfig, CircuitUrls, ClientOptions, DehydratedKeypair, DehydratedKeys, ECDHCurve, EncodeResult, FileManifest, FileStat, FileStorageOptions, Groth16Proof, HttpClientOptions, IncomingFrame, KeyExchangeFrame, KeyPair, Keypair, LoginOptions, MessageBody, MessageEvent, MessageHeaders, MessageNamespaceDeps, MessageOptions, MessageSubscription, MuhkooMessageEvent, OAuthProvider, PacketOptions, Participant, Permission, PermissionCheckRequest, PermissionCheckResult, PersonalSpaceClientOptions, PublicKeyJWK, RatchetState, ReceiveResult, RegisterParams, Role, RoomDeps, RoomFileMetadata, Session, SessionOptions, SessionStore, SetOptions, ShardClientOptions, SharedSpaceClientOptions, SnarkProof, StorageChangeEvent, StorageNamespaceDeps, StoredSession, Subscription, UserIdentity, VerificationKey, WSMessage, WSMessageType, WSTransportOptions, WrappedPayload, WriteFileInput, WriteToShardsInput, ZkAuthDeps, ZkAuthenticateRequest, ZkChallengeResponse, ZkCompiled, ZkIdentity, ZkRegisterRequest, ZkRegisterResponse };
|