@muhkoo/connect 0.2.0-alpha.2 → 0.4.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/README.md +73 -6
- package/dist/browser/index.js +844 -282
- package/dist/browser/index.js.map +1 -1
- package/dist/connect.d.ts +661 -121
- package/dist/server/index.js +1456 -894
- package/dist/server/index.js.map +1 -1
- package/dist/workers/index.js +138 -4
- package/dist/workers/index.js.map +1 -1
- package/package.json +1 -1
package/dist/browser/index.js
CHANGED
|
@@ -2383,11 +2383,31 @@ function isCipherFrame(frame) {
|
|
|
2383
2383
|
* - Emit lifecycle events (CONNECTED / DISCONNECTED / RECONNECTING / ERROR)
|
|
2384
2384
|
* plus raw inbound MESSAGE events
|
|
2385
2385
|
*/
|
|
2386
|
+
/** Monotonic source of unique transport ids when a caller doesn't supply one. */
|
|
2387
|
+
let __wsInstanceSeq = 0;
|
|
2386
2388
|
class WSTransport extends EventCore {
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2389
|
+
/**
|
|
2390
|
+
* Per-instance event scope. `EventCore` is a process-global static emitter,
|
|
2391
|
+
* so without namespacing every WSTransport would hear every other one's
|
|
2392
|
+
* frames. We prefix each event with this transport's `id` and expose
|
|
2393
|
+
* `on`/`off`/`emit` that scope transparently — callers still use the plain
|
|
2394
|
+
* `EventCoreEvents` enum and only ever see their own connection's events.
|
|
2395
|
+
*/
|
|
2396
|
+
id;
|
|
2397
|
+
emit = (event, data) => {
|
|
2398
|
+
EventCore.emit(this.scopedEvent(event), data);
|
|
2399
|
+
};
|
|
2400
|
+
on = (event, handler) => {
|
|
2401
|
+
EventCore.on(this.scopedEvent(event), handler);
|
|
2402
|
+
};
|
|
2403
|
+
off = (event, handler) => {
|
|
2404
|
+
EventCore.off(this.scopedEvent(event), handler);
|
|
2405
|
+
};
|
|
2406
|
+
scopedEvent(event) {
|
|
2407
|
+
return `${this.id}:${event}`;
|
|
2408
|
+
}
|
|
2390
2409
|
url;
|
|
2410
|
+
urlProvider;
|
|
2391
2411
|
socket = null;
|
|
2392
2412
|
_isConnected = false;
|
|
2393
2413
|
_isConnecting = false;
|
|
@@ -2400,16 +2420,29 @@ class WSTransport extends EventCore {
|
|
|
2400
2420
|
// Outbound queue (raw frame strings — already serialized by caller)
|
|
2401
2421
|
outboundQueue = [];
|
|
2402
2422
|
maxQueueSize;
|
|
2423
|
+
// Heartbeat / liveness state
|
|
2424
|
+
heartbeatInterval;
|
|
2425
|
+
heartbeatTimeout;
|
|
2426
|
+
pingFrame;
|
|
2427
|
+
pongFrame;
|
|
2428
|
+
heartbeatTimer = null;
|
|
2429
|
+
pongTimer = null;
|
|
2403
2430
|
constructor(opts) {
|
|
2404
2431
|
super();
|
|
2405
2432
|
if (!opts || !opts.url) {
|
|
2406
2433
|
throw new Error("WSTransport: `url` is required");
|
|
2407
2434
|
}
|
|
2408
2435
|
this.url = opts.url;
|
|
2436
|
+
this.urlProvider = opts.urlProvider;
|
|
2437
|
+
this.id = opts.id ?? `ws${++__wsInstanceSeq}`;
|
|
2409
2438
|
this.autoReconnect = opts.autoReconnect ?? true;
|
|
2410
2439
|
this.reconnectDelay = opts.reconnectDelay ?? 3000;
|
|
2411
2440
|
this.maxReconnectAttempts = opts.maxReconnectAttempts ?? 5;
|
|
2412
2441
|
this.maxQueueSize = opts.maxQueueSize ?? 100;
|
|
2442
|
+
this.heartbeatInterval = opts.heartbeatInterval ?? 30000;
|
|
2443
|
+
this.heartbeatTimeout = opts.heartbeatTimeout ?? 10000;
|
|
2444
|
+
this.pingFrame = opts.pingFrame ?? "ping";
|
|
2445
|
+
this.pongFrame = opts.pongFrame ?? "pong";
|
|
2413
2446
|
}
|
|
2414
2447
|
/** Open the socket. Resolves when CONNECTED fires. Rejects on first error. */
|
|
2415
2448
|
async connect() {
|
|
@@ -2417,6 +2450,18 @@ class WSTransport extends EventCore {
|
|
|
2417
2450
|
throw new Error("WSTransport: already connected or connecting");
|
|
2418
2451
|
}
|
|
2419
2452
|
this._isConnecting = true;
|
|
2453
|
+
// On a RECONNECT, refresh the URL (the baked-in single-use ticket is spent).
|
|
2454
|
+
// The first connect uses `url` as-is. If the provider throws (e.g. can't
|
|
2455
|
+
// mint a ticket), surface it and let scheduleReconnect retry.
|
|
2456
|
+
if (this.urlProvider && this.reconnectAttempts > 0) {
|
|
2457
|
+
try {
|
|
2458
|
+
this.url = await this.urlProvider();
|
|
2459
|
+
}
|
|
2460
|
+
catch (err) {
|
|
2461
|
+
this._isConnecting = false;
|
|
2462
|
+
throw err;
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2420
2465
|
return new Promise((resolve, reject) => {
|
|
2421
2466
|
try {
|
|
2422
2467
|
const socket = new WebSocket(this.url);
|
|
@@ -2427,9 +2472,19 @@ class WSTransport extends EventCore {
|
|
|
2427
2472
|
this.reconnectAttempts = 0;
|
|
2428
2473
|
this.emit(EventCoreEvents.CONNECTED, undefined);
|
|
2429
2474
|
this.flushQueue();
|
|
2475
|
+
this.startHeartbeat();
|
|
2430
2476
|
resolve();
|
|
2431
2477
|
};
|
|
2432
2478
|
socket.onmessage = (event) => {
|
|
2479
|
+
// Heartbeat ack — swallow it (don't surface as a MESSAGE) and clear
|
|
2480
|
+
// the liveness deadline. The layer above never sees keep-alive frames.
|
|
2481
|
+
if (event.data === this.pongFrame) {
|
|
2482
|
+
if (this.pongTimer) {
|
|
2483
|
+
clearTimeout(this.pongTimer);
|
|
2484
|
+
this.pongTimer = null;
|
|
2485
|
+
}
|
|
2486
|
+
return;
|
|
2487
|
+
}
|
|
2433
2488
|
// Raw inbound frame — layer above is responsible for parsing.
|
|
2434
2489
|
this.emit(EventCoreEvents.MESSAGE, event.data);
|
|
2435
2490
|
};
|
|
@@ -2444,6 +2499,7 @@ class WSTransport extends EventCore {
|
|
|
2444
2499
|
const wasConnected = this._isConnected;
|
|
2445
2500
|
this._isConnected = false;
|
|
2446
2501
|
this._isConnecting = false;
|
|
2502
|
+
this.stopHeartbeat();
|
|
2447
2503
|
this.emit(EventCoreEvents.DISCONNECTED, undefined);
|
|
2448
2504
|
if (this.autoReconnect && wasConnected) {
|
|
2449
2505
|
this.scheduleReconnect();
|
|
@@ -2462,6 +2518,7 @@ class WSTransport extends EventCore {
|
|
|
2462
2518
|
clearTimeout(this.reconnectTimeout);
|
|
2463
2519
|
this.reconnectTimeout = null;
|
|
2464
2520
|
}
|
|
2521
|
+
this.stopHeartbeat();
|
|
2465
2522
|
this.autoReconnect = false;
|
|
2466
2523
|
if (this.socket) {
|
|
2467
2524
|
try {
|
|
@@ -2492,6 +2549,78 @@ class WSTransport extends EventCore {
|
|
|
2492
2549
|
queuedFrames() { return this.outboundQueue.length; }
|
|
2493
2550
|
reconnectAttemptCount() { return this.reconnectAttempts; }
|
|
2494
2551
|
// -- internals -------------------------------------------------------------
|
|
2552
|
+
/** Begin the keep-alive loop (no-op when disabled). */
|
|
2553
|
+
startHeartbeat() {
|
|
2554
|
+
if (this.heartbeatInterval <= 0)
|
|
2555
|
+
return;
|
|
2556
|
+
this.stopHeartbeat();
|
|
2557
|
+
this.heartbeatTimer = setInterval(() => this.sendHeartbeat(), this.heartbeatInterval);
|
|
2558
|
+
}
|
|
2559
|
+
/** Stop the keep-alive loop and clear any pending liveness deadline. */
|
|
2560
|
+
stopHeartbeat() {
|
|
2561
|
+
if (this.heartbeatTimer) {
|
|
2562
|
+
clearInterval(this.heartbeatTimer);
|
|
2563
|
+
this.heartbeatTimer = null;
|
|
2564
|
+
}
|
|
2565
|
+
if (this.pongTimer) {
|
|
2566
|
+
clearTimeout(this.pongTimer);
|
|
2567
|
+
this.pongTimer = null;
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
/**
|
|
2571
|
+
* Send one ping and arm a liveness deadline. If no `pongFrame` arrives before
|
|
2572
|
+
* `heartbeatTimeout`, the connection is considered dead and we force a
|
|
2573
|
+
* reconnect — this is what catches a silently-dropped TCP connection that
|
|
2574
|
+
* never fired `onclose` (the failure mode that "loses connection with the
|
|
2575
|
+
* Accelerator" on long sessions).
|
|
2576
|
+
*/
|
|
2577
|
+
sendHeartbeat() {
|
|
2578
|
+
if (!this._isConnected || !this.socket)
|
|
2579
|
+
return;
|
|
2580
|
+
try {
|
|
2581
|
+
this.socket.send(this.pingFrame);
|
|
2582
|
+
}
|
|
2583
|
+
catch {
|
|
2584
|
+
this.forceReconnect();
|
|
2585
|
+
return;
|
|
2586
|
+
}
|
|
2587
|
+
// A still-pending deadline means the previous pong never came back.
|
|
2588
|
+
if (this.pongTimer)
|
|
2589
|
+
return;
|
|
2590
|
+
this.pongTimer = setTimeout(() => {
|
|
2591
|
+
this.pongTimer = null;
|
|
2592
|
+
this.forceReconnect();
|
|
2593
|
+
}, this.heartbeatTimeout);
|
|
2594
|
+
}
|
|
2595
|
+
/**
|
|
2596
|
+
* Tear down a connection we believe is dead and kick off reconnect, without
|
|
2597
|
+
* waiting on the OS/browser to eventually fire `onclose` (which a half-open
|
|
2598
|
+
* socket may never do). We detach the handlers first so the normal close path
|
|
2599
|
+
* can't double-fire the reconnect.
|
|
2600
|
+
*/
|
|
2601
|
+
forceReconnect() {
|
|
2602
|
+
this.stopHeartbeat();
|
|
2603
|
+
const sock = this.socket;
|
|
2604
|
+
const wasConnected = this._isConnected;
|
|
2605
|
+
this.socket = null;
|
|
2606
|
+
this._isConnected = false;
|
|
2607
|
+
this._isConnecting = false;
|
|
2608
|
+
if (sock) {
|
|
2609
|
+
sock.onopen = null;
|
|
2610
|
+
sock.onmessage = null;
|
|
2611
|
+
sock.onerror = null;
|
|
2612
|
+
sock.onclose = null;
|
|
2613
|
+
try {
|
|
2614
|
+
sock.close();
|
|
2615
|
+
}
|
|
2616
|
+
catch { /* already gone */ }
|
|
2617
|
+
}
|
|
2618
|
+
this.emit(EventCoreEvents.ERROR, new Error("WSTransport: heartbeat timeout"));
|
|
2619
|
+
this.emit(EventCoreEvents.DISCONNECTED, undefined);
|
|
2620
|
+
if (this.autoReconnect && wasConnected) {
|
|
2621
|
+
this.scheduleReconnect();
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2495
2624
|
scheduleReconnect() {
|
|
2496
2625
|
if (this.maxReconnectAttempts > 0 &&
|
|
2497
2626
|
this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
@@ -2505,7 +2634,12 @@ class WSTransport extends EventCore {
|
|
|
2505
2634
|
await this.connect();
|
|
2506
2635
|
}
|
|
2507
2636
|
catch {
|
|
2508
|
-
//
|
|
2637
|
+
// A failed reconnect never opened the socket, so its `onclose` won't
|
|
2638
|
+
// re-schedule (that path is guarded by `wasConnected`). Re-schedule here
|
|
2639
|
+
// so we keep retrying (with a fresh ticket each time) until we succeed
|
|
2640
|
+
// or hit the attempt cap.
|
|
2641
|
+
if (this.autoReconnect)
|
|
2642
|
+
this.scheduleReconnect();
|
|
2509
2643
|
}
|
|
2510
2644
|
}, this.reconnectDelay);
|
|
2511
2645
|
}
|
|
@@ -3040,7 +3174,7 @@ function alloc(u32, size) {
|
|
|
3040
3174
|
* a convenient way to passphrase-encrypt a payload before putting it.
|
|
3041
3175
|
*/
|
|
3042
3176
|
let _snarkjs$1 = null;
|
|
3043
|
-
async function loadSnarkjs$
|
|
3177
|
+
async function loadSnarkjs$1() {
|
|
3044
3178
|
if (_snarkjs$1)
|
|
3045
3179
|
return _snarkjs$1;
|
|
3046
3180
|
_snarkjs$1 = (await import('snarkjs'));
|
|
@@ -3054,14 +3188,14 @@ async function loadSnarkjs$2() {
|
|
|
3054
3188
|
* Must stay in sync with the `nonce` public-signal width used by both
|
|
3055
3189
|
* accelerator's `verifyZkAuthProof` and the `preimagePoK` circuit.
|
|
3056
3190
|
*/
|
|
3057
|
-
const FIELD_SIZE$
|
|
3191
|
+
const FIELD_SIZE$1 = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
|
|
3058
3192
|
/**
|
|
3059
3193
|
* Reduce a hex nonce into a BN254 field-element decimal string, matching what
|
|
3060
3194
|
* the Circom circuit expects as the `nonce` public signal.
|
|
3061
3195
|
*/
|
|
3062
|
-
function nonceHexToFieldString
|
|
3196
|
+
function nonceHexToFieldString(nonceHex) {
|
|
3063
3197
|
const hex = nonceHex.startsWith("0x") ? nonceHex : "0x" + nonceHex;
|
|
3064
|
-
return (BigInt(hex) % FIELD_SIZE$
|
|
3198
|
+
return (BigInt(hex) % FIELD_SIZE$1).toString();
|
|
3065
3199
|
}
|
|
3066
3200
|
/**
|
|
3067
3201
|
* Read the response body as JSON, falling back to text for the error message
|
|
@@ -3163,7 +3297,7 @@ class PersonalSpaceClient {
|
|
|
3163
3297
|
*/
|
|
3164
3298
|
async proveFreshChallenge() {
|
|
3165
3299
|
const challenge = await this.requestChallenge();
|
|
3166
|
-
const nonceField = nonceHexToFieldString
|
|
3300
|
+
const nonceField = nonceHexToFieldString(challenge.nonce);
|
|
3167
3301
|
// Inputs to the `preimagePoK` circuit. Public signals are
|
|
3168
3302
|
// `[commitment, nonce, ecdsaPubHash]`; the rest are private witnesses.
|
|
3169
3303
|
const input = {
|
|
@@ -3174,7 +3308,7 @@ class PersonalSpaceClient {
|
|
|
3174
3308
|
salt: this.salt,
|
|
3175
3309
|
ecdsaPub: this.ecdsaPub,
|
|
3176
3310
|
};
|
|
3177
|
-
const snarkjs = await loadSnarkjs$
|
|
3311
|
+
const snarkjs = await loadSnarkjs$1();
|
|
3178
3312
|
const { proof, publicSignals } = await snarkjs.groth16.fullProve(input, this.wasmUrl, this.zkeyUrl);
|
|
3179
3313
|
return {
|
|
3180
3314
|
challengeId: challenge.challengeId,
|
|
@@ -3821,6 +3955,8 @@ class FileStorage {
|
|
|
3821
3955
|
const fileId = input.fileId ?? generateId();
|
|
3822
3956
|
const createdAt = Date.now();
|
|
3823
3957
|
const chunks = [];
|
|
3958
|
+
const totalChunks = Math.max(1, Math.ceil(data.length / this.chunkSize));
|
|
3959
|
+
input.onProgress?.(0, totalChunks);
|
|
3824
3960
|
// Slicing + encrypting + encoding + uploading is done per-chunk so
|
|
3825
3961
|
// the peak memory footprint stays at ~chunkSize even for huge files.
|
|
3826
3962
|
// Within a chunk, the shard uploads are parallelized up to
|
|
@@ -3830,6 +3966,7 @@ class FileStorage {
|
|
|
3830
3966
|
const end = Math.min(start + this.chunkSize, data.length);
|
|
3831
3967
|
const plaintext = data.subarray(start, end);
|
|
3832
3968
|
chunks.push(await this.writeChunk(plaintext, chunkIndex));
|
|
3969
|
+
input.onProgress?.(chunks.length, totalChunks);
|
|
3833
3970
|
// Stop after one iteration if the file is empty — we still emit a
|
|
3834
3971
|
// single zero-length chunk so the manifest has a stable shape.
|
|
3835
3972
|
if (data.length === 0)
|
|
@@ -4017,248 +4154,94 @@ async function toUint8Array(data) {
|
|
|
4017
4154
|
}
|
|
4018
4155
|
|
|
4019
4156
|
/**
|
|
4020
|
-
* HTTP client for the
|
|
4021
|
-
*
|
|
4022
|
-
*
|
|
4023
|
-
*
|
|
4024
|
-
*
|
|
4025
|
-
*
|
|
4026
|
-
*
|
|
4027
|
-
*
|
|
4028
|
-
*
|
|
4029
|
-
*
|
|
4030
|
-
*
|
|
4031
|
-
*
|
|
4032
|
-
*
|
|
4033
|
-
*
|
|
4034
|
-
*
|
|
4035
|
-
*
|
|
4036
|
-
*
|
|
4037
|
-
*
|
|
4038
|
-
*
|
|
4039
|
-
* the next slice of work in accelerator):
|
|
4040
|
-
*
|
|
4041
|
-
* POST /api/spaces create a new space (gated by caller proof)
|
|
4042
|
-
* body: { challengeId, proof, publicSignals, initialAcl? }
|
|
4043
|
-
* returns: { spaceId }
|
|
4044
|
-
*
|
|
4045
|
-
* POST /api/spaces/:spaceId/challenge issue a nonce for the caller
|
|
4046
|
-
* body: { commitment }
|
|
4047
|
-
* returns: { challengeId, nonce }
|
|
4048
|
-
*
|
|
4049
|
-
* POST /api/spaces/:spaceId/files write a file manifest
|
|
4050
|
-
* body: { challengeId, proof, publicSignals, manifest }
|
|
4051
|
-
* returns: { ok: true, fileId }
|
|
4052
|
-
*
|
|
4053
|
-
* POST /api/spaces/:spaceId/files/:fileId/get read a file manifest
|
|
4054
|
-
* body: { challengeId, proof, publicSignals }
|
|
4055
|
-
* returns: { manifest }
|
|
4056
|
-
*
|
|
4057
|
-
* DELETE /api/spaces/:spaceId/files/:fileId delete a file manifest
|
|
4058
|
-
* body: { challengeId, proof, publicSignals }
|
|
4059
|
-
* returns: { ok: true, existed: boolean }
|
|
4060
|
-
*
|
|
4061
|
-
* POST /api/spaces/:spaceId/files/list list manifests in space
|
|
4062
|
-
* body: { challengeId, proof, publicSignals }
|
|
4063
|
-
* returns: { files: FileStat[] }
|
|
4064
|
-
*
|
|
4065
|
-
* POST /api/spaces/:spaceId/participants add a participant
|
|
4066
|
-
* body: { challengeId, proof, publicSignals, participant: Participant }
|
|
4067
|
-
* returns: { ok: true }
|
|
4068
|
-
*
|
|
4069
|
-
* DELETE /api/spaces/:spaceId/participants/:commitment remove a participant
|
|
4070
|
-
* body: { challengeId, proof, publicSignals }
|
|
4071
|
-
* returns: { ok: true, existed: boolean }
|
|
4072
|
-
*
|
|
4073
|
-
* POST /api/spaces/:spaceId/acl set / replace an ACL entry
|
|
4074
|
-
* body: { challengeId, proof, publicSignals, entry: ACLEntry }
|
|
4075
|
-
* returns: { ok: true }
|
|
4157
|
+
* HTTP client for the gated file-manifest store on a SharedSpace
|
|
4158
|
+
* (`/api/spaces/:spaceId/files`).
|
|
4159
|
+
*
|
|
4160
|
+
* A "file" in a space is its **manifest** (per-chunk keys + the SHA-256 of each
|
|
4161
|
+
* global shard). The shard bytes live in the open `/api/shards` store; the
|
|
4162
|
+
* manifest is the capability. This client only moves manifests in/out of the
|
|
4163
|
+
* space's gated store.
|
|
4164
|
+
*
|
|
4165
|
+
* Authorization is **session-based**, consistent with the rest of the
|
|
4166
|
+
* SharedSpace (keyring, websocket): the SDK's credential-stamping `fetch`
|
|
4167
|
+
* (from {@link HttpClient}) attaches the app key + session token, the platform
|
|
4168
|
+
* validates the session and injects a trusted `X-Muhkoo-User-Context`, and the
|
|
4169
|
+
* DO authorizes on that + channel membership. There is no per-op Groth16 proof.
|
|
4170
|
+
*
|
|
4171
|
+
* Wire protocol (server: `SharedSpaceDO`):
|
|
4172
|
+
* POST /api/spaces/:spaceId/files body: { manifest } → { ok, fileId }
|
|
4173
|
+
* GET /api/spaces/:spaceId/files → { files: FileStat[] }
|
|
4174
|
+
* GET /api/spaces/:spaceId/files/:fileId → { manifest }
|
|
4175
|
+
* DELETE /api/spaces/:spaceId/files/:fileId → { ok, existed }
|
|
4076
4176
|
*/
|
|
4077
|
-
let snarkjsModule = null;
|
|
4078
|
-
async function loadSnarkjs$1() {
|
|
4079
|
-
if (snarkjsModule)
|
|
4080
|
-
return snarkjsModule;
|
|
4081
|
-
snarkjsModule = (await import('snarkjs'));
|
|
4082
|
-
return snarkjsModule;
|
|
4083
|
-
}
|
|
4084
|
-
// BN254 scalar field modulus — same value used everywhere else in the ZK path.
|
|
4085
|
-
const FIELD_SIZE$1 = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
|
|
4086
4177
|
class SharedSpaceClient {
|
|
4087
4178
|
baseUrl;
|
|
4088
|
-
commitment;
|
|
4089
|
-
secret;
|
|
4090
|
-
salt;
|
|
4091
|
-
ecdsaPub;
|
|
4092
|
-
ecdsaPubHash;
|
|
4093
|
-
wasmUrl;
|
|
4094
|
-
zkeyUrl;
|
|
4095
4179
|
fetchFn;
|
|
4096
4180
|
constructor(opts) {
|
|
4097
4181
|
if (!opts?.baseUrl)
|
|
4098
4182
|
throw new Error("SharedSpaceClient: `baseUrl` is required");
|
|
4099
|
-
if (!opts.commitment)
|
|
4100
|
-
throw new Error("SharedSpaceClient: `commitment` is required");
|
|
4101
|
-
if (!opts.secret)
|
|
4102
|
-
throw new Error("SharedSpaceClient: `secret` is required");
|
|
4103
|
-
if (!opts.salt)
|
|
4104
|
-
throw new Error("SharedSpaceClient: `salt` is required");
|
|
4105
|
-
if (!opts.ecdsaPub)
|
|
4106
|
-
throw new Error("SharedSpaceClient: `ecdsaPub` is required");
|
|
4107
|
-
if (!opts.ecdsaPubHash)
|
|
4108
|
-
throw new Error("SharedSpaceClient: `ecdsaPubHash` is required");
|
|
4109
|
-
if (!opts.circuits?.wasmUrl || !opts.circuits?.zkeyUrl) {
|
|
4110
|
-
throw new Error("SharedSpaceClient: `circuits.wasmUrl` and `circuits.zkeyUrl` are required");
|
|
4111
|
-
}
|
|
4112
4183
|
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
4113
|
-
this.commitment = opts.commitment;
|
|
4114
|
-
this.secret = opts.secret;
|
|
4115
|
-
this.salt = opts.salt;
|
|
4116
|
-
this.ecdsaPub = opts.ecdsaPub;
|
|
4117
|
-
this.ecdsaPubHash = opts.ecdsaPubHash;
|
|
4118
|
-
this.wasmUrl = opts.circuits.wasmUrl;
|
|
4119
|
-
this.zkeyUrl = opts.circuits.zkeyUrl;
|
|
4120
4184
|
const f = opts.fetch ?? globalThis.fetch;
|
|
4121
4185
|
if (typeof f !== "function") {
|
|
4122
4186
|
throw new Error("SharedSpaceClient: `globalThis.fetch` is unavailable; pass an explicit fetch.");
|
|
4123
4187
|
}
|
|
4124
4188
|
this.fetchFn = f.bind(globalThis);
|
|
4125
4189
|
}
|
|
4126
|
-
/**
|
|
4127
|
-
* Create a brand new space. The caller becomes the owner; `initialAcl`
|
|
4128
|
-
* adds any additional ACL entries up front (e.g. inviting a co-owner at
|
|
4129
|
-
* creation time).
|
|
4130
|
-
*/
|
|
4131
|
-
async createSpace(initialAcl) {
|
|
4132
|
-
// Creating a space still requires the caller to prove they hold the
|
|
4133
|
-
// commitment they're claiming to own — that's what the proof
|
|
4134
|
-
// accomplishes here. The server issues a challenge against a global
|
|
4135
|
-
// "newspace" pseudo-id for the bootstrap step.
|
|
4136
|
-
const proof = await this.proveFreshChallenge("__new__");
|
|
4137
|
-
const body = { ...proof };
|
|
4138
|
-
if (initialAcl)
|
|
4139
|
-
body.initialAcl = initialAcl;
|
|
4140
|
-
const url = `${this.baseUrl}/api/spaces`;
|
|
4141
|
-
const response = await this.fetchFn(url, this.jsonRequest("POST", body));
|
|
4142
|
-
const data = await parseOrThrow(response, "createSpace");
|
|
4143
|
-
return { spaceId: data.spaceId };
|
|
4144
|
-
}
|
|
4145
|
-
/** Write a file manifest into the space. Requires `write` permission. */
|
|
4190
|
+
/** Write a file manifest into the space. Requires space membership. */
|
|
4146
4191
|
async writeFileManifest(spaceId, manifest) {
|
|
4147
|
-
const
|
|
4148
|
-
const
|
|
4149
|
-
|
|
4150
|
-
const response = await this.fetchFn(url, this.jsonRequest("POST", body));
|
|
4151
|
-
await parseOrThrow(response, `writeFileManifest(${manifest.id})`);
|
|
4192
|
+
const url = `${this.filesBase(spaceId)}`;
|
|
4193
|
+
const res = await this.fetchFn(url, this.jsonRequest("POST", { manifest }));
|
|
4194
|
+
await parseOrThrow(res, `writeFileManifest(${manifest.id})`);
|
|
4152
4195
|
}
|
|
4153
|
-
/** Read a file manifest by id. Requires
|
|
4196
|
+
/** Read a file manifest by id. Requires space membership. */
|
|
4154
4197
|
async readFileManifest(spaceId, fileId) {
|
|
4155
|
-
const
|
|
4156
|
-
const
|
|
4157
|
-
const
|
|
4158
|
-
const response = await this.fetchFn(url, this.jsonRequest("POST", body));
|
|
4159
|
-
const data = await parseOrThrow(response, `readFileManifest(${fileId})`);
|
|
4198
|
+
const url = `${this.filesBase(spaceId)}/${encodeURIComponent(fileId)}`;
|
|
4199
|
+
const res = await this.fetchFn(url, this.jsonRequest("GET"));
|
|
4200
|
+
const data = await parseOrThrow(res, `readFileManifest(${fileId})`);
|
|
4160
4201
|
return data.manifest;
|
|
4161
4202
|
}
|
|
4162
|
-
/** Delete a file manifest by id. Requires
|
|
4203
|
+
/** Delete a file manifest by id. Requires space membership. */
|
|
4163
4204
|
async deleteFileManifest(spaceId, fileId) {
|
|
4164
|
-
const
|
|
4165
|
-
const
|
|
4166
|
-
const
|
|
4167
|
-
const response = await this.fetchFn(url, this.jsonRequest("DELETE", body));
|
|
4168
|
-
const data = await parseOrThrow(response, `deleteFileManifest(${fileId})`);
|
|
4205
|
+
const url = `${this.filesBase(spaceId)}/${encodeURIComponent(fileId)}`;
|
|
4206
|
+
const res = await this.fetchFn(url, this.jsonRequest("DELETE"));
|
|
4207
|
+
const data = await parseOrThrow(res, `deleteFileManifest(${fileId})`);
|
|
4169
4208
|
return data.existed;
|
|
4170
4209
|
}
|
|
4171
|
-
/** List file summaries in the space. Requires
|
|
4210
|
+
/** List file summaries in the space. Requires space membership. */
|
|
4172
4211
|
async listFiles(spaceId) {
|
|
4173
|
-
const
|
|
4174
|
-
const
|
|
4175
|
-
const
|
|
4176
|
-
const response = await this.fetchFn(url, this.jsonRequest("POST", body));
|
|
4177
|
-
const data = await parseOrThrow(response, "listFiles");
|
|
4212
|
+
const url = `${this.filesBase(spaceId)}`;
|
|
4213
|
+
const res = await this.fetchFn(url, this.jsonRequest("GET"));
|
|
4214
|
+
const data = await parseOrThrow(res, "listFiles");
|
|
4178
4215
|
return Array.isArray(data.files) ? data.files : [];
|
|
4179
4216
|
}
|
|
4180
|
-
/** Add a participant. Requires `invite` or `admin`. */
|
|
4181
|
-
async addParticipant(spaceId, participant) {
|
|
4182
|
-
const proof = await this.proveFreshChallenge(spaceId);
|
|
4183
|
-
const body = { ...proof, participant };
|
|
4184
|
-
const url = `${this.baseUrl}/api/spaces/${encodeURIComponent(spaceId)}/participants`;
|
|
4185
|
-
const response = await this.fetchFn(url, this.jsonRequest("POST", body));
|
|
4186
|
-
await parseOrThrow(response, "addParticipant");
|
|
4187
|
-
}
|
|
4188
|
-
/** Remove a participant by their public-key identity. Requires `admin`. */
|
|
4189
|
-
async removeParticipant(spaceId, participantCommitment) {
|
|
4190
|
-
const proof = await this.proveFreshChallenge(spaceId);
|
|
4191
|
-
const body = { ...proof };
|
|
4192
|
-
const url = `${this.baseUrl}/api/spaces/${encodeURIComponent(spaceId)}/participants/${encodeURIComponent(participantCommitment)}`;
|
|
4193
|
-
const response = await this.fetchFn(url, this.jsonRequest("DELETE", body));
|
|
4194
|
-
const data = await parseOrThrow(response, "removeParticipant");
|
|
4195
|
-
return data.existed;
|
|
4196
|
-
}
|
|
4197
|
-
/** Set / replace an ACL entry. Requires `admin`. */
|
|
4198
|
-
async setACLEntry(spaceId, entry) {
|
|
4199
|
-
const proof = await this.proveFreshChallenge(spaceId);
|
|
4200
|
-
const body = { ...proof, entry };
|
|
4201
|
-
const url = `${this.baseUrl}/api/spaces/${encodeURIComponent(spaceId)}/acl`;
|
|
4202
|
-
const response = await this.fetchFn(url, this.jsonRequest("POST", body));
|
|
4203
|
-
await parseOrThrow(response, "setACLEntry");
|
|
4204
|
-
}
|
|
4205
4217
|
// -------------------------------------------------------------------------
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
/** Request a challenge for `spaceId` and generate the matching proof. */
|
|
4209
|
-
async proveFreshChallenge(spaceId) {
|
|
4210
|
-
const challenge = await this.requestChallenge(spaceId);
|
|
4211
|
-
const nonceField = nonceHexToFieldString(challenge.nonce);
|
|
4212
|
-
const input = {
|
|
4213
|
-
commitment: this.commitment,
|
|
4214
|
-
nonce: nonceField,
|
|
4215
|
-
ecdsaPubHash: this.ecdsaPubHash,
|
|
4216
|
-
secret: this.secret,
|
|
4217
|
-
salt: this.salt,
|
|
4218
|
-
ecdsaPub: this.ecdsaPub,
|
|
4219
|
-
};
|
|
4220
|
-
const snarkjs = await loadSnarkjs$1();
|
|
4221
|
-
const { proof, publicSignals } = await snarkjs.groth16.fullProve(input, this.wasmUrl, this.zkeyUrl);
|
|
4222
|
-
return {
|
|
4223
|
-
challengeId: challenge.challengeId,
|
|
4224
|
-
proof: proof,
|
|
4225
|
-
publicSignals: publicSignals,
|
|
4226
|
-
};
|
|
4227
|
-
}
|
|
4228
|
-
/**
|
|
4229
|
-
* Hit `/challenge`. The body includes the caller's commitment so the
|
|
4230
|
-
* server can scope the issued nonce to this principal.
|
|
4231
|
-
*/
|
|
4232
|
-
async requestChallenge(spaceId) {
|
|
4233
|
-
const url = `${this.baseUrl}/api/spaces/${encodeURIComponent(spaceId)}/challenge`;
|
|
4234
|
-
const response = await this.fetchFn(url, this.jsonRequest("POST", { commitment: this.commitment }));
|
|
4235
|
-
return await parseOrThrow(response, "challenge");
|
|
4218
|
+
filesBase(spaceId) {
|
|
4219
|
+
return `${this.baseUrl}/api/spaces/${encodeURIComponent(spaceId)}/files`;
|
|
4236
4220
|
}
|
|
4237
4221
|
jsonRequest(method, body) {
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
headers
|
|
4241
|
-
body
|
|
4242
|
-
}
|
|
4222
|
+
const init = { method };
|
|
4223
|
+
if (body !== undefined) {
|
|
4224
|
+
init.headers = { "Content-Type": "application/json" };
|
|
4225
|
+
init.body = JSON.stringify(body);
|
|
4226
|
+
}
|
|
4227
|
+
return init;
|
|
4243
4228
|
}
|
|
4244
4229
|
}
|
|
4245
|
-
function
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4230
|
+
async function parseOrThrow(response, op) {
|
|
4231
|
+
let parsed = null;
|
|
4232
|
+
try {
|
|
4233
|
+
parsed = await response.json();
|
|
4234
|
+
}
|
|
4235
|
+
catch {
|
|
4236
|
+
// non-JSON body
|
|
4237
|
+
}
|
|
4250
4238
|
if (!response.ok) {
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
}
|
|
4256
|
-
catch {
|
|
4257
|
-
// ignore — we still throw with the status
|
|
4258
|
-
}
|
|
4259
|
-
throw new Error(`SharedSpaceClient ${label} failed (${response.status})${detail}`);
|
|
4239
|
+
const msg = parsed && typeof parsed === "object" && "error" in parsed
|
|
4240
|
+
? String(parsed.error)
|
|
4241
|
+
: `${response.status} ${response.statusText}`;
|
|
4242
|
+
throw new Error(`SharedSpaceClient.${op}: ${msg}`);
|
|
4260
4243
|
}
|
|
4261
|
-
return
|
|
4244
|
+
return parsed;
|
|
4262
4245
|
}
|
|
4263
4246
|
|
|
4264
4247
|
/**
|
|
@@ -4692,6 +4675,7 @@ class HttpClient {
|
|
|
4692
4675
|
baseUrl;
|
|
4693
4676
|
apiKey;
|
|
4694
4677
|
getSessionToken;
|
|
4678
|
+
onUnauthorized;
|
|
4695
4679
|
fetchFn;
|
|
4696
4680
|
constructor(opts) {
|
|
4697
4681
|
if (!opts?.baseUrl)
|
|
@@ -4699,6 +4683,7 @@ class HttpClient {
|
|
|
4699
4683
|
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
4700
4684
|
this.apiKey = opts.apiKey ?? null;
|
|
4701
4685
|
this.getSessionToken = opts.getSessionToken ?? (() => null);
|
|
4686
|
+
this.onUnauthorized = opts.onUnauthorized ?? null;
|
|
4702
4687
|
const f = opts.fetch ?? globalThis.fetch;
|
|
4703
4688
|
if (typeof f !== "function") {
|
|
4704
4689
|
throw new Error("HttpClient: `globalThis.fetch` is unavailable; pass an explicit `fetch`.");
|
|
@@ -4728,10 +4713,13 @@ class HttpClient {
|
|
|
4728
4713
|
post(path, body) {
|
|
4729
4714
|
return this.request("POST", path, body);
|
|
4730
4715
|
}
|
|
4716
|
+
patch(path, body) {
|
|
4717
|
+
return this.request("PATCH", path, body);
|
|
4718
|
+
}
|
|
4731
4719
|
del(path, body) {
|
|
4732
4720
|
return this.request("DELETE", path, body);
|
|
4733
4721
|
}
|
|
4734
|
-
async request(method, path, body) {
|
|
4722
|
+
async request(method, path, body, allowRecovery = true) {
|
|
4735
4723
|
const url = path.startsWith("http") ? path : `${this.baseUrl}${path.startsWith("/") ? "" : "/"}${path}`;
|
|
4736
4724
|
const init = { method };
|
|
4737
4725
|
if (body !== undefined) {
|
|
@@ -4739,6 +4727,15 @@ class HttpClient {
|
|
|
4739
4727
|
init.body = JSON.stringify(body);
|
|
4740
4728
|
}
|
|
4741
4729
|
const res = await this.fetch(url, init);
|
|
4730
|
+
// Stale session: try to silently re-auth and replay the request once.
|
|
4731
|
+
// `allowRecovery` is cleared on the retry so a still-401 response (e.g.
|
|
4732
|
+
// recovery minted a token the endpoint still rejects) can't loop.
|
|
4733
|
+
if (res.status === 401 && allowRecovery && this.onUnauthorized && this.getSessionToken()) {
|
|
4734
|
+
const recovered = await this.onUnauthorized();
|
|
4735
|
+
if (recovered) {
|
|
4736
|
+
return this.request(method, path, body, false);
|
|
4737
|
+
}
|
|
4738
|
+
}
|
|
4742
4739
|
let parsed = null;
|
|
4743
4740
|
try {
|
|
4744
4741
|
parsed = await res.json();
|
|
@@ -4968,6 +4965,44 @@ class ZkAuth {
|
|
|
4968
4965
|
*/
|
|
4969
4966
|
async login(username, password, opts = {}) {
|
|
4970
4967
|
const identity = await deriveIdentity(username, password);
|
|
4968
|
+
return this.proveAndStore(username, identity, opts);
|
|
4969
|
+
}
|
|
4970
|
+
/**
|
|
4971
|
+
* Silently re-authenticate the current user without prompting for a
|
|
4972
|
+
* password. Only possible while the client is **unlocked** — i.e. the
|
|
4973
|
+
* derived {@link ZkIdentity} (secret + keypairs) is still in memory from a
|
|
4974
|
+
* prior `login()`/`unlock()`. Re-runs the full challenge→proof→authenticate
|
|
4975
|
+
* dance with that in-memory identity and swaps in the fresh token.
|
|
4976
|
+
*
|
|
4977
|
+
* Returns `true` if a new session was minted, `false` if recovery isn't
|
|
4978
|
+
* possible (no active user, or locked — the identity was never derived or
|
|
4979
|
+
* was lost on reload). A `false` result is the app's cue to send the user
|
|
4980
|
+
* back to the login screen.
|
|
4981
|
+
*
|
|
4982
|
+
* Used automatically by the {@link Client} when a token-gated request comes
|
|
4983
|
+
* back `401` (stale/expired token), so transient session expiry self-heals
|
|
4984
|
+
* instead of surfacing errors mid-session.
|
|
4985
|
+
*/
|
|
4986
|
+
async recover() {
|
|
4987
|
+
const username = this.deps.session.username;
|
|
4988
|
+
const identity = this.deps.session.identity;
|
|
4989
|
+
if (!username || !identity)
|
|
4990
|
+
return false;
|
|
4991
|
+
try {
|
|
4992
|
+
await this.proveAndStore(username, identity, {});
|
|
4993
|
+
return true;
|
|
4994
|
+
}
|
|
4995
|
+
catch {
|
|
4996
|
+
return false;
|
|
4997
|
+
}
|
|
4998
|
+
}
|
|
4999
|
+
/**
|
|
5000
|
+
* Shared sign-in core: prove knowledge of `identity` against a fresh
|
|
5001
|
+
* challenge and persist the resulting session + identity. Used by both
|
|
5002
|
+
* `login()` (identity derived from a password) and `recover()` (identity
|
|
5003
|
+
* already in memory).
|
|
5004
|
+
*/
|
|
5005
|
+
async proveAndStore(username, identity, opts) {
|
|
4971
5006
|
const ecdsaPubHex = await exportPublicKeyHex(identity.ecdsaKeyPair.publicKey);
|
|
4972
5007
|
const challenge = await this.deps.auth.getChallenge(username);
|
|
4973
5008
|
const { proof, publicSignals, commitment } = await generateAuthProof({
|
|
@@ -5125,14 +5160,14 @@ class StorageCipher {
|
|
|
5125
5160
|
}
|
|
5126
5161
|
|
|
5127
5162
|
/**
|
|
5128
|
-
* `client.
|
|
5129
|
-
*
|
|
5163
|
+
* `client.kv` — per-user key/value storage backed by the accelerator's personal
|
|
5164
|
+
* space.
|
|
5130
5165
|
*
|
|
5131
|
-
* await client.
|
|
5132
|
-
* const todo = await client.
|
|
5133
|
-
* const ids = await client.
|
|
5134
|
-
* await client.
|
|
5135
|
-
* const off = client.
|
|
5166
|
+
* await client.kv.set('todos', id, { title, completed })
|
|
5167
|
+
* const todo = await client.kv.get('todos', id)
|
|
5168
|
+
* const ids = await client.kv.list('todos')
|
|
5169
|
+
* await client.kv.delete('todos', id)
|
|
5170
|
+
* const off = client.kv.on('change', e => …) // realtime, cross-device
|
|
5136
5171
|
*
|
|
5137
5172
|
* A *collection* is a key namespace within the user's space; the stored key is
|
|
5138
5173
|
* `${collection}/${id}`. Operations are authorized by the user's session token
|
|
@@ -5140,12 +5175,14 @@ class StorageCipher {
|
|
|
5140
5175
|
*
|
|
5141
5176
|
* Values are encrypted at rest by default ({@link StorageCipher}, an AES-GCM
|
|
5142
5177
|
* key derived from the ZK identity); pass `{ encrypt: false }` to store
|
|
5143
|
-
* plaintext. The realtime feed rides the personal space's
|
|
5144
|
-
*
|
|
5178
|
+
* plaintext. The realtime feed rides the personal space's websocket over a
|
|
5179
|
+
* {@link WSTransport} (heartbeat keep-alive + auto-reconnect, event-namespaced
|
|
5180
|
+
* so it doesn't cross wires with chat/space transports).
|
|
5145
5181
|
*
|
|
5146
|
-
*
|
|
5182
|
+
* For **files** (blobs), use `client.storage` instead — `client.kv` is for
|
|
5183
|
+
* small structured values. `query()` is intentionally deferred.
|
|
5147
5184
|
*/
|
|
5148
|
-
class
|
|
5185
|
+
class KvNamespace {
|
|
5149
5186
|
deps;
|
|
5150
5187
|
cipher = null;
|
|
5151
5188
|
cipherIdentity = null;
|
|
@@ -5178,7 +5215,7 @@ class StorageNamespace {
|
|
|
5178
5215
|
}
|
|
5179
5216
|
/** Reserved — server-side query is deferred (see the overhaul plan). */
|
|
5180
5217
|
async query(_collection, _filter) {
|
|
5181
|
-
throw new Error("client.
|
|
5218
|
+
throw new Error("client.kv.query is not implemented yet (deferred in the SDK overhaul).");
|
|
5182
5219
|
}
|
|
5183
5220
|
/**
|
|
5184
5221
|
* Subscribe to realtime changes to this user's data across devices. Opens
|
|
@@ -5189,24 +5226,30 @@ class StorageNamespace {
|
|
|
5189
5226
|
*/
|
|
5190
5227
|
on(event, handler) {
|
|
5191
5228
|
if (event !== "change")
|
|
5192
|
-
throw new Error(`client.
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
throw new Error("client.storage.on: no `WebSocket` global available in this runtime.");
|
|
5229
|
+
throw new Error(`client.kv: unknown event "${event}"`);
|
|
5230
|
+
if (typeof globalThis.WebSocket !== "function") {
|
|
5231
|
+
throw new Error("client.kv.on: no `WebSocket` global available in this runtime.");
|
|
5196
5232
|
}
|
|
5197
5233
|
const token = this.deps.session.token;
|
|
5198
5234
|
if (!token)
|
|
5199
|
-
throw new Error("client.
|
|
5200
|
-
const
|
|
5201
|
-
const
|
|
5202
|
-
socket
|
|
5203
|
-
|
|
5204
|
-
|
|
5235
|
+
throw new Error("client.kv.on: not signed in.");
|
|
5236
|
+
const commitment = this.commitment();
|
|
5237
|
+
const url = `${this.deps.wsBaseUrl}/api/personal/${encodeURIComponent(commitment)}/websocket?session=${encodeURIComponent(token)}`;
|
|
5238
|
+
// Ride a WSTransport rather than a bare socket: it keeps the (otherwise
|
|
5239
|
+
// idle) personal-space feed warm with heartbeat pings, auto-reconnects
|
|
5240
|
+
// if it drops, and namespaces its events by `id` so this feed never
|
|
5241
|
+
// crosses wires with chat/space transports on the shared EventCore bus.
|
|
5242
|
+
const transport = new WSTransport({ url, id: `personal:${commitment}` });
|
|
5243
|
+
const onMessage = (e) => {
|
|
5244
|
+
void this.dispatchChange(e.detail, handler);
|
|
5245
|
+
};
|
|
5246
|
+
transport.on(EventCoreEvents.MESSAGE, onMessage);
|
|
5247
|
+
// Fire-and-forget: the realtime feed is best-effort, and the transport
|
|
5248
|
+
// queues nothing it needs before CONNECTED (server→client only).
|
|
5249
|
+
void transport.connect();
|
|
5205
5250
|
return () => {
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
}
|
|
5209
|
-
catch { /* already closed */ }
|
|
5251
|
+
transport.off(EventCoreEvents.MESSAGE, onMessage);
|
|
5252
|
+
transport.disconnect();
|
|
5210
5253
|
};
|
|
5211
5254
|
}
|
|
5212
5255
|
// -------------------------------------------------------------------------
|
|
@@ -5241,7 +5284,7 @@ class StorageNamespace {
|
|
|
5241
5284
|
commitment() {
|
|
5242
5285
|
const c = this.deps.session.commitment;
|
|
5243
5286
|
if (!c)
|
|
5244
|
-
throw new Error("client.
|
|
5287
|
+
throw new Error("client.kv: not signed in — call client.auth.zk.login() first.");
|
|
5245
5288
|
return c;
|
|
5246
5289
|
}
|
|
5247
5290
|
spacePath(suffix) {
|
|
@@ -5262,6 +5305,146 @@ class StorageNamespace {
|
|
|
5262
5305
|
}
|
|
5263
5306
|
}
|
|
5264
5307
|
|
|
5308
|
+
/**
|
|
5309
|
+
* `client.storage` — file storage. Hides the `FileStorage` / `ShardClient` /
|
|
5310
|
+
* `SharedSpaceClient` primitives behind the unified client.
|
|
5311
|
+
*
|
|
5312
|
+
* const { stat, manifest } = await client.storage.writeFile({ spaceId, data, metadata })
|
|
5313
|
+
* const { data } = await client.storage.readFile(spaceId, fileId)
|
|
5314
|
+
* const files = await client.storage.listFiles() // my shared files
|
|
5315
|
+
* const files = await client.storage.listFiles({ spaceId }) // a space's files
|
|
5316
|
+
* await client.storage.deleteFile(spaceId, fileId)
|
|
5317
|
+
*
|
|
5318
|
+
* Model:
|
|
5319
|
+
* - **Files are written to a SharedSpace** (never the personal space). A file
|
|
5320
|
+
* is its manifest (per-chunk keys + global shard hashes); the manifest lives
|
|
5321
|
+
* in the space's gated, metered `/files` store, the shard ciphertext in the
|
|
5322
|
+
* global open `/api/shards` store.
|
|
5323
|
+
* - **The manifest is the capability** — anyone holding it reads the file from
|
|
5324
|
+
* the global shards, anywhere ({@link readByManifest}). Share a file by
|
|
5325
|
+
* sharing its manifest.
|
|
5326
|
+
* - **Personal discovery mirror** — on a successful write the SDK also records
|
|
5327
|
+
* the manifest in the user's `client.kv` under a reserved collection, so
|
|
5328
|
+
* {@link listFiles} (no `spaceId`) can show "files I've shared" without
|
|
5329
|
+
* enumerating spaces. The mirror is unmetered and is NOT a write path.
|
|
5330
|
+
*
|
|
5331
|
+
* For small structured values use `client.kv` instead.
|
|
5332
|
+
*/
|
|
5333
|
+
/** Reserved `client.kv` collection holding the personal manifest mirror. */
|
|
5334
|
+
const MIRROR_COLLECTION = "__files__";
|
|
5335
|
+
class StorageNamespace {
|
|
5336
|
+
deps;
|
|
5337
|
+
fileStorage = null;
|
|
5338
|
+
constructor(deps) {
|
|
5339
|
+
this.deps = deps;
|
|
5340
|
+
}
|
|
5341
|
+
/**
|
|
5342
|
+
* Write a file to a SharedSpace: shard ciphertext → the global store, the
|
|
5343
|
+
* manifest → the space's gated store, and a mirror copy → the caller's
|
|
5344
|
+
* personal index. Returns the `stat` and the `manifest` (share the manifest
|
|
5345
|
+
* to let others read the file).
|
|
5346
|
+
*/
|
|
5347
|
+
async writeFile(input) {
|
|
5348
|
+
if (!input?.spaceId) {
|
|
5349
|
+
throw new Error("client.storage.writeFile: `spaceId` is required (files live in a SharedSpace).");
|
|
5350
|
+
}
|
|
5351
|
+
const fs = this.fs();
|
|
5352
|
+
const { manifest, stat } = await fs.writeFileToShards({
|
|
5353
|
+
data: input.data,
|
|
5354
|
+
metadata: input.metadata,
|
|
5355
|
+
});
|
|
5356
|
+
// Authoritative, metered write: the manifest into the space's gated store.
|
|
5357
|
+
await this.space().writeFileManifest(input.spaceId, manifest);
|
|
5358
|
+
// Best-effort personal discovery mirror (unmetered). A failure here
|
|
5359
|
+
// leaves the file intact in the space — log and continue.
|
|
5360
|
+
try {
|
|
5361
|
+
await this.deps.kv.set(MIRROR_COLLECTION, manifest.id, {
|
|
5362
|
+
spaceId: input.spaceId,
|
|
5363
|
+
manifest,
|
|
5364
|
+
});
|
|
5365
|
+
}
|
|
5366
|
+
catch (err) {
|
|
5367
|
+
console.warn("client.storage.writeFile: discovery mirror failed (file still written):", err);
|
|
5368
|
+
}
|
|
5369
|
+
return { stat, manifest };
|
|
5370
|
+
}
|
|
5371
|
+
/** Read a file from its space (resolves the manifest, then the shards). */
|
|
5372
|
+
async readFile(spaceId, fileId) {
|
|
5373
|
+
return this.fs().readFile(spaceId, fileId);
|
|
5374
|
+
}
|
|
5375
|
+
/**
|
|
5376
|
+
* Read a file straight from a manifest — the capability path. No space
|
|
5377
|
+
* membership needed; this is how a shared file is opened.
|
|
5378
|
+
*/
|
|
5379
|
+
async readByManifest(manifest) {
|
|
5380
|
+
return this.fs().readFileFromShards(manifest);
|
|
5381
|
+
}
|
|
5382
|
+
/**
|
|
5383
|
+
* List files. With no argument, returns the caller's own shared files from
|
|
5384
|
+
* the personal mirror; with `{ spaceId }`, lists that space's files.
|
|
5385
|
+
*/
|
|
5386
|
+
async listFiles(opts) {
|
|
5387
|
+
if (opts?.spaceId)
|
|
5388
|
+
return this.space().listFiles(opts.spaceId);
|
|
5389
|
+
const ids = await this.deps.kv.list(MIRROR_COLLECTION);
|
|
5390
|
+
const entries = await Promise.all(ids.map((id) => this.deps.kv.get(MIRROR_COLLECTION, id)));
|
|
5391
|
+
return entries
|
|
5392
|
+
.filter((e) => Boolean(e?.manifest))
|
|
5393
|
+
.map((e) => manifestToStat(e.manifest));
|
|
5394
|
+
}
|
|
5395
|
+
/** Delete a file from its space and remove the personal mirror entry. */
|
|
5396
|
+
async deleteFile(spaceId, fileId) {
|
|
5397
|
+
const existed = await this.fs().deleteFile(spaceId, fileId);
|
|
5398
|
+
try {
|
|
5399
|
+
await this.deps.kv.delete(MIRROR_COLLECTION, fileId);
|
|
5400
|
+
}
|
|
5401
|
+
catch {
|
|
5402
|
+
// mirror cleanup is best-effort
|
|
5403
|
+
}
|
|
5404
|
+
return existed;
|
|
5405
|
+
}
|
|
5406
|
+
/** Read a manifest verbatim (e.g. to share a file). */
|
|
5407
|
+
async getManifest(spaceId, fileId) {
|
|
5408
|
+
return this.fs().readManifest(spaceId, fileId);
|
|
5409
|
+
}
|
|
5410
|
+
// -------------------------------------------------------------------------
|
|
5411
|
+
// Internals — lazily built so an unauthenticated client is cheap to create.
|
|
5412
|
+
// -------------------------------------------------------------------------
|
|
5413
|
+
fs() {
|
|
5414
|
+
if (!this.fileStorage) {
|
|
5415
|
+
this.fileStorage = new FileStorage({
|
|
5416
|
+
shards: this.shards(),
|
|
5417
|
+
space: this.space(),
|
|
5418
|
+
rsWasmModule: this.deps.rsWasmModule,
|
|
5419
|
+
});
|
|
5420
|
+
}
|
|
5421
|
+
return this.fileStorage;
|
|
5422
|
+
}
|
|
5423
|
+
_shards = null;
|
|
5424
|
+
shards() {
|
|
5425
|
+
// Global, content-addressed shard store. The credential-stamping fetch
|
|
5426
|
+
// attaches the app key so PUTs are attributed + quota-gated; GETs are open.
|
|
5427
|
+
if (!this._shards) {
|
|
5428
|
+
this._shards = new ShardClient({
|
|
5429
|
+
baseUrl: this.deps.baseUrl,
|
|
5430
|
+
pathPrefix: "/api/shards",
|
|
5431
|
+
fetch: this.deps.http.fetch,
|
|
5432
|
+
});
|
|
5433
|
+
}
|
|
5434
|
+
return this._shards;
|
|
5435
|
+
}
|
|
5436
|
+
_space = null;
|
|
5437
|
+
space() {
|
|
5438
|
+
if (!this._space) {
|
|
5439
|
+
this._space = new SharedSpaceClient({
|
|
5440
|
+
baseUrl: this.deps.baseUrl,
|
|
5441
|
+
fetch: this.deps.http.fetch,
|
|
5442
|
+
});
|
|
5443
|
+
}
|
|
5444
|
+
return this._space;
|
|
5445
|
+
}
|
|
5446
|
+
}
|
|
5447
|
+
|
|
5265
5448
|
/**
|
|
5266
5449
|
* SpaceCipher — pure crypto for the fan-out group-encryption layer.
|
|
5267
5450
|
*
|
|
@@ -5501,12 +5684,27 @@ class Space {
|
|
|
5501
5684
|
/** Open the space websocket (resolves the ticket + identity lazily). */
|
|
5502
5685
|
async connect() {
|
|
5503
5686
|
if (!this.channel) {
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
|
|
5687
|
+
// The WS upgrade ticket is single-use + short-TTL, so a reconnect
|
|
5688
|
+
// must mint a FRESH one — otherwise the socket drops (idle timeout,
|
|
5689
|
+
// network blip) and never comes back. `buildUrl` is handed to the
|
|
5690
|
+
// transport as a `urlProvider` it calls before each reconnect.
|
|
5691
|
+
const buildUrl = async () => {
|
|
5692
|
+
const ticket = await this.deps.fetchTicket();
|
|
5693
|
+
return (`${this.deps.wsBaseUrl}/api/spaces/${encodeURIComponent(this.name)}/websocket` +
|
|
5694
|
+
(ticket ? `?ticket=${encodeURIComponent(ticket)}` : ""));
|
|
5695
|
+
};
|
|
5696
|
+
const url = await buildUrl();
|
|
5507
5697
|
this.channel = this.deps.createChannel
|
|
5508
5698
|
? this.deps.createChannel(url, this.deps.myId())
|
|
5509
|
-
: new BroadcastChannel({
|
|
5699
|
+
: new BroadcastChannel({
|
|
5700
|
+
url,
|
|
5701
|
+
myId: this.deps.myId(),
|
|
5702
|
+
autoAnnounce: false,
|
|
5703
|
+
urlProvider: buildUrl,
|
|
5704
|
+
// Keep recovering across network flaps (each attempt mints a
|
|
5705
|
+
// fresh ticket); teardown calls disconnect() which stops it.
|
|
5706
|
+
maxReconnectAttempts: 0,
|
|
5707
|
+
});
|
|
5510
5708
|
for (const [event, handler] of this.pending)
|
|
5511
5709
|
this.channel.on(event, handler);
|
|
5512
5710
|
this.pending.length = 0;
|
|
@@ -5570,23 +5768,7 @@ class Space {
|
|
|
5570
5768
|
* persisted `spaceMessage`. Requires a keyring.
|
|
5571
5769
|
*/
|
|
5572
5770
|
async sendMessage(payload, opts = {}) {
|
|
5573
|
-
|
|
5574
|
-
const message = new Message(payload);
|
|
5575
|
-
const headers = await cipher.seal(message.serialize());
|
|
5576
|
-
const source = this.deps.myId();
|
|
5577
|
-
const subject = opts.channel ?? "default";
|
|
5578
|
-
// Sign the message with our identity ECDSA key so receivers can verify
|
|
5579
|
-
// authorship end-to-end (independent of the relay's `source` stamp).
|
|
5580
|
-
const authPriv = KeyStore.getInstance().getAuthKeyPair(source)?.privateKey;
|
|
5581
|
-
if (authPriv) {
|
|
5582
|
-
const canonical = canonicalMessage({
|
|
5583
|
-
source, target: this.name, subject,
|
|
5584
|
-
epoch: Number(headers.epoch), iv: String(headers.iv), ciphertext: String(headers.ciphertext),
|
|
5585
|
-
});
|
|
5586
|
-
headers.sig = await signSpaceMessage(canonical, authPriv);
|
|
5587
|
-
}
|
|
5588
|
-
const packet = new Packet({ subject, source, target: this.name, headers });
|
|
5589
|
-
this.sendRaw({ spaceMessage: packet.serialize() });
|
|
5771
|
+
this.sendRaw({ spaceMessage: await this.sealMessage(payload, opts.channel, opts.contentType) });
|
|
5590
5772
|
}
|
|
5591
5773
|
/** Subscribe to decrypted fan-out messages. Returns an unsubscribe fn. */
|
|
5592
5774
|
onMessage(handler) {
|
|
@@ -5594,6 +5776,56 @@ class Space {
|
|
|
5594
5776
|
this.events.addEventListener("message", listener);
|
|
5595
5777
|
return () => this.events.removeEventListener("message", listener);
|
|
5596
5778
|
}
|
|
5779
|
+
/**
|
|
5780
|
+
* Edit a persisted message IN PLACE, addressed by its server
|
|
5781
|
+
* {@link SpaceMessageEvent.handle}. Re-seals `payload` and asks the server
|
|
5782
|
+
* to replace the stored entry; the server authorizes against the original
|
|
5783
|
+
* author. Receivers get an {@link onMessageEdited} event carrying the new
|
|
5784
|
+
* content at the same handle. This is a generic persisted-item edit — the
|
|
5785
|
+
* server has no notion of what the payload means.
|
|
5786
|
+
*/
|
|
5787
|
+
async editMessage(handle, payload, opts = {}) {
|
|
5788
|
+
const spaceMessage = await this.sealMessage(payload, opts.channel, opts.contentType);
|
|
5789
|
+
this.sendRaw({ editSpaceMessage: { ts: handle, spaceMessage } });
|
|
5790
|
+
}
|
|
5791
|
+
/**
|
|
5792
|
+
* Hard-delete a persisted message by its server {@link SpaceMessageEvent.handle}
|
|
5793
|
+
* — the ciphertext is removed from storage. Server authorizes against the
|
|
5794
|
+
* original author. Receivers get an {@link onMessageDeleted} event.
|
|
5795
|
+
*/
|
|
5796
|
+
async deleteMessage(handle) {
|
|
5797
|
+
this.sendRaw({ deleteSpaceMessage: { ts: handle } });
|
|
5798
|
+
}
|
|
5799
|
+
/** Subscribe to in-place edits of persisted messages (new content at the same handle). */
|
|
5800
|
+
onMessageEdited(handler) {
|
|
5801
|
+
const listener = (e) => handler(e.detail);
|
|
5802
|
+
this.events.addEventListener("message-edited", listener);
|
|
5803
|
+
return () => this.events.removeEventListener("message-edited", listener);
|
|
5804
|
+
}
|
|
5805
|
+
/** Subscribe to deletions of persisted messages. */
|
|
5806
|
+
onMessageDeleted(handler) {
|
|
5807
|
+
const listener = (e) => handler(e.detail);
|
|
5808
|
+
this.events.addEventListener("message-deleted", listener);
|
|
5809
|
+
return () => this.events.removeEventListener("message-deleted", listener);
|
|
5810
|
+
}
|
|
5811
|
+
/**
|
|
5812
|
+
* Broadcast an ephemeral, never-persisted signal to the room over the
|
|
5813
|
+
* generic `pub` relay. `subject` is an app-defined routing key; `data` is
|
|
5814
|
+
* opaque. The transport stays domain-agnostic — app concepts like typing
|
|
5815
|
+
* indicators, presence, or live cursors are built on top of this. No-op if
|
|
5816
|
+
* the space isn't connected (ephemeral signals aren't worth queuing).
|
|
5817
|
+
*/
|
|
5818
|
+
sendEphemeral(subject, data) {
|
|
5819
|
+
if (!this.isConnected())
|
|
5820
|
+
return;
|
|
5821
|
+
this.sendRaw({ pub: { subject, data } });
|
|
5822
|
+
}
|
|
5823
|
+
/** Subscribe to inbound ephemeral signals. `from` is the authenticated sender. */
|
|
5824
|
+
onEphemeral(handler) {
|
|
5825
|
+
const listener = (e) => handler(e.detail);
|
|
5826
|
+
this.events.addEventListener("ephemeral", listener);
|
|
5827
|
+
return () => this.events.removeEventListener("ephemeral", listener);
|
|
5828
|
+
}
|
|
5597
5829
|
/** Subscribe to inbound join requests (for a key-holder to admit). */
|
|
5598
5830
|
onJoinRequest(handler) {
|
|
5599
5831
|
const listener = (e) => handler(e.detail);
|
|
@@ -5676,16 +5908,20 @@ class Space {
|
|
|
5676
5908
|
return this.requireKeyring().rotate(roster);
|
|
5677
5909
|
}
|
|
5678
5910
|
// -- room-scoped files (no websocket needed) -------------------------------
|
|
5679
|
-
async putFile(file, metadata) {
|
|
5680
|
-
return this.fileStorage().writeFileToShards({ data: file, metadata });
|
|
5911
|
+
async putFile(file, metadata, opts = {}) {
|
|
5912
|
+
return this.fileStorage().writeFileToShards({ data: file, metadata, onProgress: opts.onProgress });
|
|
5681
5913
|
}
|
|
5682
5914
|
async getFile(manifest) {
|
|
5683
5915
|
return this.fileStorage().readFileFromShards(manifest);
|
|
5684
5916
|
}
|
|
5685
5917
|
fileStorage() {
|
|
5918
|
+
// Global, content-addressed shard store — same store `client.storage`
|
|
5919
|
+
// uses, so a file shared into a channel resolves from anywhere by
|
|
5920
|
+
// manifest. (Shards are encrypted ciphertext; the manifest is the
|
|
5921
|
+
// capability.)
|
|
5686
5922
|
const shards = new ShardClient({
|
|
5687
5923
|
baseUrl: this.deps.httpBaseUrl,
|
|
5688
|
-
pathPrefix:
|
|
5924
|
+
pathPrefix: "/api/shards",
|
|
5689
5925
|
fetch: this.deps.fetch,
|
|
5690
5926
|
});
|
|
5691
5927
|
return new FileStorage({ shards });
|
|
@@ -5705,11 +5941,39 @@ class Space {
|
|
|
5705
5941
|
if (!f || typeof f !== "object")
|
|
5706
5942
|
return;
|
|
5707
5943
|
if (typeof f.spaceMessage === "string" && this.cipher) {
|
|
5708
|
-
const decoded = await this.decodeFrame(f.spaceMessage, this.cipher, /*raw*/ true);
|
|
5944
|
+
const decoded = await this.decodeFrame(f.spaceMessage, this.cipher, /*raw*/ true, Number(f.timestamp ?? 0));
|
|
5709
5945
|
if (decoded)
|
|
5710
5946
|
this.emitEvent("message", decoded);
|
|
5711
5947
|
return;
|
|
5712
5948
|
}
|
|
5949
|
+
if (f.editSpaceMessage && typeof f.editSpaceMessage === "object" && this.cipher) {
|
|
5950
|
+
// Server-authoritative in-place edit: new sealed content at the same
|
|
5951
|
+
// handle. Decode and surface it; the app replaces by `handle`.
|
|
5952
|
+
const e = f.editSpaceMessage;
|
|
5953
|
+
if (typeof e.spaceMessage === "string") {
|
|
5954
|
+
const decoded = await this.decodeFrame(e.spaceMessage, this.cipher, /*raw*/ true, Number(e.ts ?? 0));
|
|
5955
|
+
if (decoded)
|
|
5956
|
+
this.emitEvent("message-edited", decoded);
|
|
5957
|
+
}
|
|
5958
|
+
return;
|
|
5959
|
+
}
|
|
5960
|
+
if (f.deleteSpaceMessage && typeof f.deleteSpaceMessage === "object") {
|
|
5961
|
+
const d = f.deleteSpaceMessage;
|
|
5962
|
+
this.emitEvent("message-deleted", { handle: Number(d.ts ?? 0) });
|
|
5963
|
+
return;
|
|
5964
|
+
}
|
|
5965
|
+
if (f.pub && typeof f.pub === "object") {
|
|
5966
|
+
// Generic ephemeral relay frame. The server stamps the authenticated
|
|
5967
|
+
// sender as `name`; `pub` carries the app's {subject, data}. We
|
|
5968
|
+
// surface the authenticated `name` as `from` (not any client-set
|
|
5969
|
+
// value inside `pub`), so app signals can't spoof their sender.
|
|
5970
|
+
const p = f.pub;
|
|
5971
|
+
const from = typeof f.name === "string" ? f.name : "";
|
|
5972
|
+
if (from && typeof p.subject === "string") {
|
|
5973
|
+
this.emitEvent("ephemeral", { from, subject: p.subject, data: p.data });
|
|
5974
|
+
}
|
|
5975
|
+
return;
|
|
5976
|
+
}
|
|
5713
5977
|
if (f.joinRequest && typeof f.joinRequest === "object") {
|
|
5714
5978
|
const req = f.joinRequest;
|
|
5715
5979
|
this.emitEvent("join-request", req);
|
|
@@ -5726,19 +5990,46 @@ class Space {
|
|
|
5726
5990
|
void this.deps.keyring.pullKeys().catch(() => { });
|
|
5727
5991
|
}
|
|
5728
5992
|
}
|
|
5993
|
+
/**
|
|
5994
|
+
* Seal `payload` once with the current group key + sign it, returning the
|
|
5995
|
+
* serialized `Packet` string. Shared by send (a new message) and edit
|
|
5996
|
+
* (replacement content for an existing handle). Requires a keyring.
|
|
5997
|
+
*/
|
|
5998
|
+
async sealMessage(payload, channel, contentType) {
|
|
5999
|
+
const cipher = this.requireCipher(contentType);
|
|
6000
|
+
const message = new Message(payload);
|
|
6001
|
+
const headers = await cipher.seal(message.serialize());
|
|
6002
|
+
const source = this.deps.myId();
|
|
6003
|
+
const subject = channel ?? "default";
|
|
6004
|
+
// Sign with our identity ECDSA key so receivers can verify authorship
|
|
6005
|
+
// end-to-end (independent of the relay's `source` stamp).
|
|
6006
|
+
const authPriv = KeyStore.getInstance().getAuthKeyPair(source)?.privateKey;
|
|
6007
|
+
if (authPriv) {
|
|
6008
|
+
const canonical = canonicalMessage({
|
|
6009
|
+
source, target: this.name, subject,
|
|
6010
|
+
epoch: Number(headers.epoch), iv: String(headers.iv), ciphertext: String(headers.ciphertext),
|
|
6011
|
+
});
|
|
6012
|
+
headers.sig = await signSpaceMessage(canonical, authPriv);
|
|
6013
|
+
}
|
|
6014
|
+
return new Packet({ subject, source, target: this.name, headers }).serialize();
|
|
6015
|
+
}
|
|
5729
6016
|
/**
|
|
5730
6017
|
* Decode one persisted/broadcast frame into a SpaceMessageEvent.
|
|
5731
6018
|
* `isRawFrame` true means `input` is the already-extracted `spaceMessage`
|
|
5732
|
-
* packet string
|
|
6019
|
+
* packet string (and `serverTs` carries the message's storage handle from
|
|
6020
|
+
* the enclosing frame); false means `input` is the full
|
|
6021
|
+
* `{spaceMessage, name, timestamp}` frame and the handle is read from it.
|
|
5733
6022
|
*/
|
|
5734
|
-
async decodeFrame(input, cipher, isRawFrame = false) {
|
|
6023
|
+
async decodeFrame(input, cipher, isRawFrame = false, serverTs = 0) {
|
|
5735
6024
|
try {
|
|
5736
6025
|
let packetJson = input;
|
|
6026
|
+
let handle = serverTs;
|
|
5737
6027
|
if (!isRawFrame) {
|
|
5738
6028
|
const outer = JSON.parse(input);
|
|
5739
6029
|
if (typeof outer.spaceMessage !== "string")
|
|
5740
6030
|
return null;
|
|
5741
6031
|
packetJson = outer.spaceMessage;
|
|
6032
|
+
handle = Number(outer.timestamp ?? 0);
|
|
5742
6033
|
}
|
|
5743
6034
|
const packet = Packet.deserialize(packetJson);
|
|
5744
6035
|
if (!cipher.handles(packet.headers))
|
|
@@ -5759,6 +6050,7 @@ class Space {
|
|
|
5759
6050
|
channel: packet.subject,
|
|
5760
6051
|
epoch: Number(packet.headers?.epoch ?? 0),
|
|
5761
6052
|
contentType: packet.headers?.contentType,
|
|
6053
|
+
handle,
|
|
5762
6054
|
message,
|
|
5763
6055
|
};
|
|
5764
6056
|
}
|
|
@@ -5774,6 +6066,12 @@ class Space {
|
|
|
5774
6066
|
* nothing to verify against → reject.
|
|
5775
6067
|
*/
|
|
5776
6068
|
async verifySender(packet) {
|
|
6069
|
+
// Trust our OWN messages — we sent them, so there's nothing to prove
|
|
6070
|
+
// against the roster. This also keeps a returning member (who loaded a
|
|
6071
|
+
// cached key and never re-published a join-request) from dropping the
|
|
6072
|
+
// echo of their own messages when their `ecdsaPub` isn't in the roster.
|
|
6073
|
+
if (packet.source === this.deps.myId())
|
|
6074
|
+
return true;
|
|
5777
6075
|
const keyring = this.deps.keyring;
|
|
5778
6076
|
const sig = packet.headers?.sig;
|
|
5779
6077
|
if (!keyring || typeof sig !== "string")
|
|
@@ -6270,6 +6568,34 @@ class KeyringClient {
|
|
|
6270
6568
|
async invite(username) {
|
|
6271
6569
|
await this.post("/keyring/invite", { username });
|
|
6272
6570
|
}
|
|
6571
|
+
// -- Shareable invite links ----------------------------------------------
|
|
6572
|
+
// A capability token tied to this space. Redeeming it allowlists the holder
|
|
6573
|
+
// so the keeper admits them — the link IS the invitation. Mint/list/revoke
|
|
6574
|
+
// require the caller to be the creator or an existing member.
|
|
6575
|
+
async createInviteLink(opts = {}) {
|
|
6576
|
+
return this.post("/keyring/link", opts);
|
|
6577
|
+
}
|
|
6578
|
+
async listInviteLinks() {
|
|
6579
|
+
const body = await this.get("/keyring/links");
|
|
6580
|
+
return body.links ?? [];
|
|
6581
|
+
}
|
|
6582
|
+
/** Set a member's role ("viewer" | "editor"). Owner-only on the server. */
|
|
6583
|
+
async setMemberRole(username, role) {
|
|
6584
|
+
await this.post("/keyring/role", { username, role });
|
|
6585
|
+
}
|
|
6586
|
+
/** Roster with roles (owner/members only). */
|
|
6587
|
+
async members() {
|
|
6588
|
+
const body = await this.get("/keyring/members");
|
|
6589
|
+
return body.members ?? [];
|
|
6590
|
+
}
|
|
6591
|
+
async revokeInviteLink(token) {
|
|
6592
|
+
await this.post("/keyring/link-revoke", { token });
|
|
6593
|
+
}
|
|
6594
|
+
/** Redeem a link: the server allowlists this member and asks the keeper to
|
|
6595
|
+
* wrap the group key for their identity. Caller must be authenticated. */
|
|
6596
|
+
async redeemInvite(req) {
|
|
6597
|
+
await this.post("/keyring/redeem", req);
|
|
6598
|
+
}
|
|
6273
6599
|
async fetchMetadata() {
|
|
6274
6600
|
try {
|
|
6275
6601
|
return await this.get("/metadata");
|
|
@@ -6407,6 +6733,56 @@ class SpaceNamespace {
|
|
|
6407
6733
|
return this.joinSpace(spaceId, opts);
|
|
6408
6734
|
}
|
|
6409
6735
|
// -------------------------------------------------------------------------
|
|
6736
|
+
// Invite links — shareable, keeper-gated invitations to a space. The token
|
|
6737
|
+
// is a capability: anyone signed in who redeems it is admitted by the
|
|
6738
|
+
// keeper. Mint/list/revoke require the caller to be the space's creator or
|
|
6739
|
+
// an existing member.
|
|
6740
|
+
// -------------------------------------------------------------------------
|
|
6741
|
+
/** Mint a shareable invite link for a space. `opts.expiresInSec` / `maxUses`
|
|
6742
|
+
* bound it (0/undefined = unlimited); `opts.role` ("viewer"|"editor") is the
|
|
6743
|
+
* access granted to redeemers (default viewer). Returns the link. */
|
|
6744
|
+
async createInviteLink(spaceId, opts = {}) {
|
|
6745
|
+
return this.keyringFor(spaceId).createInviteLink(opts);
|
|
6746
|
+
}
|
|
6747
|
+
/** Set a member's role on a space ("viewer" | "editor"). Owner-only. */
|
|
6748
|
+
async setMemberRole(spaceId, username, role) {
|
|
6749
|
+
await this.keyringFor(spaceId).setMemberRole(username, role);
|
|
6750
|
+
}
|
|
6751
|
+
/** Roster with roles (owner/members only). */
|
|
6752
|
+
async members(spaceId) {
|
|
6753
|
+
return this.keyringFor(spaceId).members();
|
|
6754
|
+
}
|
|
6755
|
+
/** List a space's active invite links (creator/members only). */
|
|
6756
|
+
async listInviteLinks(spaceId) {
|
|
6757
|
+
return this.keyringFor(spaceId).listInviteLinks();
|
|
6758
|
+
}
|
|
6759
|
+
/** Revoke an invite link by token. */
|
|
6760
|
+
async revokeInviteLink(spaceId, token) {
|
|
6761
|
+
await this.keyringFor(spaceId).revokeInviteLink(token);
|
|
6762
|
+
}
|
|
6763
|
+
/**
|
|
6764
|
+
* Join a space using an invite link: redeem the token (the keeper admits
|
|
6765
|
+
* this member), then connect and resolve once the group key arrives.
|
|
6766
|
+
* Returns the open {@link Space}.
|
|
6767
|
+
*/
|
|
6768
|
+
async joinByInvite(spaceId, token, opts = {}) {
|
|
6769
|
+
const memberId = this.myId();
|
|
6770
|
+
const { ecdhPub, ecdsaPub } = await this.ensureIdentity(memberId);
|
|
6771
|
+
await this.keyringFor(spaceId).redeemInvite({ token, identityEcdhPub: ecdhPub, identityEcdsaPub: ecdsaPub });
|
|
6772
|
+
return this.joinSpace(spaceId, opts);
|
|
6773
|
+
}
|
|
6774
|
+
/** The current member roster of a space (member id → identity public keys). */
|
|
6775
|
+
async roster(spaceId) {
|
|
6776
|
+
return this.keyringFor(spaceId).fetchRoster();
|
|
6777
|
+
}
|
|
6778
|
+
keyringFor(spaceId) {
|
|
6779
|
+
return new KeyringClient({
|
|
6780
|
+
spaceId,
|
|
6781
|
+
httpBaseUrl: this.deps.http.baseUrl,
|
|
6782
|
+
fetch: this.deps.http.fetch,
|
|
6783
|
+
});
|
|
6784
|
+
}
|
|
6785
|
+
// -------------------------------------------------------------------------
|
|
6410
6786
|
// Internals
|
|
6411
6787
|
// -------------------------------------------------------------------------
|
|
6412
6788
|
async build(spaceId, historyPolicy) {
|
|
@@ -6473,6 +6849,133 @@ class SpaceNamespace {
|
|
|
6473
6849
|
}
|
|
6474
6850
|
}
|
|
6475
6851
|
|
|
6852
|
+
/**
|
|
6853
|
+
* `client.agents` — manage an app's **Programmable Agents** (server-side
|
|
6854
|
+
* "virtual users" backed by Workers AI).
|
|
6855
|
+
*
|
|
6856
|
+
* const { config } = await client.agents.create(appId, {
|
|
6857
|
+
* handle: '@assistant', displayName: 'Assistant',
|
|
6858
|
+
* systemPrompt: 'You are a helpful teammate in this Space.',
|
|
6859
|
+
* triggers: [{ type: 'mention' }],
|
|
6860
|
+
* });
|
|
6861
|
+
* await client.agents.enable(appId, config.agentId, spaceId); // per-Space opt-in
|
|
6862
|
+
*
|
|
6863
|
+
* These are MANAGEMENT calls — the agent itself runs on the accelerator. Only an
|
|
6864
|
+
* app's owner or an editor of its shared Space may create/update/enable an agent
|
|
6865
|
+
* (viewers can read). Authorization rides the user session token; non-owner
|
|
6866
|
+
* editors pass their management Space via `opts.space`.
|
|
6867
|
+
*
|
|
6868
|
+
* v1 is "prompts + persona": a system prompt, a model, named prompt `skills`,
|
|
6869
|
+
* and `triggers` that decide when the agent speaks. No tools, no code.
|
|
6870
|
+
*/
|
|
6871
|
+
class AgentsNamespace {
|
|
6872
|
+
deps;
|
|
6873
|
+
constructor(deps) {
|
|
6874
|
+
this.deps = deps;
|
|
6875
|
+
}
|
|
6876
|
+
/** List the app's agents. */
|
|
6877
|
+
async list(appId, opts = {}) {
|
|
6878
|
+
const res = await this.deps.http.get(this.path(appId, "", opts));
|
|
6879
|
+
return res.agents ?? [];
|
|
6880
|
+
}
|
|
6881
|
+
/** Read one agent (config + identity keys). */
|
|
6882
|
+
async get(appId, agentId, opts = {}) {
|
|
6883
|
+
return this.deps.http.get(this.path(appId, `/${encodeURIComponent(agentId)}`, opts));
|
|
6884
|
+
}
|
|
6885
|
+
/** Create an agent (owner/editor only). */
|
|
6886
|
+
async create(appId, input, opts = {}) {
|
|
6887
|
+
return this.deps.http.post(this.path(appId, "", {}), { ...input, space: opts.space });
|
|
6888
|
+
}
|
|
6889
|
+
/** Update an agent's editor-settable fields (owner/editor only). */
|
|
6890
|
+
async update(appId, agentId, patch, opts = {}) {
|
|
6891
|
+
return this.deps.http.patch(this.path(appId, `/${encodeURIComponent(agentId)}`, {}), { ...patch, space: opts.space });
|
|
6892
|
+
}
|
|
6893
|
+
/** Delete an agent (owner/editor only). */
|
|
6894
|
+
async delete(appId, agentId, opts = {}) {
|
|
6895
|
+
return this.deps.http.del(this.path(appId, `/${encodeURIComponent(agentId)}`, {}), { space: opts.space });
|
|
6896
|
+
}
|
|
6897
|
+
/** Enable the agent on a Space (per-Space opt-in). */
|
|
6898
|
+
async enable(appId, agentId, spaceId, opts = {}) {
|
|
6899
|
+
return this.deps.http.post(this.path(appId, `/${encodeURIComponent(agentId)}/enable`, {}), { targetSpaceId: spaceId, space: opts.space });
|
|
6900
|
+
}
|
|
6901
|
+
/** Disable the agent on a Space. */
|
|
6902
|
+
async disable(appId, agentId, spaceId, opts = {}) {
|
|
6903
|
+
return this.deps.http.post(this.path(appId, `/${encodeURIComponent(agentId)}/disable`, {}), { targetSpaceId: spaceId, space: opts.space });
|
|
6904
|
+
}
|
|
6905
|
+
path(appId, suffix, opts) {
|
|
6906
|
+
const q = opts.space ? `?space=${encodeURIComponent(opts.space)}` : "";
|
|
6907
|
+
return `/api/apps/${encodeURIComponent(appId)}/agents${suffix}${q}`;
|
|
6908
|
+
}
|
|
6909
|
+
}
|
|
6910
|
+
|
|
6911
|
+
/**
|
|
6912
|
+
* `client.functions` — manage an app's **serverless functions** (developer-
|
|
6913
|
+
* authored code that runs on the accelerator).
|
|
6914
|
+
*
|
|
6915
|
+
* const { config } = await client.functions.deploy(appId, {
|
|
6916
|
+
* name: 'hello', displayName: 'Hello',
|
|
6917
|
+
* code: `export default { fetch: () => new Response('hi') }`,
|
|
6918
|
+
* triggers: { http: { enabled: true } },
|
|
6919
|
+
* });
|
|
6920
|
+
* // HTTP-reachable at hello--<slug>.fns.muhkoo.dev
|
|
6921
|
+
* await client.functions.enable(appId, config.functionId, spaceId); // Space-bound
|
|
6922
|
+
*
|
|
6923
|
+
* These are MANAGEMENT calls — the function itself runs on the accelerator
|
|
6924
|
+
* (uploaded just-in-time, decrypted only at invocation). Only an app's owner or
|
|
6925
|
+
* an editor of its shared Space may deploy/update/enable (viewers read).
|
|
6926
|
+
* Authorization rides the user session token; non-owner editors pass their
|
|
6927
|
+
* management Space via `opts.space`.
|
|
6928
|
+
*
|
|
6929
|
+
* v1 is "compute-only": a function is an untrusted single-module ES worker
|
|
6930
|
+
* (`export default { fetch }`) with NO platform bindings — it reaches the
|
|
6931
|
+
* platform as an external client (the end-user session is passed through). Two
|
|
6932
|
+
* triggers: `http` (its own subdomain) and `space` (invoked on Space messages,
|
|
6933
|
+
* like a Programmable Agent).
|
|
6934
|
+
*/
|
|
6935
|
+
class FunctionsNamespace {
|
|
6936
|
+
deps;
|
|
6937
|
+
constructor(deps) {
|
|
6938
|
+
this.deps = deps;
|
|
6939
|
+
}
|
|
6940
|
+
/** List the app's functions. */
|
|
6941
|
+
async list(appId, opts = {}) {
|
|
6942
|
+
const res = await this.deps.http.get(this.path(appId, "", opts));
|
|
6943
|
+
return res.functions ?? [];
|
|
6944
|
+
}
|
|
6945
|
+
/** Read one function's config. */
|
|
6946
|
+
async get(appId, functionId, opts = {}) {
|
|
6947
|
+
return this.deps.http.get(this.path(appId, `/${encodeURIComponent(functionId)}`, opts));
|
|
6948
|
+
}
|
|
6949
|
+
/** Read a function's decrypted source (owner/editor; for the editor UI). */
|
|
6950
|
+
async code(appId, functionId, opts = {}) {
|
|
6951
|
+
return this.deps.http.get(this.path(appId, `/${encodeURIComponent(functionId)}/code`, opts));
|
|
6952
|
+
}
|
|
6953
|
+
/** Deploy (create) a function (owner/editor only). */
|
|
6954
|
+
async deploy(appId, input, opts = {}) {
|
|
6955
|
+
return this.deps.http.post(this.path(appId, "", {}), { ...input, space: opts.space });
|
|
6956
|
+
}
|
|
6957
|
+
/** Update a function's editor-settable fields and/or redeploy its `code`. */
|
|
6958
|
+
async update(appId, functionId, patch, opts = {}) {
|
|
6959
|
+
return this.deps.http.patch(this.path(appId, `/${encodeURIComponent(functionId)}`, {}), { ...patch, space: opts.space });
|
|
6960
|
+
}
|
|
6961
|
+
/** Delete a function (owner/editor only). */
|
|
6962
|
+
async delete(appId, functionId, opts = {}) {
|
|
6963
|
+
return this.deps.http.del(this.path(appId, `/${encodeURIComponent(functionId)}`, {}), { space: opts.space });
|
|
6964
|
+
}
|
|
6965
|
+
/** Enable the function on a Space (per-Space opt-in for the `space` trigger). */
|
|
6966
|
+
async enable(appId, functionId, spaceId, opts = {}) {
|
|
6967
|
+
return this.deps.http.post(this.path(appId, `/${encodeURIComponent(functionId)}/enable`, {}), { targetSpaceId: spaceId, space: opts.space });
|
|
6968
|
+
}
|
|
6969
|
+
/** Disable the function on a Space. */
|
|
6970
|
+
async disable(appId, functionId, spaceId, opts = {}) {
|
|
6971
|
+
return this.deps.http.post(this.path(appId, `/${encodeURIComponent(functionId)}/disable`, {}), { targetSpaceId: spaceId, space: opts.space });
|
|
6972
|
+
}
|
|
6973
|
+
path(appId, suffix, opts) {
|
|
6974
|
+
const q = opts.space ? `?space=${encodeURIComponent(opts.space)}` : "";
|
|
6975
|
+
return `/api/apps/${encodeURIComponent(appId)}/functions${suffix}${q}`;
|
|
6976
|
+
}
|
|
6977
|
+
}
|
|
6978
|
+
|
|
6476
6979
|
/**
|
|
6477
6980
|
* `Client` — the single entry point to the Muhkoo SDK.
|
|
6478
6981
|
*
|
|
@@ -6509,14 +7012,24 @@ class Client {
|
|
|
6509
7012
|
baseUrl;
|
|
6510
7013
|
/** Authentication — `client.auth.zk.login(...)`, etc. */
|
|
6511
7014
|
auth;
|
|
6512
|
-
/** Per-user
|
|
7015
|
+
/** Per-user key/value storage — `client.kv.set(...)`, etc. */
|
|
7016
|
+
kv;
|
|
7017
|
+
/** File storage — `client.storage.writeFile(...)`, etc. */
|
|
6513
7018
|
storage;
|
|
6514
7019
|
/** Realtime messaging — `client.message.subscribe(...)`, etc. */
|
|
6515
7020
|
message;
|
|
6516
7021
|
/** Fan-out group spaces — `client.space.createSpace(...)`, etc. */
|
|
6517
7022
|
space;
|
|
7023
|
+
/** Programmable Agents — `client.agents.create(appId, …)`, etc. */
|
|
7024
|
+
agents;
|
|
7025
|
+
/** Serverless functions — `client.functions.deploy(appId, …)`, etc. */
|
|
7026
|
+
functions;
|
|
6518
7027
|
session;
|
|
6519
7028
|
http;
|
|
7029
|
+
/** Subscribers notified when a session expires and can't be recovered. */
|
|
7030
|
+
sessionExpiredHandlers = new Set();
|
|
7031
|
+
/** Shared in-flight recovery so a burst of 401s triggers one re-auth. */
|
|
7032
|
+
recoverInFlight = null;
|
|
6520
7033
|
constructor(options = {}) {
|
|
6521
7034
|
if (options.logLevel && typeof globalThis.appLogger?.setLevel === "function") {
|
|
6522
7035
|
globalThis.appLogger.setLevel(options.logLevel);
|
|
@@ -6528,19 +7041,24 @@ class Client {
|
|
|
6528
7041
|
apiKey: options.apiKey,
|
|
6529
7042
|
getSessionToken: () => this.session.token,
|
|
6530
7043
|
fetch: options.fetch,
|
|
7044
|
+
// Self-heal stale sessions: on a 401, try a silent re-auth (only
|
|
7045
|
+
// works while unlocked). If it can't, `recoverSession` fires the
|
|
7046
|
+
// session-expired event so the app can redirect to login.
|
|
7047
|
+
onUnauthorized: () => this.recoverSession(),
|
|
6531
7048
|
});
|
|
6532
7049
|
const circuits = options.circuits ?? defaultCircuitUrls(this.baseUrl);
|
|
6533
7050
|
const authClient = new AuthClient({ baseUrl: this.baseUrl, fetch: this.http.fetch });
|
|
6534
7051
|
const wsBaseUrl = toWsBase(this.baseUrl);
|
|
6535
7052
|
this.auth = new AuthNamespace({ auth: authClient, circuits, session: this.session });
|
|
6536
|
-
this.
|
|
7053
|
+
this.kv = new KvNamespace({ http: this.http, session: this.session, wsBaseUrl });
|
|
7054
|
+
this.storage = new StorageNamespace({ http: this.http, baseUrl: this.baseUrl, kv: this.kv });
|
|
6537
7055
|
this.message = new MessageNamespace({ http: this.http, session: this.session, wsBaseUrl });
|
|
6538
7056
|
// Group keys are cached in the user's PersonalSpace (encrypted at rest
|
|
6539
|
-
// by
|
|
6540
|
-
//
|
|
7057
|
+
// by client.kv), so a returning member hydrates without a fresh keyring
|
|
7058
|
+
// round-trip. The server only ever sees the ciphertext.
|
|
6541
7059
|
const spaceKeyCache = {
|
|
6542
|
-
loadKeys: (spaceId) => this.
|
|
6543
|
-
saveKeys: (spaceId, keys) => this.
|
|
7060
|
+
loadKeys: (spaceId) => this.kv.get("space-keys", spaceId),
|
|
7061
|
+
saveKeys: (spaceId, keys) => this.kv.set("space-keys", spaceId, keys),
|
|
6544
7062
|
};
|
|
6545
7063
|
this.space = new SpaceNamespace({
|
|
6546
7064
|
http: this.http,
|
|
@@ -6548,6 +7066,8 @@ class Client {
|
|
|
6548
7066
|
wsBaseUrl,
|
|
6549
7067
|
cache: spaceKeyCache,
|
|
6550
7068
|
});
|
|
7069
|
+
this.agents = new AgentsNamespace({ http: this.http });
|
|
7070
|
+
this.functions = new FunctionsNamespace({ http: this.http });
|
|
6551
7071
|
}
|
|
6552
7072
|
/** The currently signed-in user, or `null`. */
|
|
6553
7073
|
get user() {
|
|
@@ -6557,6 +7077,48 @@ class Client {
|
|
|
6557
7077
|
get isAuthenticated() {
|
|
6558
7078
|
return this.session.isAuthenticated;
|
|
6559
7079
|
}
|
|
7080
|
+
/**
|
|
7081
|
+
* Subscribe to session expiry that couldn't be silently recovered (the
|
|
7082
|
+
* user's identity wasn't in memory to re-prove — typically after a reload
|
|
7083
|
+
* where only the token was persisted, and that token has since gone stale).
|
|
7084
|
+
* This is the signal to send the user back to the login screen.
|
|
7085
|
+
*
|
|
7086
|
+
* Returns an unsubscribe function.
|
|
7087
|
+
*
|
|
7088
|
+
* const off = client.onSessionExpired(() => router.push('/login'));
|
|
7089
|
+
*/
|
|
7090
|
+
onSessionExpired(handler) {
|
|
7091
|
+
this.sessionExpiredHandlers.add(handler);
|
|
7092
|
+
return () => this.sessionExpiredHandlers.delete(handler);
|
|
7093
|
+
}
|
|
7094
|
+
/**
|
|
7095
|
+
* Attempt a silent re-auth of the current user. Concurrent callers share a
|
|
7096
|
+
* single in-flight attempt (a stale token usually 401s several requests at
|
|
7097
|
+
* once — we re-auth once, not once per request). On failure the
|
|
7098
|
+
* session-expired event fires so the app can react. Exposed so apps can
|
|
7099
|
+
* also drive recovery proactively (e.g. on `visibilitychange`).
|
|
7100
|
+
*/
|
|
7101
|
+
recoverSession() {
|
|
7102
|
+
if (this.recoverInFlight)
|
|
7103
|
+
return this.recoverInFlight;
|
|
7104
|
+
this.recoverInFlight = this.auth.zk
|
|
7105
|
+
.recover()
|
|
7106
|
+
.then((ok) => {
|
|
7107
|
+
if (!ok) {
|
|
7108
|
+
for (const handler of this.sessionExpiredHandlers) {
|
|
7109
|
+
try {
|
|
7110
|
+
handler();
|
|
7111
|
+
}
|
|
7112
|
+
catch { /* a bad listener mustn't break recovery */ }
|
|
7113
|
+
}
|
|
7114
|
+
}
|
|
7115
|
+
return ok;
|
|
7116
|
+
})
|
|
7117
|
+
.finally(() => {
|
|
7118
|
+
this.recoverInFlight = null;
|
|
7119
|
+
});
|
|
7120
|
+
return this.recoverInFlight;
|
|
7121
|
+
}
|
|
6560
7122
|
}
|
|
6561
7123
|
|
|
6562
7124
|
/**
|
|
@@ -6625,5 +7187,5 @@ var bundledLoader = /*#__PURE__*/Object.freeze({
|
|
|
6625
7187
|
default: loadRsWasm
|
|
6626
7188
|
});
|
|
6627
7189
|
|
|
6628
|
-
export { AES_GCM_IV_BYTES, AES_GCM_KEY_BYTES, AES_GCM_TAG_BYTES, AuthClient, AuthNamespace, AuthPublicInput, Authenticator, BroadcastChannel, BroadcastChannelEvents, ChannelExistsError, ChannelNotFoundError, ChunkCipher, Client, DEFAULT_BASE_URL, DoubleRatchet, DoubleRatchetCipher, DoubleRatchetManager, EncryptedSession, EventCore, EventCoreEvents, Field, Field as FieldElement, FileStorage, HashKnowledge, HttpClient, HttpError, KEEPER_MEMBER_ID, KeyStore, KeyringClient, LocalStorageSessionStore, MemorySessionStore, Message, Message as MessageDefault, MessageNamespace, PBKDF2_DEFAULT_ITERATIONS, PREIMAGE_POK_VERIFICATION_KEY, Packet, PersonalSpaceClient, Poseidon, PreimagePoK, ReedSolomonCodec, Space as Room, SerializeMessage, SessionState, ShardClient, SharedSpaceClient, Space, SpaceKeyring, SpaceNamespace, SpacePacketCipher, 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 };
|
|
7190
|
+
export { AES_GCM_IV_BYTES, AES_GCM_KEY_BYTES, AES_GCM_TAG_BYTES, AgentsNamespace, AuthClient, AuthNamespace, AuthPublicInput, Authenticator, BroadcastChannel, BroadcastChannelEvents, ChannelExistsError, ChannelNotFoundError, ChunkCipher, Client, DEFAULT_BASE_URL, DoubleRatchet, DoubleRatchetCipher, DoubleRatchetManager, EncryptedSession, EventCore, EventCoreEvents, Field, Field as FieldElement, FileStorage, FunctionsNamespace, HashKnowledge, HttpClient, HttpError, KEEPER_MEMBER_ID, KeyStore, KeyringClient, KvNamespace, LocalStorageSessionStore, MemorySessionStore, Message, Message as MessageDefault, MessageNamespace, PBKDF2_DEFAULT_ITERATIONS, PREIMAGE_POK_VERIFICATION_KEY, Packet, PersonalSpaceClient, Poseidon, PreimagePoK, ReedSolomonCodec, Space as Room, SerializeMessage, SessionState, ShardClient, SharedSpaceClient, Space, SpaceKeyring, SpaceNamespace, SpacePacketCipher, 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 };
|
|
6629
7191
|
//# sourceMappingURL=index.js.map
|