@muhkoo/connect 0.1.0-alpha.2 → 0.2.0-alpha.2
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 +26 -18
- package/dist/browser/index.js +1021 -28
- package/dist/browser/index.js.map +1 -1
- package/dist/connect.d.ts +527 -28
- package/dist/server/index.js +1017 -28
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/dist/browser/index.js
CHANGED
|
@@ -275,6 +275,10 @@ function fromBase64(b64) {
|
|
|
275
275
|
out[i] = binary.charCodeAt(i);
|
|
276
276
|
return out;
|
|
277
277
|
}
|
|
278
|
+
/** Encode a byte array as a base64url string (no padding). */
|
|
279
|
+
function toBase64Url(bytes) {
|
|
280
|
+
return toBase64(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
281
|
+
}
|
|
278
282
|
/** Decode a base64url string (with or without padding) to bytes. */
|
|
279
283
|
function fromBase64Url(b64url) {
|
|
280
284
|
const padded = b64url.replace(/-/g, "+").replace(/_/g, "/") +
|
|
@@ -5259,50 +5263,269 @@ class StorageNamespace {
|
|
|
5259
5263
|
}
|
|
5260
5264
|
|
|
5261
5265
|
/**
|
|
5262
|
-
*
|
|
5263
|
-
*
|
|
5264
|
-
*
|
|
5265
|
-
*
|
|
5266
|
-
*
|
|
5267
|
-
*
|
|
5268
|
-
*
|
|
5269
|
-
*
|
|
5270
|
-
*
|
|
5271
|
-
*
|
|
5272
|
-
*
|
|
5273
|
-
*
|
|
5274
|
-
*
|
|
5275
|
-
*
|
|
5276
|
-
*
|
|
5277
|
-
*
|
|
5266
|
+
* SpaceCipher — pure crypto for the fan-out group-encryption layer.
|
|
5267
|
+
*
|
|
5268
|
+
* Everything here routes through the canonical primitives in
|
|
5269
|
+
* `src/crypto/primitives` (AES-256-GCM, HKDF-SHA256, randomBytes) and the
|
|
5270
|
+
* P-384 ECDH `deriveBits` call the Double Ratchet already uses. No new
|
|
5271
|
+
* algorithms, no `crypto.subtle` reach-arounds beyond key gen / derive /
|
|
5272
|
+
* import-export (which the primitive layer intentionally doesn't cover).
|
|
5273
|
+
*
|
|
5274
|
+
* Three concerns:
|
|
5275
|
+
* 1. Space identity — a P-384 ECDH keypair; the encoded public key is the
|
|
5276
|
+
* space's id.
|
|
5277
|
+
* 2. Group key distribution — ECIES wrap/unwrap of `K_space` to a member's
|
|
5278
|
+
* identity ECDH public key (ephemeral-static ECDH → HKDF → AES-GCM).
|
|
5279
|
+
* 3. Message sealing — encrypt the serialized `Message` once under the
|
|
5280
|
+
* epoch's group key.
|
|
5281
|
+
*
|
|
5282
|
+
* Public keys are transported as base64url-encoded JWK, matching the JWK
|
|
5283
|
+
* transport convention already used in `EncryptedSession`.
|
|
5284
|
+
*/
|
|
5285
|
+
/** HKDF label binding the wrap key to this protocol + version. */
|
|
5286
|
+
const WRAP_INFO = "muhkoo/space/keywrap/v1";
|
|
5287
|
+
/** Algorithm tag stamped into every WrappedKey. */
|
|
5288
|
+
const WRAP_ALG = "ECDH-P384/HKDF-SHA256/AES-256-GCM";
|
|
5289
|
+
/** Bits in a P-384 ECDH shared secret. */
|
|
5290
|
+
const P384_BITS = 384;
|
|
5291
|
+
const ECDH_PARAMS = { name: "ECDH", namedCurve: "P-384" };
|
|
5292
|
+
// ---------------------------------------------------------------------------
|
|
5293
|
+
// Public-key transport (base64url JWK)
|
|
5294
|
+
// ---------------------------------------------------------------------------
|
|
5295
|
+
/** Encode an ECDH public `CryptoKey` as a base64url JWK string. */
|
|
5296
|
+
async function exportEcdhPublicKey(key) {
|
|
5297
|
+
const jwk = await getSubtle().exportKey("jwk", key);
|
|
5298
|
+
return toBase64Url(utf8Encode(JSON.stringify(jwk)));
|
|
5299
|
+
}
|
|
5300
|
+
/** Decode a base64url JWK string back into an ECDH public `CryptoKey`. */
|
|
5301
|
+
async function importEcdhPublicKey(encoded) {
|
|
5302
|
+
const jwk = JSON.parse(utf8Decode(fromBase64Url(encoded)));
|
|
5303
|
+
// Public keys carry no usages.
|
|
5304
|
+
return getSubtle().importKey("jwk", jwk, ECDH_PARAMS, true, []);
|
|
5305
|
+
}
|
|
5306
|
+
/** The space's id is just its encoded ECDH public key. */
|
|
5307
|
+
const encodeSpaceId = exportEcdhPublicKey;
|
|
5308
|
+
// ---------------------------------------------------------------------------
|
|
5309
|
+
// Sender authentication (ECDSA P-384, matching the Double Ratchet path).
|
|
5310
|
+
// A fan-out message is signed by its sender's identity ECDSA key so receivers
|
|
5311
|
+
// can verify authorship end-to-end — independent of the relay, which only
|
|
5312
|
+
// stamps `source` server-side. SHA-256 hash to match `DoubleRatchet`.
|
|
5313
|
+
// ---------------------------------------------------------------------------
|
|
5314
|
+
const ECDSA_PARAMS = { name: "ECDSA", namedCurve: "P-384" };
|
|
5315
|
+
const ECDSA_SIGN = { name: "ECDSA", hash: "SHA-256" };
|
|
5316
|
+
/** Encode an ECDSA public `CryptoKey` as a base64url JWK string. */
|
|
5317
|
+
async function exportEcdsaPublicKey(key) {
|
|
5318
|
+
const jwk = await getSubtle().exportKey("jwk", key);
|
|
5319
|
+
return toBase64Url(utf8Encode(JSON.stringify(jwk)));
|
|
5320
|
+
}
|
|
5321
|
+
/** Decode a base64url JWK string back into an ECDSA verify-only public key. */
|
|
5322
|
+
async function importEcdsaPublicKey(encoded) {
|
|
5323
|
+
const jwk = JSON.parse(utf8Decode(fromBase64Url(encoded)));
|
|
5324
|
+
return getSubtle().importKey("jwk", jwk, ECDSA_PARAMS, true, ["verify"]);
|
|
5325
|
+
}
|
|
5326
|
+
/**
|
|
5327
|
+
* Canonical bytes a sender signs (and a receiver re-derives). Binds the claimed
|
|
5328
|
+
* sender + routing + the sealed ciphertext, so a relay can't relabel `source`
|
|
5329
|
+
* and a member can't reuse another's ciphertext under a new envelope.
|
|
5330
|
+
*/
|
|
5331
|
+
function canonicalMessage(fields) {
|
|
5332
|
+
return [fields.source, fields.target, fields.subject, fields.epoch, fields.iv, fields.ciphertext].join("\n");
|
|
5333
|
+
}
|
|
5334
|
+
/** Sign the canonical bytes with the sender's ECDSA identity private key. */
|
|
5335
|
+
async function signSpaceMessage(canonical, ecdsaPriv) {
|
|
5336
|
+
const sig = await getSubtle().sign(ECDSA_SIGN, ecdsaPriv, utf8Encode(canonical));
|
|
5337
|
+
return toBase64(new Uint8Array(sig));
|
|
5338
|
+
}
|
|
5339
|
+
/** Verify a signature over the canonical bytes against the sender's ECDSA key. */
|
|
5340
|
+
async function verifySpaceMessage(canonical, sigB64, ecdsaPub) {
|
|
5341
|
+
try {
|
|
5342
|
+
return await getSubtle().verify(ECDSA_SIGN, ecdsaPub, fromBase64(sigB64), utf8Encode(canonical));
|
|
5343
|
+
}
|
|
5344
|
+
catch {
|
|
5345
|
+
return false;
|
|
5346
|
+
}
|
|
5347
|
+
}
|
|
5348
|
+
/** Generate a fresh space identity keypair (P-384 ECDH). */
|
|
5349
|
+
async function generateSpaceIdentity() {
|
|
5350
|
+
const kp = await getSubtle().generateKey(ECDH_PARAMS, true, ["deriveBits"]);
|
|
5351
|
+
const id = await encodeSpaceId(kp.publicKey);
|
|
5352
|
+
return { id, publicKey: kp.publicKey, privateKey: kp.privateKey };
|
|
5353
|
+
}
|
|
5354
|
+
// ---------------------------------------------------------------------------
|
|
5355
|
+
// Group key
|
|
5356
|
+
// ---------------------------------------------------------------------------
|
|
5357
|
+
/** Mint a fresh 256-bit group key for an epoch. */
|
|
5358
|
+
function generateSpaceKey() {
|
|
5359
|
+
return randomBytes(AES_GCM_KEY_BYTES);
|
|
5360
|
+
}
|
|
5361
|
+
// ---------------------------------------------------------------------------
|
|
5362
|
+
// ECIES wrap / unwrap of the group key
|
|
5363
|
+
// ---------------------------------------------------------------------------
|
|
5364
|
+
/**
|
|
5365
|
+
* Wrap `kSpace` for `recipientEcdhPub` using an ephemeral ECDH keypair.
|
|
5366
|
+
* The HKDF salt binds the wrap key to `epoch`, so a wrap minted for one epoch
|
|
5367
|
+
* cannot be replayed to unwrap a different epoch's blob.
|
|
5368
|
+
*/
|
|
5369
|
+
async function wrapSpaceKey(kSpace, epoch, recipientEcdhPub) {
|
|
5370
|
+
const eph = await getSubtle().generateKey(ECDH_PARAMS, true, ["deriveBits"]);
|
|
5371
|
+
const shared = new Uint8Array(await getSubtle().deriveBits({ name: "ECDH", public: recipientEcdhPub }, eph.privateKey, P384_BITS));
|
|
5372
|
+
const wrapKey = await deriveBitsHkdf(shared, WRAP_INFO, AES_GCM_KEY_BYTES, utf8Encode(`epoch:${epoch}`));
|
|
5373
|
+
const iv = randomBytes(AES_GCM_IV_BYTES);
|
|
5374
|
+
const ciphertext = await encryptAesGcm(wrapKey, iv, kSpace);
|
|
5375
|
+
return {
|
|
5376
|
+
epoch,
|
|
5377
|
+
ephemeralPub: await exportEcdhPublicKey(eph.publicKey),
|
|
5378
|
+
iv: toBase64(iv),
|
|
5379
|
+
ciphertext: toBase64(ciphertext),
|
|
5380
|
+
alg: WRAP_ALG,
|
|
5381
|
+
};
|
|
5382
|
+
}
|
|
5383
|
+
/**
|
|
5384
|
+
* Unwrap a {@link WrappedKey} using the recipient's identity ECDH private key.
|
|
5385
|
+
* Throws (GCM tag failure) on the wrong private key or a tampered blob.
|
|
5386
|
+
*/
|
|
5387
|
+
async function unwrapSpaceKey(wrapped, ownEcdhPriv) {
|
|
5388
|
+
const ephPub = await importEcdhPublicKey(wrapped.ephemeralPub);
|
|
5389
|
+
const shared = new Uint8Array(await getSubtle().deriveBits({ name: "ECDH", public: ephPub }, ownEcdhPriv, P384_BITS));
|
|
5390
|
+
const wrapKey = await deriveBitsHkdf(shared, WRAP_INFO, AES_GCM_KEY_BYTES, utf8Encode(`epoch:${wrapped.epoch}`));
|
|
5391
|
+
return decryptAesGcm(wrapKey, fromBase64(wrapped.iv), fromBase64(wrapped.ciphertext));
|
|
5392
|
+
}
|
|
5393
|
+
// ---------------------------------------------------------------------------
|
|
5394
|
+
// Message sealing
|
|
5395
|
+
// ---------------------------------------------------------------------------
|
|
5396
|
+
/** Seal an already-serialized message string under the group key. */
|
|
5397
|
+
async function sealSerialized(kSpace, serialized) {
|
|
5398
|
+
const iv = randomBytes(AES_GCM_IV_BYTES);
|
|
5399
|
+
const ciphertext = await encryptAesGcm(kSpace, iv, utf8Encode(serialized));
|
|
5400
|
+
return { iv: toBase64(iv), ciphertext: toBase64(ciphertext) };
|
|
5401
|
+
}
|
|
5402
|
+
/**
|
|
5403
|
+
* Open a sealed payload back into its serialized message string. Throws (GCM
|
|
5404
|
+
* tag failure) on the wrong group key or a tampered ciphertext.
|
|
5405
|
+
*/
|
|
5406
|
+
async function openSerialized(kSpace, iv, ciphertext) {
|
|
5407
|
+
const plaintext = await decryptAesGcm(kSpace, fromBase64(iv), fromBase64(ciphertext));
|
|
5408
|
+
return utf8Decode(plaintext);
|
|
5409
|
+
}
|
|
5410
|
+
|
|
5411
|
+
/**
|
|
5412
|
+
* SpacePacketCipher — the {@link PacketCipher} that seals packets with a
|
|
5413
|
+
* space's group key instead of a per-peer Double Ratchet.
|
|
5414
|
+
*
|
|
5415
|
+
* It pulls the active epoch's key from an {@link EpochKeyProvider} (the
|
|
5416
|
+
* SpaceKeyring), so this adapter stays decoupled from the keyring's transport
|
|
5417
|
+
* and caching concerns — it only needs "what key seals epoch N".
|
|
5418
|
+
*/
|
|
5419
|
+
class SpacePacketCipher {
|
|
5420
|
+
keys;
|
|
5421
|
+
contentType;
|
|
5422
|
+
constructor(keys, contentType) {
|
|
5423
|
+
this.keys = keys;
|
|
5424
|
+
this.contentType = contentType;
|
|
5425
|
+
}
|
|
5426
|
+
async seal(serializedMessage) {
|
|
5427
|
+
const epoch = this.keys.currentEpoch();
|
|
5428
|
+
const key = this.keys.keyForEpoch(epoch);
|
|
5429
|
+
if (!key) {
|
|
5430
|
+
throw new Error(`SpacePacketCipher: no group key for epoch ${epoch}`);
|
|
5431
|
+
}
|
|
5432
|
+
const { iv, ciphertext } = await sealSerialized(key, serializedMessage);
|
|
5433
|
+
const headers = { spaceSealed: true, epoch, iv, ciphertext };
|
|
5434
|
+
if (this.contentType)
|
|
5435
|
+
headers.contentType = this.contentType;
|
|
5436
|
+
return headers;
|
|
5437
|
+
}
|
|
5438
|
+
handles(headers) {
|
|
5439
|
+
return headers?.spaceSealed === true &&
|
|
5440
|
+
typeof headers?.iv === "string" &&
|
|
5441
|
+
typeof headers?.ciphertext === "string";
|
|
5442
|
+
}
|
|
5443
|
+
/** Returns null for epochs we don't hold (surfaced as undecryptable). */
|
|
5444
|
+
async open(headers) {
|
|
5445
|
+
if (!this.handles(headers))
|
|
5446
|
+
return null;
|
|
5447
|
+
const epoch = Number(headers.epoch);
|
|
5448
|
+
const key = this.keys.keyForEpoch(epoch);
|
|
5449
|
+
if (!key)
|
|
5450
|
+
return null;
|
|
5451
|
+
return openSerialized(key, headers.iv, headers.ciphertext);
|
|
5452
|
+
}
|
|
5453
|
+
}
|
|
5454
|
+
|
|
5455
|
+
/**
|
|
5456
|
+
* `Space` — a client handle for one shared space.
|
|
5457
|
+
*
|
|
5458
|
+
* A space is the unit everything is built on. It carries:
|
|
5459
|
+
* - **realtime transport** over a {@link BroadcastChannel} (group handshake,
|
|
5460
|
+
* roster frames, raw-frame passthrough);
|
|
5461
|
+
* - **fan-out group messaging** — `Message`s sealed once with the space's
|
|
5462
|
+
* group key (via {@link SpacePacketCipher}), wrapped in a cleartext
|
|
5463
|
+
* {@link Packet} envelope, broadcast AND persisted by the server as
|
|
5464
|
+
* history (the `spaceMessage` frame);
|
|
5465
|
+
* - **room-scoped file storage** via {@link FileStorage} over the shard
|
|
5466
|
+
* endpoint;
|
|
5467
|
+
* - **legacy per-peer Double Ratchet messaging** (`send`/`announce`) for the
|
|
5468
|
+
* ephemeral, no-history path — unchanged so existing callers (the chat hook
|
|
5469
|
+
* reached via `client.message.room(name)`) keep working.
|
|
5470
|
+
*
|
|
5471
|
+
* `Space` was formerly `Room`; `Room` remains exported as an alias. The
|
|
5472
|
+
* fan-out methods (`sendMessage`/`onMessage`/`history`/`join`/`admit`/`rotate`)
|
|
5473
|
+
* require a {@link SpaceKeyring} dep; without one only the transport + DR +
|
|
5474
|
+
* file surface is available.
|
|
5278
5475
|
*/
|
|
5279
|
-
class
|
|
5476
|
+
class Space {
|
|
5280
5477
|
deps;
|
|
5281
5478
|
name;
|
|
5479
|
+
historyPolicy;
|
|
5282
5480
|
channel = null;
|
|
5283
5481
|
pending = [];
|
|
5482
|
+
cipher;
|
|
5483
|
+
/** Per-instance event bus for fan-out events (isolated across spaces). */
|
|
5484
|
+
events = new EventTarget();
|
|
5485
|
+
rawWired = false;
|
|
5284
5486
|
constructor(deps) {
|
|
5285
5487
|
this.deps = deps;
|
|
5286
5488
|
this.name = deps.name;
|
|
5489
|
+
this.historyPolicy = deps.historyPolicy ?? "static";
|
|
5490
|
+
this.cipher = deps.keyring ? new SpacePacketCipher(deps.keyring) : null;
|
|
5491
|
+
}
|
|
5492
|
+
/** The encoded space id (same as the transport name for fan-out spaces). */
|
|
5493
|
+
get id() {
|
|
5494
|
+
return this.name;
|
|
5287
5495
|
}
|
|
5288
|
-
|
|
5289
|
-
|
|
5496
|
+
/** The keyring backing this space's group keys, if any. */
|
|
5497
|
+
get keyring() {
|
|
5498
|
+
return this.deps.keyring;
|
|
5499
|
+
}
|
|
5500
|
+
// -- transport -------------------------------------------------------------
|
|
5501
|
+
/** Open the space websocket (resolves the ticket + identity lazily). */
|
|
5290
5502
|
async connect() {
|
|
5291
5503
|
if (!this.channel) {
|
|
5292
5504
|
const ticket = await this.deps.fetchTicket();
|
|
5293
5505
|
const url = `${this.deps.wsBaseUrl}/api/spaces/${encodeURIComponent(this.name)}/websocket` +
|
|
5294
5506
|
(ticket ? `?ticket=${encodeURIComponent(ticket)}` : "");
|
|
5295
|
-
this.channel =
|
|
5507
|
+
this.channel = this.deps.createChannel
|
|
5508
|
+
? this.deps.createChannel(url, this.deps.myId())
|
|
5509
|
+
: new BroadcastChannel({ url, myId: this.deps.myId(), autoAnnounce: false });
|
|
5296
5510
|
for (const [event, handler] of this.pending)
|
|
5297
5511
|
this.channel.on(event, handler);
|
|
5298
5512
|
this.pending.length = 0;
|
|
5513
|
+
// Fan-out spaces drive their own `{name}` handshake + frame routing.
|
|
5514
|
+
// Legacy (keyring-less) callers wire these themselves, so leave that
|
|
5515
|
+
// path byte-for-byte unchanged.
|
|
5516
|
+
if (this.deps.keyring) {
|
|
5517
|
+
this.channel.on(BroadcastChannelEvents.CONNECTED, () => {
|
|
5518
|
+
this.channel?.sendRaw({ name: this.deps.myId() });
|
|
5519
|
+
});
|
|
5520
|
+
this.wireFanout();
|
|
5521
|
+
}
|
|
5299
5522
|
}
|
|
5300
5523
|
await this.channel.connect();
|
|
5301
5524
|
}
|
|
5302
5525
|
disconnect() {
|
|
5303
5526
|
this.channel?.disconnect();
|
|
5304
5527
|
}
|
|
5305
|
-
/** Subscribe to a channel event. Buffered until `connect()` if early. */
|
|
5528
|
+
/** Subscribe to a raw channel event. Buffered until `connect()` if early. */
|
|
5306
5529
|
on(event, handler) {
|
|
5307
5530
|
if (this.channel)
|
|
5308
5531
|
this.channel.on(event, handler);
|
|
@@ -5318,11 +5541,12 @@ class Room {
|
|
|
5318
5541
|
this.pending.splice(i, 1);
|
|
5319
5542
|
}
|
|
5320
5543
|
}
|
|
5544
|
+
// -- legacy Double Ratchet path (unchanged) --------------------------------
|
|
5321
5545
|
/** Advertise our keyExchange to the room (idempotent per connection). */
|
|
5322
5546
|
announce() {
|
|
5323
5547
|
return this.channel ? this.channel.announce() : Promise.resolve();
|
|
5324
5548
|
}
|
|
5325
|
-
/** E2E-encrypt `text` to every peer
|
|
5549
|
+
/** E2E-encrypt `text` to every peer via the Double Ratchet (no history). */
|
|
5326
5550
|
send(text) {
|
|
5327
5551
|
return this.channel ? this.channel.send(text) : Promise.resolve(0);
|
|
5328
5552
|
}
|
|
@@ -5331,21 +5555,130 @@ class Room {
|
|
|
5331
5555
|
this.channel?.sendRaw(frame);
|
|
5332
5556
|
}
|
|
5333
5557
|
peers() {
|
|
5334
|
-
return this.channel ? this.channel.peers() : [];
|
|
5558
|
+
return this.channel?.peers ? this.channel.peers() : [];
|
|
5335
5559
|
}
|
|
5336
5560
|
isConnected() {
|
|
5337
|
-
return this.channel ? this.channel.isConnected() : false;
|
|
5561
|
+
return this.channel?.isConnected ? this.channel.isConnected() : false;
|
|
5338
5562
|
}
|
|
5339
5563
|
/** The underlying channel, for advanced use. `null` before `connect()`. */
|
|
5340
5564
|
get raw() {
|
|
5341
5565
|
return this.channel;
|
|
5342
5566
|
}
|
|
5567
|
+
// -- fan-out group messaging -----------------------------------------------
|
|
5568
|
+
/**
|
|
5569
|
+
* Seal `payload` once with the current group key and broadcast it as a
|
|
5570
|
+
* persisted `spaceMessage`. Requires a keyring.
|
|
5571
|
+
*/
|
|
5572
|
+
async sendMessage(payload, opts = {}) {
|
|
5573
|
+
const cipher = this.requireCipher(opts.contentType);
|
|
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() });
|
|
5590
|
+
}
|
|
5591
|
+
/** Subscribe to decrypted fan-out messages. Returns an unsubscribe fn. */
|
|
5592
|
+
onMessage(handler) {
|
|
5593
|
+
const listener = (e) => handler(e.detail);
|
|
5594
|
+
this.events.addEventListener("message", listener);
|
|
5595
|
+
return () => this.events.removeEventListener("message", listener);
|
|
5596
|
+
}
|
|
5597
|
+
/** Subscribe to inbound join requests (for a key-holder to admit). */
|
|
5598
|
+
onJoinRequest(handler) {
|
|
5599
|
+
const listener = (e) => handler(e.detail);
|
|
5600
|
+
this.events.addEventListener("join-request", listener);
|
|
5601
|
+
return () => this.events.removeEventListener("join-request", listener);
|
|
5602
|
+
}
|
|
5603
|
+
/**
|
|
5604
|
+
* Fetch + decrypt persisted history beyond the live backlog.
|
|
5605
|
+
* `before` is an opaque cursor returned as `nextCursor`.
|
|
5606
|
+
*/
|
|
5607
|
+
async history(opts = {}) {
|
|
5608
|
+
const cipher = this.requireCipher();
|
|
5609
|
+
const params = new URLSearchParams();
|
|
5610
|
+
if (opts.before)
|
|
5611
|
+
params.set("before", opts.before);
|
|
5612
|
+
if (opts.limit)
|
|
5613
|
+
params.set("limit", String(opts.limit));
|
|
5614
|
+
const url = `${this.deps.httpBaseUrl}/api/spaces/${encodeURIComponent(this.name)}/history` +
|
|
5615
|
+
(params.toString() ? `?${params}` : "");
|
|
5616
|
+
const res = await this.deps.fetch(url);
|
|
5617
|
+
if (!res.ok)
|
|
5618
|
+
throw new Error(`Space.history: ${res.status} ${res.statusText}`);
|
|
5619
|
+
const body = (await res.json());
|
|
5620
|
+
// Warm the member directory once so historical senders' signatures verify.
|
|
5621
|
+
// (refreshDirectory swallows its own transport errors.)
|
|
5622
|
+
await this.deps.keyring?.refreshDirectory();
|
|
5623
|
+
const messages = [];
|
|
5624
|
+
for (const raw of body.messages ?? []) {
|
|
5625
|
+
const decoded = await this.decodeFrame(raw, cipher);
|
|
5626
|
+
if (decoded)
|
|
5627
|
+
messages.push(decoded);
|
|
5628
|
+
}
|
|
5629
|
+
return { messages, nextCursor: body.nextCursor ?? null };
|
|
5630
|
+
}
|
|
5631
|
+
/**
|
|
5632
|
+
* Bring this member's group keys up to date: load from cache, and if we
|
|
5633
|
+
* hold nothing, request a key and poll until one arrives (or timeout).
|
|
5634
|
+
*/
|
|
5635
|
+
async join(opts = {}) {
|
|
5636
|
+
const keyring = this.requireKeyring();
|
|
5637
|
+
await this.connect();
|
|
5638
|
+
if (await keyring.loadFromCache())
|
|
5639
|
+
return true;
|
|
5640
|
+
if (keyring.hasAnyKey())
|
|
5641
|
+
return true;
|
|
5642
|
+
await keyring.requestKey();
|
|
5643
|
+
const deadline = Date.now() + (opts.timeoutMs ?? 15000);
|
|
5644
|
+
const pollMs = opts.pollMs ?? 500;
|
|
5645
|
+
// eslint-disable-next-line no-constant-condition
|
|
5646
|
+
while (Date.now() < deadline) {
|
|
5647
|
+
if ((await keyring.pullKeys()) > 0)
|
|
5648
|
+
return true;
|
|
5649
|
+
await delay(pollMs);
|
|
5650
|
+
}
|
|
5651
|
+
return false;
|
|
5652
|
+
}
|
|
5653
|
+
/** Creator path: mint the initial group key, then connect. */
|
|
5654
|
+
async create() {
|
|
5655
|
+
const keyring = this.requireKeyring();
|
|
5656
|
+
await keyring.bootstrapNew();
|
|
5657
|
+
await this.connect();
|
|
5658
|
+
// Publish our identity in the member directory so peers can verify our
|
|
5659
|
+
// message signatures (the creator never "joins", so register here).
|
|
5660
|
+
await keyring.requestKey().catch(() => { });
|
|
5661
|
+
// Admit anyone who requested a key before we were listening.
|
|
5662
|
+
if (this.deps.autoAdmit !== false) {
|
|
5663
|
+
await keyring.admitPending().catch(() => { });
|
|
5664
|
+
}
|
|
5665
|
+
}
|
|
5666
|
+
/** Wrap the group key for a newcomer and post it (key-holder action). */
|
|
5667
|
+
async admit(memberId, identityEcdhPub) {
|
|
5668
|
+
await this.requireKeyring().admit(memberId, identityEcdhPub);
|
|
5669
|
+
}
|
|
5670
|
+
/** Invite a user to this (private) channel's membership allowlist. */
|
|
5671
|
+
async invite(username) {
|
|
5672
|
+
await this.requireKeyring().invite(username);
|
|
5673
|
+
}
|
|
5674
|
+
/** Rotate to a fresh epoch and re-wrap to the roster (rotate spaces). */
|
|
5675
|
+
async rotate(roster) {
|
|
5676
|
+
return this.requireKeyring().rotate(roster);
|
|
5677
|
+
}
|
|
5343
5678
|
// -- room-scoped files (no websocket needed) -------------------------------
|
|
5344
|
-
/** Encrypt + erasure-code + upload `file` to this room's shards. */
|
|
5345
5679
|
async putFile(file, metadata) {
|
|
5346
5680
|
return this.fileStorage().writeFileToShards({ data: file, metadata });
|
|
5347
5681
|
}
|
|
5348
|
-
/** Fetch + decode a file previously shared in this room. */
|
|
5349
5682
|
async getFile(manifest) {
|
|
5350
5683
|
return this.fileStorage().readFileFromShards(manifest);
|
|
5351
5684
|
}
|
|
@@ -5357,6 +5690,129 @@ class Room {
|
|
|
5357
5690
|
});
|
|
5358
5691
|
return new FileStorage({ shards });
|
|
5359
5692
|
}
|
|
5693
|
+
// -- internals -------------------------------------------------------------
|
|
5694
|
+
/** Wire the RAW_FRAME handler that drives fan-out events. */
|
|
5695
|
+
wireFanout() {
|
|
5696
|
+
if (this.rawWired || !this.channel)
|
|
5697
|
+
return;
|
|
5698
|
+
this.rawWired = true;
|
|
5699
|
+
this.channel.on(BroadcastChannelEvents.RAW_FRAME, (e) => {
|
|
5700
|
+
void this.onRawFrame(e.detail);
|
|
5701
|
+
});
|
|
5702
|
+
}
|
|
5703
|
+
async onRawFrame(frame) {
|
|
5704
|
+
const f = frame;
|
|
5705
|
+
if (!f || typeof f !== "object")
|
|
5706
|
+
return;
|
|
5707
|
+
if (typeof f.spaceMessage === "string" && this.cipher) {
|
|
5708
|
+
const decoded = await this.decodeFrame(f.spaceMessage, this.cipher, /*raw*/ true);
|
|
5709
|
+
if (decoded)
|
|
5710
|
+
this.emitEvent("message", decoded);
|
|
5711
|
+
return;
|
|
5712
|
+
}
|
|
5713
|
+
if (f.joinRequest && typeof f.joinRequest === "object") {
|
|
5714
|
+
const req = f.joinRequest;
|
|
5715
|
+
this.emitEvent("join-request", req);
|
|
5716
|
+
// A new member appeared — refresh the directory so their future
|
|
5717
|
+
// messages verify, and (if we hold the key) auto-admit them.
|
|
5718
|
+
void this.deps.keyring?.refreshDirectory();
|
|
5719
|
+
if (this.deps.autoAdmit !== false && this.deps.keyring?.hasAnyKey() && req.memberId !== this.deps.myId()) {
|
|
5720
|
+
void this.deps.keyring.admit(req.memberId, req.identityEcdhPub).catch(() => { });
|
|
5721
|
+
}
|
|
5722
|
+
return;
|
|
5723
|
+
}
|
|
5724
|
+
if (f.keyringReady && this.deps.keyring) {
|
|
5725
|
+
// A key was wrapped for us — pull it.
|
|
5726
|
+
void this.deps.keyring.pullKeys().catch(() => { });
|
|
5727
|
+
}
|
|
5728
|
+
}
|
|
5729
|
+
/**
|
|
5730
|
+
* Decode one persisted/broadcast frame into a SpaceMessageEvent.
|
|
5731
|
+
* `isRawFrame` true means `input` is the already-extracted `spaceMessage`
|
|
5732
|
+
* packet string; false means it's the full `{spaceMessage, name, ...}` frame.
|
|
5733
|
+
*/
|
|
5734
|
+
async decodeFrame(input, cipher, isRawFrame = false) {
|
|
5735
|
+
try {
|
|
5736
|
+
let packetJson = input;
|
|
5737
|
+
if (!isRawFrame) {
|
|
5738
|
+
const outer = JSON.parse(input);
|
|
5739
|
+
if (typeof outer.spaceMessage !== "string")
|
|
5740
|
+
return null;
|
|
5741
|
+
packetJson = outer.spaceMessage;
|
|
5742
|
+
}
|
|
5743
|
+
const packet = Packet.deserialize(packetJson);
|
|
5744
|
+
if (!cipher.handles(packet.headers))
|
|
5745
|
+
return null;
|
|
5746
|
+
// Verify sender authenticity end-to-end: the message must be signed
|
|
5747
|
+
// by `source`'s identity ECDSA key (looked up in the member
|
|
5748
|
+
// directory). Drop anything unsigned, signed by an unknown member,
|
|
5749
|
+
// or whose signature doesn't match — this defeats both member
|
|
5750
|
+
// impersonation and a relay rewriting `source`.
|
|
5751
|
+
if (!(await this.verifySender(packet)))
|
|
5752
|
+
return null;
|
|
5753
|
+
const serialized = await cipher.open(packet.headers);
|
|
5754
|
+
if (serialized === null)
|
|
5755
|
+
return null; // epoch key we don't hold
|
|
5756
|
+
const message = Message.deserialize(serialized);
|
|
5757
|
+
return {
|
|
5758
|
+
from: packet.source,
|
|
5759
|
+
channel: packet.subject,
|
|
5760
|
+
epoch: Number(packet.headers?.epoch ?? 0),
|
|
5761
|
+
contentType: packet.headers?.contentType,
|
|
5762
|
+
message,
|
|
5763
|
+
};
|
|
5764
|
+
}
|
|
5765
|
+
catch (err) {
|
|
5766
|
+
this.emitEvent("error", err);
|
|
5767
|
+
return null;
|
|
5768
|
+
}
|
|
5769
|
+
}
|
|
5770
|
+
/**
|
|
5771
|
+
* True if `packet` carries a valid signature by `source`'s identity key.
|
|
5772
|
+
* On a directory cache miss, refresh once (a new member may have just
|
|
5773
|
+
* joined) before giving up. Without a keyring (legacy handle) there's
|
|
5774
|
+
* nothing to verify against → reject.
|
|
5775
|
+
*/
|
|
5776
|
+
async verifySender(packet) {
|
|
5777
|
+
const keyring = this.deps.keyring;
|
|
5778
|
+
const sig = packet.headers?.sig;
|
|
5779
|
+
if (!keyring || typeof sig !== "string")
|
|
5780
|
+
return false;
|
|
5781
|
+
let key = keyring.ecdsaKeyFor(packet.source);
|
|
5782
|
+
if (!key) {
|
|
5783
|
+
await keyring.refreshDirectory();
|
|
5784
|
+
key = keyring.ecdsaKeyFor(packet.source);
|
|
5785
|
+
if (!key)
|
|
5786
|
+
return false;
|
|
5787
|
+
}
|
|
5788
|
+
const canonical = canonicalMessage({
|
|
5789
|
+
source: packet.source,
|
|
5790
|
+
target: packet.target,
|
|
5791
|
+
subject: packet.subject,
|
|
5792
|
+
epoch: Number(packet.headers?.epoch ?? 0),
|
|
5793
|
+
iv: String(packet.headers?.iv ?? ""),
|
|
5794
|
+
ciphertext: String(packet.headers?.ciphertext ?? ""),
|
|
5795
|
+
});
|
|
5796
|
+
return verifySpaceMessage(canonical, sig, key);
|
|
5797
|
+
}
|
|
5798
|
+
emitEvent(type, detail) {
|
|
5799
|
+
this.events.dispatchEvent(new CustomEvent(type, { detail }));
|
|
5800
|
+
}
|
|
5801
|
+
requireKeyring() {
|
|
5802
|
+
if (!this.deps.keyring) {
|
|
5803
|
+
throw new Error("Space: this operation requires a keyring (open via client.space).");
|
|
5804
|
+
}
|
|
5805
|
+
return this.deps.keyring;
|
|
5806
|
+
}
|
|
5807
|
+
requireCipher(contentType) {
|
|
5808
|
+
if (!this.deps.keyring) {
|
|
5809
|
+
throw new Error("Space: fan-out messaging requires a keyring (open via client.space).");
|
|
5810
|
+
}
|
|
5811
|
+
return contentType ? new SpacePacketCipher(this.deps.keyring, contentType) : this.cipher;
|
|
5812
|
+
}
|
|
5813
|
+
}
|
|
5814
|
+
function delay(ms) {
|
|
5815
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
5360
5816
|
}
|
|
5361
5817
|
|
|
5362
5818
|
/**
|
|
@@ -5461,7 +5917,7 @@ class MessageNamespace {
|
|
|
5461
5917
|
* higher-level conveniences layered over the same space transport).
|
|
5462
5918
|
*/
|
|
5463
5919
|
room(roomName) {
|
|
5464
|
-
return new
|
|
5920
|
+
return new Space({
|
|
5465
5921
|
name: roomName,
|
|
5466
5922
|
wsBaseUrl: this.deps.wsBaseUrl,
|
|
5467
5923
|
httpBaseUrl: this.deps.http.baseUrl,
|
|
@@ -5537,6 +5993,486 @@ function parseMaybeJson(text) {
|
|
|
5537
5993
|
}
|
|
5538
5994
|
}
|
|
5539
5995
|
|
|
5996
|
+
/**
|
|
5997
|
+
* SpaceKeyring — the client's view of a space's group keys.
|
|
5998
|
+
*
|
|
5999
|
+
* Holds the per-epoch group keys this member possesses (in memory, optionally
|
|
6000
|
+
* mirrored to PersonalSpace), and orchestrates the server-blind distribution
|
|
6001
|
+
* dance against a {@link KeyringTransport}:
|
|
6002
|
+
*
|
|
6003
|
+
* - a newcomer posts a join request and pulls the wrapped key blob addressed
|
|
6004
|
+
* to it, unwrapping with its identity private key;
|
|
6005
|
+
* - an existing key-holder wraps the group key for a newcomer (ECIES to the
|
|
6006
|
+
* newcomer's identity public key) and posts the opaque blob;
|
|
6007
|
+
* - on membership change (rotate spaces) a fresh epoch key is minted and
|
|
6008
|
+
* re-wrapped to the current roster.
|
|
6009
|
+
*
|
|
6010
|
+
* The server only ever stores opaque {@link WrappedKey} blobs — it cannot read
|
|
6011
|
+
* the group key. Implements {@link EpochKeyProvider} so it can back a
|
|
6012
|
+
* {@link SpacePacketCipher} directly.
|
|
6013
|
+
*/
|
|
6014
|
+
/**
|
|
6015
|
+
* Reserved member id for the app's "keeper" — a trusted always-available member
|
|
6016
|
+
* (the accelerator's AppDOv1) that holds the group key and re-issues it to
|
|
6017
|
+
* newcomers, so a channel is joinable with no human online. Admitting this id
|
|
6018
|
+
* at channel creation hands the key to the keeper. MUST match the accelerator's
|
|
6019
|
+
* `KEEPER_MEMBER_ID`.
|
|
6020
|
+
*/
|
|
6021
|
+
const KEEPER_MEMBER_ID = "__keeper__";
|
|
6022
|
+
class SpaceKeyring {
|
|
6023
|
+
deps;
|
|
6024
|
+
keys = new Map();
|
|
6025
|
+
epoch = 0;
|
|
6026
|
+
policy;
|
|
6027
|
+
/** Cached member directory: memberId → imported ECDSA verify key. */
|
|
6028
|
+
memberEcdsa = new Map();
|
|
6029
|
+
constructor(deps) {
|
|
6030
|
+
this.deps = deps;
|
|
6031
|
+
this.policy = deps.historyPolicy ?? "static";
|
|
6032
|
+
}
|
|
6033
|
+
// -- member directory (for verifying sender signatures) -------------------
|
|
6034
|
+
/** The cached ECDSA verify key for a member, if known. */
|
|
6035
|
+
ecdsaKeyFor(memberId) {
|
|
6036
|
+
return this.memberEcdsa.get(memberId);
|
|
6037
|
+
}
|
|
6038
|
+
/** Refresh the member directory (memberId → ECDSA key) from the roster. */
|
|
6039
|
+
async refreshDirectory() {
|
|
6040
|
+
let roster;
|
|
6041
|
+
try {
|
|
6042
|
+
roster = await this.deps.transport.fetchRoster();
|
|
6043
|
+
}
|
|
6044
|
+
catch {
|
|
6045
|
+
return;
|
|
6046
|
+
}
|
|
6047
|
+
for (const m of roster) {
|
|
6048
|
+
if (!m.identityEcdsaPub || this.memberEcdsa.has(m.memberId))
|
|
6049
|
+
continue;
|
|
6050
|
+
try {
|
|
6051
|
+
this.memberEcdsa.set(m.memberId, await importEcdsaPublicKey(m.identityEcdsaPub));
|
|
6052
|
+
}
|
|
6053
|
+
catch {
|
|
6054
|
+
/* skip malformed entries */
|
|
6055
|
+
}
|
|
6056
|
+
}
|
|
6057
|
+
}
|
|
6058
|
+
// -- EpochKeyProvider ------------------------------------------------------
|
|
6059
|
+
currentEpoch() {
|
|
6060
|
+
return this.epoch;
|
|
6061
|
+
}
|
|
6062
|
+
keyForEpoch(epoch) {
|
|
6063
|
+
return this.keys.get(epoch);
|
|
6064
|
+
}
|
|
6065
|
+
hasAnyKey() {
|
|
6066
|
+
return this.keys.size > 0;
|
|
6067
|
+
}
|
|
6068
|
+
// -- lifecycle -------------------------------------------------------------
|
|
6069
|
+
/** Hydrate keys from the PersonalSpace cache, if available. Best-effort —
|
|
6070
|
+
* a cache miss/failure (e.g. not unlocked) must not block joining. */
|
|
6071
|
+
async loadFromCache() {
|
|
6072
|
+
if (!this.deps.cache)
|
|
6073
|
+
return false;
|
|
6074
|
+
let cached;
|
|
6075
|
+
try {
|
|
6076
|
+
cached = await this.deps.cache.loadKeys(this.deps.spaceId);
|
|
6077
|
+
}
|
|
6078
|
+
catch {
|
|
6079
|
+
return false;
|
|
6080
|
+
}
|
|
6081
|
+
if (!cached)
|
|
6082
|
+
return false;
|
|
6083
|
+
for (const [epochStr, b64] of Object.entries(cached)) {
|
|
6084
|
+
this.keys.set(Number(epochStr), fromBase64(b64));
|
|
6085
|
+
}
|
|
6086
|
+
this.epoch = Math.max(0, ...Array.from(this.keys.keys()));
|
|
6087
|
+
return this.keys.size > 0;
|
|
6088
|
+
}
|
|
6089
|
+
async persist() {
|
|
6090
|
+
if (!this.deps.cache)
|
|
6091
|
+
return;
|
|
6092
|
+
const out = {};
|
|
6093
|
+
for (const [epoch, key] of this.keys)
|
|
6094
|
+
out[String(epoch)] = toBase64(key);
|
|
6095
|
+
// Best-effort: caching is an optimization; a write failure (e.g. locked
|
|
6096
|
+
// storage) must not break key acquisition.
|
|
6097
|
+
try {
|
|
6098
|
+
await this.deps.cache.saveKeys(this.deps.spaceId, out);
|
|
6099
|
+
}
|
|
6100
|
+
catch {
|
|
6101
|
+
/* ignore */
|
|
6102
|
+
}
|
|
6103
|
+
}
|
|
6104
|
+
/** Creator path: mint epoch-0 group key for a brand-new space. */
|
|
6105
|
+
async bootstrapNew() {
|
|
6106
|
+
this.keys.set(0, generateSpaceKey());
|
|
6107
|
+
this.epoch = 0;
|
|
6108
|
+
await this.persist();
|
|
6109
|
+
}
|
|
6110
|
+
// -- newcomer: request + pull ---------------------------------------------
|
|
6111
|
+
/**
|
|
6112
|
+
* Post a join request so an online key-holder can wrap a key for us. Also
|
|
6113
|
+
* publishes our identity keys in the member directory (so others can verify
|
|
6114
|
+
* our message signatures, and the keeper can wrap to us).
|
|
6115
|
+
*/
|
|
6116
|
+
async requestKey(desiredEpoch) {
|
|
6117
|
+
await this.deps.transport.postJoinRequest({
|
|
6118
|
+
memberId: this.deps.memberId,
|
|
6119
|
+
identityEcdhPub: this.deps.identityEcdhPub,
|
|
6120
|
+
identityEcdsaPub: this.deps.identityEcdsaPub,
|
|
6121
|
+
desiredEpoch,
|
|
6122
|
+
});
|
|
6123
|
+
}
|
|
6124
|
+
/**
|
|
6125
|
+
* Pull and unwrap any blobs addressed to us. Returns the number of new
|
|
6126
|
+
* epoch keys obtained. Safe to call repeatedly (idempotent).
|
|
6127
|
+
*/
|
|
6128
|
+
async pullKeys() {
|
|
6129
|
+
const priv = this.deps.ownPrivateKey();
|
|
6130
|
+
if (!priv)
|
|
6131
|
+
throw new Error("SpaceKeyring: no identity private key to unwrap with");
|
|
6132
|
+
const blobs = await this.deps.transport.fetchBlobs(this.deps.memberId);
|
|
6133
|
+
let added = 0;
|
|
6134
|
+
for (const blob of blobs) {
|
|
6135
|
+
if (this.keys.has(blob.epoch))
|
|
6136
|
+
continue;
|
|
6137
|
+
try {
|
|
6138
|
+
const key = await unwrapSpaceKey(blob, priv);
|
|
6139
|
+
this.keys.set(blob.epoch, key);
|
|
6140
|
+
added++;
|
|
6141
|
+
}
|
|
6142
|
+
catch {
|
|
6143
|
+
// Not for us / tampered — skip this blob.
|
|
6144
|
+
}
|
|
6145
|
+
}
|
|
6146
|
+
if (added > 0) {
|
|
6147
|
+
this.epoch = Math.max(this.epoch, ...Array.from(this.keys.keys()));
|
|
6148
|
+
await this.persist();
|
|
6149
|
+
}
|
|
6150
|
+
return added;
|
|
6151
|
+
}
|
|
6152
|
+
// -- key-holder: admit + rotate -------------------------------------------
|
|
6153
|
+
/**
|
|
6154
|
+
* Wrap our group key(s) for a newcomer and post the blobs. In a `static`
|
|
6155
|
+
* space the newcomer receives every epoch we hold (full history); in a
|
|
6156
|
+
* `rotate` space they receive only the current epoch.
|
|
6157
|
+
*/
|
|
6158
|
+
async admit(targetMemberId, identityEcdhPub) {
|
|
6159
|
+
const recipient = await importEcdhPublicKey(identityEcdhPub);
|
|
6160
|
+
const epochs = this.policy === "static"
|
|
6161
|
+
? Array.from(this.keys.keys())
|
|
6162
|
+
: [this.epoch];
|
|
6163
|
+
for (const epoch of epochs) {
|
|
6164
|
+
const key = this.keys.get(epoch);
|
|
6165
|
+
if (!key)
|
|
6166
|
+
continue;
|
|
6167
|
+
const wrapped = await wrapSpaceKey(key, epoch, recipient);
|
|
6168
|
+
await this.deps.transport.postWrappedKey(targetMemberId, wrapped, this.deps.memberId);
|
|
6169
|
+
}
|
|
6170
|
+
}
|
|
6171
|
+
/**
|
|
6172
|
+
* Admit every outstanding join request (an online key-holder fulfilling
|
|
6173
|
+
* pending newcomers). Returns the ids admitted.
|
|
6174
|
+
*/
|
|
6175
|
+
async admitPending() {
|
|
6176
|
+
const pending = await this.deps.transport.fetchPending();
|
|
6177
|
+
const admitted = [];
|
|
6178
|
+
for (const req of pending) {
|
|
6179
|
+
if (req.memberId === this.deps.memberId)
|
|
6180
|
+
continue;
|
|
6181
|
+
await this.admit(req.memberId, req.identityEcdhPub);
|
|
6182
|
+
admitted.push(req.memberId);
|
|
6183
|
+
}
|
|
6184
|
+
return admitted;
|
|
6185
|
+
}
|
|
6186
|
+
/**
|
|
6187
|
+
* Rotate to a fresh epoch (rotate spaces). Mints a new group key, advances
|
|
6188
|
+
* the epoch, and re-wraps it to every member in `roster` (plus ourselves).
|
|
6189
|
+
*/
|
|
6190
|
+
async rotate(roster) {
|
|
6191
|
+
const members = roster ?? (await this.deps.transport.fetchRoster());
|
|
6192
|
+
const { epoch: nextEpoch } = await this.deps.transport.rotate(this.epoch + 1);
|
|
6193
|
+
const newKey = generateSpaceKey();
|
|
6194
|
+
this.keys.set(nextEpoch, newKey);
|
|
6195
|
+
this.epoch = nextEpoch;
|
|
6196
|
+
for (const m of members) {
|
|
6197
|
+
if (m.memberId === this.deps.memberId)
|
|
6198
|
+
continue;
|
|
6199
|
+
const recipient = await importEcdhPublicKey(m.identityEcdhPub);
|
|
6200
|
+
const wrapped = await wrapSpaceKey(newKey, nextEpoch, recipient);
|
|
6201
|
+
await this.deps.transport.postWrappedKey(m.memberId, wrapped, this.deps.memberId);
|
|
6202
|
+
}
|
|
6203
|
+
await this.persist();
|
|
6204
|
+
return nextEpoch;
|
|
6205
|
+
}
|
|
6206
|
+
/** Add `username` to a private channel's membership allowlist (member-only). */
|
|
6207
|
+
async invite(username) {
|
|
6208
|
+
await this.deps.transport.invite(username);
|
|
6209
|
+
}
|
|
6210
|
+
}
|
|
6211
|
+
|
|
6212
|
+
/**
|
|
6213
|
+
* KeyringClient — HTTP implementation of {@link KeyringTransport} against the
|
|
6214
|
+
* accelerator's per-space keyring endpoints. All blobs are opaque to the
|
|
6215
|
+
* server; this client just moves them.
|
|
6216
|
+
*
|
|
6217
|
+
* POST /api/spaces/:id/keyring/join-request { ...JoinRequest }
|
|
6218
|
+
* GET /api/spaces/:id/keyring/blobs/:member -> WrappedKey[]
|
|
6219
|
+
* POST /api/spaces/:id/keyring/wrap { targetMemberId, fromMemberId, wrapped }
|
|
6220
|
+
* GET /api/spaces/:id/keyring/pending -> JoinRequest[]
|
|
6221
|
+
* GET /api/spaces/:id/keyring/roster -> { memberId, identityEcdhPub }[]
|
|
6222
|
+
* POST /api/spaces/:id/keyring/rotate { nextEpoch } -> { epoch }
|
|
6223
|
+
* GET /api/spaces/:id/metadata -> SpaceMetadata
|
|
6224
|
+
*/
|
|
6225
|
+
class KeyringClient {
|
|
6226
|
+
deps;
|
|
6227
|
+
constructor(deps) {
|
|
6228
|
+
this.deps = deps;
|
|
6229
|
+
}
|
|
6230
|
+
url(suffix) {
|
|
6231
|
+
return `${this.deps.httpBaseUrl}/api/spaces/${encodeURIComponent(this.deps.spaceId)}${suffix}`;
|
|
6232
|
+
}
|
|
6233
|
+
async post(suffix, body) {
|
|
6234
|
+
const res = await this.deps.fetch(this.url(suffix), {
|
|
6235
|
+
method: "POST",
|
|
6236
|
+
headers: { "Content-Type": "application/json" },
|
|
6237
|
+
body: JSON.stringify(body),
|
|
6238
|
+
});
|
|
6239
|
+
if (!res.ok)
|
|
6240
|
+
throw new Error(`KeyringClient POST ${suffix}: ${res.status} ${res.statusText}`);
|
|
6241
|
+
return (await res.json().catch(() => ({})));
|
|
6242
|
+
}
|
|
6243
|
+
async get(suffix) {
|
|
6244
|
+
const res = await this.deps.fetch(this.url(suffix));
|
|
6245
|
+
if (!res.ok)
|
|
6246
|
+
throw new Error(`KeyringClient GET ${suffix}: ${res.status} ${res.statusText}`);
|
|
6247
|
+
return (await res.json());
|
|
6248
|
+
}
|
|
6249
|
+
async postJoinRequest(req) {
|
|
6250
|
+
await this.post("/keyring/join-request", req);
|
|
6251
|
+
}
|
|
6252
|
+
async fetchBlobs(memberId) {
|
|
6253
|
+
const body = await this.get(`/keyring/blobs/${encodeURIComponent(memberId)}`);
|
|
6254
|
+
return body.blobs ?? [];
|
|
6255
|
+
}
|
|
6256
|
+
async postWrappedKey(targetMemberId, wrapped, fromMemberId) {
|
|
6257
|
+
await this.post("/keyring/wrap", { targetMemberId, fromMemberId, wrapped });
|
|
6258
|
+
}
|
|
6259
|
+
async fetchPending() {
|
|
6260
|
+
const body = await this.get("/keyring/pending");
|
|
6261
|
+
return body.pending ?? [];
|
|
6262
|
+
}
|
|
6263
|
+
async fetchRoster() {
|
|
6264
|
+
const body = await this.get("/keyring/roster");
|
|
6265
|
+
return body.roster ?? [];
|
|
6266
|
+
}
|
|
6267
|
+
async rotate(nextEpoch) {
|
|
6268
|
+
return this.post("/keyring/rotate", { nextEpoch });
|
|
6269
|
+
}
|
|
6270
|
+
async invite(username) {
|
|
6271
|
+
await this.post("/keyring/invite", { username });
|
|
6272
|
+
}
|
|
6273
|
+
async fetchMetadata() {
|
|
6274
|
+
try {
|
|
6275
|
+
return await this.get("/metadata");
|
|
6276
|
+
}
|
|
6277
|
+
catch {
|
|
6278
|
+
return null;
|
|
6279
|
+
}
|
|
6280
|
+
}
|
|
6281
|
+
}
|
|
6282
|
+
|
|
6283
|
+
/**
|
|
6284
|
+
* `client.space` — the fan-out group-encryption surface.
|
|
6285
|
+
*
|
|
6286
|
+
* const space = await client.space.createSpace({ historyPolicy: "static" });
|
|
6287
|
+
* await space.sendMessage({ text: "hello" });
|
|
6288
|
+
* space.onMessage(e => …);
|
|
6289
|
+
*
|
|
6290
|
+
* const joined = await client.space.joinSpace(space.id); // newcomer
|
|
6291
|
+
*
|
|
6292
|
+
* A space owns a client-generated keypair whose public key is its id, a
|
|
6293
|
+
* symmetric group key distributed server-blind via the keyring, and a
|
|
6294
|
+
* persisted message log. This namespace wires the {@link Space} handle to the
|
|
6295
|
+
* HTTP {@link KeyringClient}, the member's identity keys (from {@link KeyStore},
|
|
6296
|
+
* shared with the Double Ratchet path), and a PersonalSpace-backed key cache.
|
|
6297
|
+
*/
|
|
6298
|
+
/** Thrown by `joinChannel` when no channel with that name is registered. */
|
|
6299
|
+
class ChannelNotFoundError extends Error {
|
|
6300
|
+
channelName;
|
|
6301
|
+
constructor(channelName) {
|
|
6302
|
+
super(`Channel "${channelName}" does not exist. Create it first.`);
|
|
6303
|
+
this.channelName = channelName;
|
|
6304
|
+
this.name = "ChannelNotFoundError";
|
|
6305
|
+
}
|
|
6306
|
+
}
|
|
6307
|
+
/** Thrown by `createChannel` when the name is already registered. */
|
|
6308
|
+
class ChannelExistsError extends Error {
|
|
6309
|
+
channelName;
|
|
6310
|
+
constructor(channelName) {
|
|
6311
|
+
super(`Channel "${channelName}" already exists.`);
|
|
6312
|
+
this.channelName = channelName;
|
|
6313
|
+
this.name = "ChannelExistsError";
|
|
6314
|
+
}
|
|
6315
|
+
}
|
|
6316
|
+
class SpaceNamespace {
|
|
6317
|
+
deps;
|
|
6318
|
+
constructor(deps) {
|
|
6319
|
+
this.deps = deps;
|
|
6320
|
+
}
|
|
6321
|
+
/**
|
|
6322
|
+
* Create a new space: generate its keypair (public key = id), register
|
|
6323
|
+
* metadata, mint the initial group key, and connect. Returns the open Space.
|
|
6324
|
+
*/
|
|
6325
|
+
async createSpace(opts = {}) {
|
|
6326
|
+
const historyPolicy = opts.historyPolicy ?? "static";
|
|
6327
|
+
const identity = await generateSpaceIdentity();
|
|
6328
|
+
// Register the space (server records pubkey + policy; the DO is lazy).
|
|
6329
|
+
// For a private space the server also records the creator (this user) as
|
|
6330
|
+
// its first member — so the metadata POST must carry the user session.
|
|
6331
|
+
await this.deps.http.post(`/api/spaces/${encodeURIComponent(identity.id)}/metadata`, { spacePubKey: identity.id, historyPolicy, visibility: opts.private ? "private" : "public" });
|
|
6332
|
+
const space = await this.build(identity.id, historyPolicy);
|
|
6333
|
+
await space.create();
|
|
6334
|
+
return space;
|
|
6335
|
+
}
|
|
6336
|
+
/** Join an existing space by id (its public key). Resolves once a key arrives. */
|
|
6337
|
+
async joinSpace(spaceId, opts = {}) {
|
|
6338
|
+
const policy = await this.fetchPolicy(spaceId);
|
|
6339
|
+
const space = await this.build(spaceId, policy);
|
|
6340
|
+
await space.join({ timeoutMs: opts.timeoutMs });
|
|
6341
|
+
return space;
|
|
6342
|
+
}
|
|
6343
|
+
/** A handle to a space without auto-connecting (uses cached keys on demand). */
|
|
6344
|
+
async get(spaceId, historyPolicy = "static") {
|
|
6345
|
+
return this.build(spaceId, historyPolicy);
|
|
6346
|
+
}
|
|
6347
|
+
// -------------------------------------------------------------------------
|
|
6348
|
+
// Channels — named, app-scoped pointers to spaces. The name → space-id
|
|
6349
|
+
// directory is app-public config (server-readable, scoped to this app via
|
|
6350
|
+
// its key); the channel CONTENTS stay fan-out E2E-encrypted. Lets chat use
|
|
6351
|
+
// human room names (`general`) instead of raw pubkey ids.
|
|
6352
|
+
// -------------------------------------------------------------------------
|
|
6353
|
+
/** List this app's channels (name → space-id) for the key's environment. */
|
|
6354
|
+
async listChannels() {
|
|
6355
|
+
const body = await this.deps.http.get("/api/app/channels");
|
|
6356
|
+
return body.channels ?? [];
|
|
6357
|
+
}
|
|
6358
|
+
/** Resolve a channel name to its space id, or null if it doesn't exist. */
|
|
6359
|
+
async resolveChannel(name) {
|
|
6360
|
+
try {
|
|
6361
|
+
const body = await this.deps.http.get(`/api/app/channels/${encodeURIComponent(name)}`);
|
|
6362
|
+
return body.spaceId ?? null;
|
|
6363
|
+
}
|
|
6364
|
+
catch (err) {
|
|
6365
|
+
if (err instanceof HttpError && err.status === 404)
|
|
6366
|
+
return null;
|
|
6367
|
+
throw err;
|
|
6368
|
+
}
|
|
6369
|
+
}
|
|
6370
|
+
/**
|
|
6371
|
+
* Create a new channel: mint a fan-out space (the caller becomes its first
|
|
6372
|
+
* key-holder) and register `name → spaceId` in the app directory. Throws
|
|
6373
|
+
* {@link ChannelExistsError} if the name is already taken.
|
|
6374
|
+
*/
|
|
6375
|
+
async createChannel(name, opts = {}) {
|
|
6376
|
+
const space = await this.createSpace(opts);
|
|
6377
|
+
// Register name → id first so a name clash doesn't leave an admitted keeper.
|
|
6378
|
+
try {
|
|
6379
|
+
await this.deps.http.post("/api/app/channels", { name, spaceId: space.id, visibility: opts.private ? "private" : "public" });
|
|
6380
|
+
}
|
|
6381
|
+
catch (err) {
|
|
6382
|
+
if (err instanceof HttpError && err.status === 409) {
|
|
6383
|
+
throw new ChannelExistsError(name);
|
|
6384
|
+
}
|
|
6385
|
+
throw err;
|
|
6386
|
+
}
|
|
6387
|
+
// Admit the app keeper so the channel is joinable with no member online.
|
|
6388
|
+
// Best-effort: a channel still works (members admit each other) without it.
|
|
6389
|
+
try {
|
|
6390
|
+
const { pubkey } = await this.deps.http.get("/api/app/keeper");
|
|
6391
|
+
if (pubkey && space.keyring)
|
|
6392
|
+
await space.keyring.admit(KEEPER_MEMBER_ID, pubkey);
|
|
6393
|
+
}
|
|
6394
|
+
catch (err) {
|
|
6395
|
+
console.warn("[client.space] keeper admit failed (channel still usable):", err);
|
|
6396
|
+
}
|
|
6397
|
+
return space;
|
|
6398
|
+
}
|
|
6399
|
+
/**
|
|
6400
|
+
* Join an existing channel by name. Throws {@link ChannelNotFoundError} if
|
|
6401
|
+
* no channel with that name is registered (use {@link createChannel} first).
|
|
6402
|
+
*/
|
|
6403
|
+
async joinChannel(name, opts = {}) {
|
|
6404
|
+
const spaceId = await this.resolveChannel(name);
|
|
6405
|
+
if (!spaceId)
|
|
6406
|
+
throw new ChannelNotFoundError(name);
|
|
6407
|
+
return this.joinSpace(spaceId, opts);
|
|
6408
|
+
}
|
|
6409
|
+
// -------------------------------------------------------------------------
|
|
6410
|
+
// Internals
|
|
6411
|
+
// -------------------------------------------------------------------------
|
|
6412
|
+
async build(spaceId, historyPolicy) {
|
|
6413
|
+
const memberId = this.myId();
|
|
6414
|
+
const { ecdhPub, ecdsaPub } = await this.ensureIdentity(memberId);
|
|
6415
|
+
const transport = new KeyringClient({
|
|
6416
|
+
spaceId,
|
|
6417
|
+
httpBaseUrl: this.deps.http.baseUrl,
|
|
6418
|
+
fetch: this.deps.http.fetch,
|
|
6419
|
+
});
|
|
6420
|
+
const keyring = new SpaceKeyring({
|
|
6421
|
+
spaceId,
|
|
6422
|
+
memberId,
|
|
6423
|
+
identityEcdhPub: ecdhPub,
|
|
6424
|
+
identityEcdsaPub: ecdsaPub,
|
|
6425
|
+
ownPrivateKey: () => KeyStore.getInstance().getKeyPair(memberId)?.privateKey ?? null,
|
|
6426
|
+
transport,
|
|
6427
|
+
cache: this.deps.cache,
|
|
6428
|
+
historyPolicy,
|
|
6429
|
+
});
|
|
6430
|
+
return new Space({
|
|
6431
|
+
name: spaceId,
|
|
6432
|
+
wsBaseUrl: this.deps.wsBaseUrl,
|
|
6433
|
+
httpBaseUrl: this.deps.http.baseUrl,
|
|
6434
|
+
fetch: this.deps.http.fetch,
|
|
6435
|
+
myId: () => memberId,
|
|
6436
|
+
fetchTicket: () => this.fetchTicket(),
|
|
6437
|
+
keyring,
|
|
6438
|
+
historyPolicy,
|
|
6439
|
+
});
|
|
6440
|
+
}
|
|
6441
|
+
/** Ensure the member has an identity keypair; return its public ECDH + ECDSA keys. */
|
|
6442
|
+
async ensureIdentity(memberId) {
|
|
6443
|
+
const store = KeyStore.getInstance();
|
|
6444
|
+
if (!store.getKeyPair(memberId)) {
|
|
6445
|
+
await store.generateOwnKeyPair(memberId);
|
|
6446
|
+
}
|
|
6447
|
+
const ecdhPub = await exportEcdhPublicKey(store.getKeyPair(memberId).publicKey);
|
|
6448
|
+
const ecdsaPub = await exportEcdsaPublicKey(store.getAuthKeyPair(memberId).publicKey);
|
|
6449
|
+
return { ecdhPub, ecdsaPub };
|
|
6450
|
+
}
|
|
6451
|
+
async fetchPolicy(spaceId) {
|
|
6452
|
+
const meta = await new KeyringClient({
|
|
6453
|
+
spaceId,
|
|
6454
|
+
httpBaseUrl: this.deps.http.baseUrl,
|
|
6455
|
+
fetch: this.deps.http.fetch,
|
|
6456
|
+
}).fetchMetadata();
|
|
6457
|
+
return meta?.historyPolicy ?? "static";
|
|
6458
|
+
}
|
|
6459
|
+
async fetchTicket() {
|
|
6460
|
+
try {
|
|
6461
|
+
const res = await this.deps.http.post("/api/ws-ticket", {});
|
|
6462
|
+
return res?.ticket ?? null;
|
|
6463
|
+
}
|
|
6464
|
+
catch {
|
|
6465
|
+
return null;
|
|
6466
|
+
}
|
|
6467
|
+
}
|
|
6468
|
+
myId() {
|
|
6469
|
+
const id = this.deps.session.username;
|
|
6470
|
+
if (!id)
|
|
6471
|
+
throw new Error("client.space: not signed in — call client.auth.zk.login() first.");
|
|
6472
|
+
return id;
|
|
6473
|
+
}
|
|
6474
|
+
}
|
|
6475
|
+
|
|
5540
6476
|
/**
|
|
5541
6477
|
* `Client` — the single entry point to the Muhkoo SDK.
|
|
5542
6478
|
*
|
|
@@ -5577,6 +6513,8 @@ class Client {
|
|
|
5577
6513
|
storage;
|
|
5578
6514
|
/** Realtime messaging — `client.message.subscribe(...)`, etc. */
|
|
5579
6515
|
message;
|
|
6516
|
+
/** Fan-out group spaces — `client.space.createSpace(...)`, etc. */
|
|
6517
|
+
space;
|
|
5580
6518
|
session;
|
|
5581
6519
|
http;
|
|
5582
6520
|
constructor(options = {}) {
|
|
@@ -5597,6 +6535,19 @@ class Client {
|
|
|
5597
6535
|
this.auth = new AuthNamespace({ auth: authClient, circuits, session: this.session });
|
|
5598
6536
|
this.storage = new StorageNamespace({ http: this.http, session: this.session, wsBaseUrl });
|
|
5599
6537
|
this.message = new MessageNamespace({ http: this.http, session: this.session, wsBaseUrl });
|
|
6538
|
+
// Group keys are cached in the user's PersonalSpace (encrypted at rest
|
|
6539
|
+
// by StorageNamespace), so a returning member hydrates without a fresh
|
|
6540
|
+
// keyring round-trip. The server only ever sees the ciphertext.
|
|
6541
|
+
const spaceKeyCache = {
|
|
6542
|
+
loadKeys: (spaceId) => this.storage.get("space-keys", spaceId),
|
|
6543
|
+
saveKeys: (spaceId, keys) => this.storage.set("space-keys", spaceId, keys),
|
|
6544
|
+
};
|
|
6545
|
+
this.space = new SpaceNamespace({
|
|
6546
|
+
http: this.http,
|
|
6547
|
+
session: this.session,
|
|
6548
|
+
wsBaseUrl,
|
|
6549
|
+
cache: spaceKeyCache,
|
|
6550
|
+
});
|
|
5600
6551
|
}
|
|
5601
6552
|
/** The currently signed-in user, or `null`. */
|
|
5602
6553
|
get user() {
|
|
@@ -5608,6 +6559,48 @@ class Client {
|
|
|
5608
6559
|
}
|
|
5609
6560
|
}
|
|
5610
6561
|
|
|
6562
|
+
/**
|
|
6563
|
+
* PacketCipher — the strategy `Network` uses to seal/open a packet's payload.
|
|
6564
|
+
*
|
|
6565
|
+
* `Network` owns the wire shape (Message ↔ Packet, header placement, transport,
|
|
6566
|
+
* reconnect, checksum). The *encryption* is pluggable: the default
|
|
6567
|
+
* {@link DoubleRatchetCipher} reproduces the per-peer Double Ratchet behavior
|
|
6568
|
+
* Network has always had, while the Space layer supplies a group-key cipher.
|
|
6569
|
+
*
|
|
6570
|
+
* The sealed ciphertext always rides in `Packet.headers` (cleartext routing
|
|
6571
|
+
* fields stay readable to the server); `Packet.message` is left undefined on
|
|
6572
|
+
* the wire so the server never sees the real payload.
|
|
6573
|
+
*/
|
|
6574
|
+
/**
|
|
6575
|
+
* Default cipher: per-peer Double Ratchet. Reproduces Network's original
|
|
6576
|
+
* `{ encrypted: true, cipherMessage: <json> }` header convention exactly so
|
|
6577
|
+
* existing consumers are unaffected.
|
|
6578
|
+
*/
|
|
6579
|
+
class DoubleRatchetCipher {
|
|
6580
|
+
deps;
|
|
6581
|
+
constructor(deps) {
|
|
6582
|
+
this.deps = deps;
|
|
6583
|
+
}
|
|
6584
|
+
async seal(serializedMessage) {
|
|
6585
|
+
const sessionId = this.deps.getSessionId();
|
|
6586
|
+
if (!sessionId) {
|
|
6587
|
+
throw new Error("DoubleRatchetCipher: no active session to seal under");
|
|
6588
|
+
}
|
|
6589
|
+
const cipherMessage = await this.deps.ratchetManager.encrypt(this.deps.clientId, this.deps.serverId, sessionId, serializedMessage, false, // newDhKey — let the ratchet manage key rotation
|
|
6590
|
+
this.deps.sessionType);
|
|
6591
|
+
return { encrypted: true, cipherMessage: JSON.stringify(cipherMessage) };
|
|
6592
|
+
}
|
|
6593
|
+
handles(headers) {
|
|
6594
|
+
return !!(headers?.encrypted && headers?.cipherMessage);
|
|
6595
|
+
}
|
|
6596
|
+
async open(headers) {
|
|
6597
|
+
if (!this.handles(headers))
|
|
6598
|
+
return null;
|
|
6599
|
+
const cipherMessage = JSON.parse(headers.cipherMessage);
|
|
6600
|
+
return this.deps.ratchetManager.decrypt(cipherMessage, this.deps.isClient ?? true);
|
|
6601
|
+
}
|
|
6602
|
+
}
|
|
6603
|
+
|
|
5611
6604
|
const appLogger$1 = new Logger("connect", 'ERROR');
|
|
5612
6605
|
globalThis.appLogger = appLogger$1;
|
|
5613
6606
|
|
|
@@ -5632,5 +6625,5 @@ var bundledLoader = /*#__PURE__*/Object.freeze({
|
|
|
5632
6625
|
default: loadRsWasm
|
|
5633
6626
|
});
|
|
5634
6627
|
|
|
5635
|
-
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 };
|
|
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 };
|
|
5636
6629
|
//# sourceMappingURL=index.js.map
|