@muhkoo/connect 0.1.0-alpha.1 → 0.2.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/dist/browser/index.js +905 -49
- package/dist/browser/index.js.map +1 -1
- package/dist/connect.d.ts +496 -28
- package/dist/server/index.js +901 -49
- package/dist/server/index.js.map +1 -1
- package/dist/workers/index.js +21 -21
- package/dist/workers/index.js.map +1 -1
- package/package.json +8 -2
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, "/") +
|
|
@@ -2856,28 +2860,28 @@ async function verifyGroth16(wasmInstance, memory, initialPFree, verificationKey
|
|
|
2856
2860
|
if (signal >= r)
|
|
2857
2861
|
return false;
|
|
2858
2862
|
}
|
|
2859
|
-
const exports
|
|
2863
|
+
const exports = wasmInstance.exports;
|
|
2860
2864
|
const u8 = new Uint8Array(memory.buffer);
|
|
2861
2865
|
const u32 = new Uint32Array(memory.buffer);
|
|
2862
2866
|
// Reset the WASM allocator's heap pointer between invocations
|
|
2863
2867
|
u32[0] = initialPFree;
|
|
2864
|
-
const g1m_add = exports
|
|
2865
|
-
const g1m_timesScalar = exports
|
|
2866
|
-
const g1m_neg = exports
|
|
2867
|
-
const g1m_inCurve = exports
|
|
2868
|
-
const g2m_inCurve = exports
|
|
2869
|
-
const ftm_one = exports
|
|
2870
|
-
const bn128_pairingEq4 = exports
|
|
2868
|
+
const g1m_add = exports["g1m_add"];
|
|
2869
|
+
const g1m_timesScalar = exports["g1m_timesScalar"];
|
|
2870
|
+
const g1m_neg = exports["g1m_neg"];
|
|
2871
|
+
const g1m_inCurve = exports["g1m_inCurve"];
|
|
2872
|
+
const g2m_inCurve = exports["g2m_inCurve"];
|
|
2873
|
+
const ftm_one = exports["ftm_one"];
|
|
2874
|
+
const bn128_pairingEq4 = exports["bn128_pairingEq4"];
|
|
2871
2875
|
// Compute vk_x = IC[0] + sum(IC[i+1] * signal[i])
|
|
2872
2876
|
const pVkX = alloc(u32, g1size);
|
|
2873
2877
|
const pIC0 = alloc(u32, g1size);
|
|
2874
|
-
writeG1Point(u8, pIC0, vk.IC[0], exports
|
|
2878
|
+
writeG1Point(u8, pIC0, vk.IC[0], exports);
|
|
2875
2879
|
u8.set(u8.slice(pIC0, pIC0 + g1size), pVkX);
|
|
2876
2880
|
const pTemp = alloc(u32, g1size);
|
|
2877
2881
|
const pScalar = alloc(u32, n8r);
|
|
2878
2882
|
for (let i = 0; i < signals.length; i++) {
|
|
2879
2883
|
const pICi = alloc(u32, g1size);
|
|
2880
|
-
writeG1Point(u8, pICi, vk.IC[i + 1], exports
|
|
2884
|
+
writeG1Point(u8, pICi, vk.IC[i + 1], exports);
|
|
2881
2885
|
u8.set(bigIntToLE(signals[i], n8r), pScalar);
|
|
2882
2886
|
g1m_timesScalar(pICi, pScalar, n8r, pTemp);
|
|
2883
2887
|
const pSum = alloc(u32, g1size);
|
|
@@ -2889,9 +2893,9 @@ async function verifyGroth16(wasmInstance, memory, initialPFree, verificationKey
|
|
|
2889
2893
|
const pNegA = alloc(u32, g1size);
|
|
2890
2894
|
const pB = alloc(u32, g2size);
|
|
2891
2895
|
const pC = alloc(u32, g1size);
|
|
2892
|
-
writeG1Point(u8, pA, p.pi_a, exports
|
|
2893
|
-
writeG2Point(u8, pB, p.pi_b, exports
|
|
2894
|
-
writeG1Point(u8, pC, p.pi_c, exports
|
|
2896
|
+
writeG1Point(u8, pA, p.pi_a, exports);
|
|
2897
|
+
writeG2Point(u8, pB, p.pi_b, exports);
|
|
2898
|
+
writeG1Point(u8, pC, p.pi_c, exports);
|
|
2895
2899
|
// Subgroup / on-curve checks
|
|
2896
2900
|
if (!g1m_inCurve(pA))
|
|
2897
2901
|
return false;
|
|
@@ -2905,10 +2909,10 @@ async function verifyGroth16(wasmInstance, memory, initialPFree, verificationKey
|
|
|
2905
2909
|
const pBeta = alloc(u32, g2size);
|
|
2906
2910
|
const pGamma = alloc(u32, g2size);
|
|
2907
2911
|
const pDelta = alloc(u32, g2size);
|
|
2908
|
-
writeG1Point(u8, pAlpha, vk.vk_alpha_1, exports
|
|
2909
|
-
writeG2Point(u8, pBeta, vk.vk_beta_2, exports
|
|
2910
|
-
writeG2Point(u8, pGamma, vk.vk_gamma_2, exports
|
|
2911
|
-
writeG2Point(u8, pDelta, vk.vk_delta_2, exports
|
|
2912
|
+
writeG1Point(u8, pAlpha, vk.vk_alpha_1, exports);
|
|
2913
|
+
writeG2Point(u8, pBeta, vk.vk_beta_2, exports);
|
|
2914
|
+
writeG2Point(u8, pGamma, vk.vk_gamma_2, exports);
|
|
2915
|
+
writeG2Point(u8, pDelta, vk.vk_delta_2, exports);
|
|
2912
2916
|
// Target: F12 element representing 1
|
|
2913
2917
|
const pOne = alloc(u32, ftsize);
|
|
2914
2918
|
ftm_one(pOne);
|
|
@@ -2976,19 +2980,19 @@ function validateG1InField(point) {
|
|
|
2976
2980
|
const y = typeof point[1] === "bigint" ? point[1] : BigInt(String(point[1]));
|
|
2977
2981
|
return x < q && y < q;
|
|
2978
2982
|
}
|
|
2979
|
-
function writeG1Point(u8, offset, point, exports
|
|
2983
|
+
function writeG1Point(u8, offset, point, exports) {
|
|
2980
2984
|
const x = point[0];
|
|
2981
2985
|
const y = point[1];
|
|
2982
2986
|
const z = point.length > 2 ? point[2] : 1n;
|
|
2983
2987
|
u8.set(bigIntToLE(x, f1size), offset);
|
|
2984
2988
|
u8.set(bigIntToLE(y, f1size), offset + f1size);
|
|
2985
2989
|
u8.set(bigIntToLE(z, f1size), offset + 2 * f1size);
|
|
2986
|
-
const f1m_toMontgomery = exports
|
|
2990
|
+
const f1m_toMontgomery = exports["f1m_toMontgomery"];
|
|
2987
2991
|
f1m_toMontgomery(offset, offset);
|
|
2988
2992
|
f1m_toMontgomery(offset + f1size, offset + f1size);
|
|
2989
2993
|
f1m_toMontgomery(offset + 2 * f1size, offset + 2 * f1size);
|
|
2990
2994
|
}
|
|
2991
|
-
function writeG2Point(u8, offset, point, exports
|
|
2995
|
+
function writeG2Point(u8, offset, point, exports) {
|
|
2992
2996
|
const x = point[0];
|
|
2993
2997
|
const y = point[1];
|
|
2994
2998
|
const z = point.length > 2 ? point[2] : [1n, 0n];
|
|
@@ -2998,7 +3002,7 @@ function writeG2Point(u8, offset, point, exports$1) {
|
|
|
2998
3002
|
u8.set(bigIntToLE(y[1], f1size), offset + 3 * f1size);
|
|
2999
3003
|
u8.set(bigIntToLE(z[0], f1size), offset + 4 * f1size);
|
|
3000
3004
|
u8.set(bigIntToLE(z[1], f1size), offset + 5 * f1size);
|
|
3001
|
-
const f1m_toMontgomery = exports
|
|
3005
|
+
const f1m_toMontgomery = exports["f1m_toMontgomery"];
|
|
3002
3006
|
for (let i = 0; i < 6; i++) {
|
|
3003
3007
|
f1m_toMontgomery(offset + i * f1size, offset + i * f1size);
|
|
3004
3008
|
}
|
|
@@ -5259,50 +5263,229 @@ 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
|
+
/** Generate a fresh space identity keypair (P-384 ECDH). */
|
|
5309
|
+
async function generateSpaceIdentity() {
|
|
5310
|
+
const kp = await getSubtle().generateKey(ECDH_PARAMS, true, ["deriveBits"]);
|
|
5311
|
+
const id = await encodeSpaceId(kp.publicKey);
|
|
5312
|
+
return { id, publicKey: kp.publicKey, privateKey: kp.privateKey };
|
|
5313
|
+
}
|
|
5314
|
+
// ---------------------------------------------------------------------------
|
|
5315
|
+
// Group key
|
|
5316
|
+
// ---------------------------------------------------------------------------
|
|
5317
|
+
/** Mint a fresh 256-bit group key for an epoch. */
|
|
5318
|
+
function generateSpaceKey() {
|
|
5319
|
+
return randomBytes(AES_GCM_KEY_BYTES);
|
|
5320
|
+
}
|
|
5321
|
+
// ---------------------------------------------------------------------------
|
|
5322
|
+
// ECIES wrap / unwrap of the group key
|
|
5323
|
+
// ---------------------------------------------------------------------------
|
|
5324
|
+
/**
|
|
5325
|
+
* Wrap `kSpace` for `recipientEcdhPub` using an ephemeral ECDH keypair.
|
|
5326
|
+
* The HKDF salt binds the wrap key to `epoch`, so a wrap minted for one epoch
|
|
5327
|
+
* cannot be replayed to unwrap a different epoch's blob.
|
|
5328
|
+
*/
|
|
5329
|
+
async function wrapSpaceKey(kSpace, epoch, recipientEcdhPub) {
|
|
5330
|
+
const eph = await getSubtle().generateKey(ECDH_PARAMS, true, ["deriveBits"]);
|
|
5331
|
+
const shared = new Uint8Array(await getSubtle().deriveBits({ name: "ECDH", public: recipientEcdhPub }, eph.privateKey, P384_BITS));
|
|
5332
|
+
const wrapKey = await deriveBitsHkdf(shared, WRAP_INFO, AES_GCM_KEY_BYTES, utf8Encode(`epoch:${epoch}`));
|
|
5333
|
+
const iv = randomBytes(AES_GCM_IV_BYTES);
|
|
5334
|
+
const ciphertext = await encryptAesGcm(wrapKey, iv, kSpace);
|
|
5335
|
+
return {
|
|
5336
|
+
epoch,
|
|
5337
|
+
ephemeralPub: await exportEcdhPublicKey(eph.publicKey),
|
|
5338
|
+
iv: toBase64(iv),
|
|
5339
|
+
ciphertext: toBase64(ciphertext),
|
|
5340
|
+
alg: WRAP_ALG,
|
|
5341
|
+
};
|
|
5342
|
+
}
|
|
5343
|
+
/**
|
|
5344
|
+
* Unwrap a {@link WrappedKey} using the recipient's identity ECDH private key.
|
|
5345
|
+
* Throws (GCM tag failure) on the wrong private key or a tampered blob.
|
|
5346
|
+
*/
|
|
5347
|
+
async function unwrapSpaceKey(wrapped, ownEcdhPriv) {
|
|
5348
|
+
const ephPub = await importEcdhPublicKey(wrapped.ephemeralPub);
|
|
5349
|
+
const shared = new Uint8Array(await getSubtle().deriveBits({ name: "ECDH", public: ephPub }, ownEcdhPriv, P384_BITS));
|
|
5350
|
+
const wrapKey = await deriveBitsHkdf(shared, WRAP_INFO, AES_GCM_KEY_BYTES, utf8Encode(`epoch:${wrapped.epoch}`));
|
|
5351
|
+
return decryptAesGcm(wrapKey, fromBase64(wrapped.iv), fromBase64(wrapped.ciphertext));
|
|
5352
|
+
}
|
|
5353
|
+
// ---------------------------------------------------------------------------
|
|
5354
|
+
// Message sealing
|
|
5355
|
+
// ---------------------------------------------------------------------------
|
|
5356
|
+
/** Seal an already-serialized message string under the group key. */
|
|
5357
|
+
async function sealSerialized(kSpace, serialized) {
|
|
5358
|
+
const iv = randomBytes(AES_GCM_IV_BYTES);
|
|
5359
|
+
const ciphertext = await encryptAesGcm(kSpace, iv, utf8Encode(serialized));
|
|
5360
|
+
return { iv: toBase64(iv), ciphertext: toBase64(ciphertext) };
|
|
5361
|
+
}
|
|
5362
|
+
/**
|
|
5363
|
+
* Open a sealed payload back into its serialized message string. Throws (GCM
|
|
5364
|
+
* tag failure) on the wrong group key or a tampered ciphertext.
|
|
5365
|
+
*/
|
|
5366
|
+
async function openSerialized(kSpace, iv, ciphertext) {
|
|
5367
|
+
const plaintext = await decryptAesGcm(kSpace, fromBase64(iv), fromBase64(ciphertext));
|
|
5368
|
+
return utf8Decode(plaintext);
|
|
5369
|
+
}
|
|
5370
|
+
|
|
5371
|
+
/**
|
|
5372
|
+
* SpacePacketCipher — the {@link PacketCipher} that seals packets with a
|
|
5373
|
+
* space's group key instead of a per-peer Double Ratchet.
|
|
5374
|
+
*
|
|
5375
|
+
* It pulls the active epoch's key from an {@link EpochKeyProvider} (the
|
|
5376
|
+
* SpaceKeyring), so this adapter stays decoupled from the keyring's transport
|
|
5377
|
+
* and caching concerns — it only needs "what key seals epoch N".
|
|
5278
5378
|
*/
|
|
5279
|
-
class
|
|
5379
|
+
class SpacePacketCipher {
|
|
5380
|
+
keys;
|
|
5381
|
+
contentType;
|
|
5382
|
+
constructor(keys, contentType) {
|
|
5383
|
+
this.keys = keys;
|
|
5384
|
+
this.contentType = contentType;
|
|
5385
|
+
}
|
|
5386
|
+
async seal(serializedMessage) {
|
|
5387
|
+
const epoch = this.keys.currentEpoch();
|
|
5388
|
+
const key = this.keys.keyForEpoch(epoch);
|
|
5389
|
+
if (!key) {
|
|
5390
|
+
throw new Error(`SpacePacketCipher: no group key for epoch ${epoch}`);
|
|
5391
|
+
}
|
|
5392
|
+
const { iv, ciphertext } = await sealSerialized(key, serializedMessage);
|
|
5393
|
+
const headers = { spaceSealed: true, epoch, iv, ciphertext };
|
|
5394
|
+
if (this.contentType)
|
|
5395
|
+
headers.contentType = this.contentType;
|
|
5396
|
+
return headers;
|
|
5397
|
+
}
|
|
5398
|
+
handles(headers) {
|
|
5399
|
+
return headers?.spaceSealed === true &&
|
|
5400
|
+
typeof headers?.iv === "string" &&
|
|
5401
|
+
typeof headers?.ciphertext === "string";
|
|
5402
|
+
}
|
|
5403
|
+
/** Returns null for epochs we don't hold (surfaced as undecryptable). */
|
|
5404
|
+
async open(headers) {
|
|
5405
|
+
if (!this.handles(headers))
|
|
5406
|
+
return null;
|
|
5407
|
+
const epoch = Number(headers.epoch);
|
|
5408
|
+
const key = this.keys.keyForEpoch(epoch);
|
|
5409
|
+
if (!key)
|
|
5410
|
+
return null;
|
|
5411
|
+
return openSerialized(key, headers.iv, headers.ciphertext);
|
|
5412
|
+
}
|
|
5413
|
+
}
|
|
5414
|
+
|
|
5415
|
+
/**
|
|
5416
|
+
* `Space` — a client handle for one shared space.
|
|
5417
|
+
*
|
|
5418
|
+
* A space is the unit everything is built on. It carries:
|
|
5419
|
+
* - **realtime transport** over a {@link BroadcastChannel} (group handshake,
|
|
5420
|
+
* roster frames, raw-frame passthrough);
|
|
5421
|
+
* - **fan-out group messaging** — `Message`s sealed once with the space's
|
|
5422
|
+
* group key (via {@link SpacePacketCipher}), wrapped in a cleartext
|
|
5423
|
+
* {@link Packet} envelope, broadcast AND persisted by the server as
|
|
5424
|
+
* history (the `spaceMessage` frame);
|
|
5425
|
+
* - **room-scoped file storage** via {@link FileStorage} over the shard
|
|
5426
|
+
* endpoint;
|
|
5427
|
+
* - **legacy per-peer Double Ratchet messaging** (`send`/`announce`) for the
|
|
5428
|
+
* ephemeral, no-history path — unchanged so existing callers (the chat hook
|
|
5429
|
+
* reached via `client.message.room(name)`) keep working.
|
|
5430
|
+
*
|
|
5431
|
+
* `Space` was formerly `Room`; `Room` remains exported as an alias. The
|
|
5432
|
+
* fan-out methods (`sendMessage`/`onMessage`/`history`/`join`/`admit`/`rotate`)
|
|
5433
|
+
* require a {@link SpaceKeyring} dep; without one only the transport + DR +
|
|
5434
|
+
* file surface is available.
|
|
5435
|
+
*/
|
|
5436
|
+
class Space {
|
|
5280
5437
|
deps;
|
|
5281
5438
|
name;
|
|
5439
|
+
historyPolicy;
|
|
5282
5440
|
channel = null;
|
|
5283
5441
|
pending = [];
|
|
5442
|
+
cipher;
|
|
5443
|
+
/** Per-instance event bus for fan-out events (isolated across spaces). */
|
|
5444
|
+
events = new EventTarget();
|
|
5445
|
+
rawWired = false;
|
|
5284
5446
|
constructor(deps) {
|
|
5285
5447
|
this.deps = deps;
|
|
5286
5448
|
this.name = deps.name;
|
|
5449
|
+
this.historyPolicy = deps.historyPolicy ?? "static";
|
|
5450
|
+
this.cipher = deps.keyring ? new SpacePacketCipher(deps.keyring) : null;
|
|
5287
5451
|
}
|
|
5288
|
-
|
|
5289
|
-
|
|
5452
|
+
/** The encoded space id (same as the transport name for fan-out spaces). */
|
|
5453
|
+
get id() {
|
|
5454
|
+
return this.name;
|
|
5455
|
+
}
|
|
5456
|
+
/** The keyring backing this space's group keys, if any. */
|
|
5457
|
+
get keyring() {
|
|
5458
|
+
return this.deps.keyring;
|
|
5459
|
+
}
|
|
5460
|
+
// -- transport -------------------------------------------------------------
|
|
5461
|
+
/** Open the space websocket (resolves the ticket + identity lazily). */
|
|
5290
5462
|
async connect() {
|
|
5291
5463
|
if (!this.channel) {
|
|
5292
5464
|
const ticket = await this.deps.fetchTicket();
|
|
5293
5465
|
const url = `${this.deps.wsBaseUrl}/api/spaces/${encodeURIComponent(this.name)}/websocket` +
|
|
5294
5466
|
(ticket ? `?ticket=${encodeURIComponent(ticket)}` : "");
|
|
5295
|
-
this.channel =
|
|
5467
|
+
this.channel = this.deps.createChannel
|
|
5468
|
+
? this.deps.createChannel(url, this.deps.myId())
|
|
5469
|
+
: new BroadcastChannel({ url, myId: this.deps.myId(), autoAnnounce: false });
|
|
5296
5470
|
for (const [event, handler] of this.pending)
|
|
5297
5471
|
this.channel.on(event, handler);
|
|
5298
5472
|
this.pending.length = 0;
|
|
5473
|
+
// Fan-out spaces drive their own `{name}` handshake + frame routing.
|
|
5474
|
+
// Legacy (keyring-less) callers wire these themselves, so leave that
|
|
5475
|
+
// path byte-for-byte unchanged.
|
|
5476
|
+
if (this.deps.keyring) {
|
|
5477
|
+
this.channel.on(BroadcastChannelEvents.CONNECTED, () => {
|
|
5478
|
+
this.channel?.sendRaw({ name: this.deps.myId() });
|
|
5479
|
+
});
|
|
5480
|
+
this.wireFanout();
|
|
5481
|
+
}
|
|
5299
5482
|
}
|
|
5300
5483
|
await this.channel.connect();
|
|
5301
5484
|
}
|
|
5302
5485
|
disconnect() {
|
|
5303
5486
|
this.channel?.disconnect();
|
|
5304
5487
|
}
|
|
5305
|
-
/** Subscribe to a channel event. Buffered until `connect()` if early. */
|
|
5488
|
+
/** Subscribe to a raw channel event. Buffered until `connect()` if early. */
|
|
5306
5489
|
on(event, handler) {
|
|
5307
5490
|
if (this.channel)
|
|
5308
5491
|
this.channel.on(event, handler);
|
|
@@ -5318,11 +5501,12 @@ class Room {
|
|
|
5318
5501
|
this.pending.splice(i, 1);
|
|
5319
5502
|
}
|
|
5320
5503
|
}
|
|
5504
|
+
// -- legacy Double Ratchet path (unchanged) --------------------------------
|
|
5321
5505
|
/** Advertise our keyExchange to the room (idempotent per connection). */
|
|
5322
5506
|
announce() {
|
|
5323
5507
|
return this.channel ? this.channel.announce() : Promise.resolve();
|
|
5324
5508
|
}
|
|
5325
|
-
/** E2E-encrypt `text` to every peer
|
|
5509
|
+
/** E2E-encrypt `text` to every peer via the Double Ratchet (no history). */
|
|
5326
5510
|
send(text) {
|
|
5327
5511
|
return this.channel ? this.channel.send(text) : Promise.resolve(0);
|
|
5328
5512
|
}
|
|
@@ -5331,21 +5515,113 @@ class Room {
|
|
|
5331
5515
|
this.channel?.sendRaw(frame);
|
|
5332
5516
|
}
|
|
5333
5517
|
peers() {
|
|
5334
|
-
return this.channel ? this.channel.peers() : [];
|
|
5518
|
+
return this.channel?.peers ? this.channel.peers() : [];
|
|
5335
5519
|
}
|
|
5336
5520
|
isConnected() {
|
|
5337
|
-
return this.channel ? this.channel.isConnected() : false;
|
|
5521
|
+
return this.channel?.isConnected ? this.channel.isConnected() : false;
|
|
5338
5522
|
}
|
|
5339
5523
|
/** The underlying channel, for advanced use. `null` before `connect()`. */
|
|
5340
5524
|
get raw() {
|
|
5341
5525
|
return this.channel;
|
|
5342
5526
|
}
|
|
5527
|
+
// -- fan-out group messaging -----------------------------------------------
|
|
5528
|
+
/**
|
|
5529
|
+
* Seal `payload` once with the current group key and broadcast it as a
|
|
5530
|
+
* persisted `spaceMessage`. Requires a keyring.
|
|
5531
|
+
*/
|
|
5532
|
+
async sendMessage(payload, opts = {}) {
|
|
5533
|
+
const cipher = this.requireCipher(opts.contentType);
|
|
5534
|
+
const message = new Message(payload);
|
|
5535
|
+
const headers = await cipher.seal(message.serialize());
|
|
5536
|
+
const packet = new Packet({
|
|
5537
|
+
subject: opts.channel ?? "default",
|
|
5538
|
+
source: this.deps.myId(),
|
|
5539
|
+
target: this.name,
|
|
5540
|
+
headers,
|
|
5541
|
+
});
|
|
5542
|
+
this.sendRaw({ spaceMessage: packet.serialize() });
|
|
5543
|
+
}
|
|
5544
|
+
/** Subscribe to decrypted fan-out messages. Returns an unsubscribe fn. */
|
|
5545
|
+
onMessage(handler) {
|
|
5546
|
+
const listener = (e) => handler(e.detail);
|
|
5547
|
+
this.events.addEventListener("message", listener);
|
|
5548
|
+
return () => this.events.removeEventListener("message", listener);
|
|
5549
|
+
}
|
|
5550
|
+
/** Subscribe to inbound join requests (for a key-holder to admit). */
|
|
5551
|
+
onJoinRequest(handler) {
|
|
5552
|
+
const listener = (e) => handler(e.detail);
|
|
5553
|
+
this.events.addEventListener("join-request", listener);
|
|
5554
|
+
return () => this.events.removeEventListener("join-request", listener);
|
|
5555
|
+
}
|
|
5556
|
+
/**
|
|
5557
|
+
* Fetch + decrypt persisted history beyond the live backlog.
|
|
5558
|
+
* `before` is an opaque cursor returned as `nextCursor`.
|
|
5559
|
+
*/
|
|
5560
|
+
async history(opts = {}) {
|
|
5561
|
+
const cipher = this.requireCipher();
|
|
5562
|
+
const params = new URLSearchParams();
|
|
5563
|
+
if (opts.before)
|
|
5564
|
+
params.set("before", opts.before);
|
|
5565
|
+
if (opts.limit)
|
|
5566
|
+
params.set("limit", String(opts.limit));
|
|
5567
|
+
const url = `${this.deps.httpBaseUrl}/api/spaces/${encodeURIComponent(this.name)}/history` +
|
|
5568
|
+
(params.toString() ? `?${params}` : "");
|
|
5569
|
+
const res = await this.deps.fetch(url);
|
|
5570
|
+
if (!res.ok)
|
|
5571
|
+
throw new Error(`Space.history: ${res.status} ${res.statusText}`);
|
|
5572
|
+
const body = (await res.json());
|
|
5573
|
+
const messages = [];
|
|
5574
|
+
for (const raw of body.messages ?? []) {
|
|
5575
|
+
const decoded = await this.decodeFrame(raw, cipher);
|
|
5576
|
+
if (decoded)
|
|
5577
|
+
messages.push(decoded);
|
|
5578
|
+
}
|
|
5579
|
+
return { messages, nextCursor: body.nextCursor ?? null };
|
|
5580
|
+
}
|
|
5581
|
+
/**
|
|
5582
|
+
* Bring this member's group keys up to date: load from cache, and if we
|
|
5583
|
+
* hold nothing, request a key and poll until one arrives (or timeout).
|
|
5584
|
+
*/
|
|
5585
|
+
async join(opts = {}) {
|
|
5586
|
+
const keyring = this.requireKeyring();
|
|
5587
|
+
await this.connect();
|
|
5588
|
+
if (await keyring.loadFromCache())
|
|
5589
|
+
return true;
|
|
5590
|
+
if (keyring.hasAnyKey())
|
|
5591
|
+
return true;
|
|
5592
|
+
await keyring.requestKey();
|
|
5593
|
+
const deadline = Date.now() + (opts.timeoutMs ?? 15000);
|
|
5594
|
+
const pollMs = opts.pollMs ?? 500;
|
|
5595
|
+
// eslint-disable-next-line no-constant-condition
|
|
5596
|
+
while (Date.now() < deadline) {
|
|
5597
|
+
if ((await keyring.pullKeys()) > 0)
|
|
5598
|
+
return true;
|
|
5599
|
+
await delay(pollMs);
|
|
5600
|
+
}
|
|
5601
|
+
return false;
|
|
5602
|
+
}
|
|
5603
|
+
/** Creator path: mint the initial group key, then connect. */
|
|
5604
|
+
async create() {
|
|
5605
|
+
const keyring = this.requireKeyring();
|
|
5606
|
+
await keyring.bootstrapNew();
|
|
5607
|
+
await this.connect();
|
|
5608
|
+
// Admit anyone who requested a key before we were listening.
|
|
5609
|
+
if (this.deps.autoAdmit !== false) {
|
|
5610
|
+
await keyring.admitPending().catch(() => { });
|
|
5611
|
+
}
|
|
5612
|
+
}
|
|
5613
|
+
/** Wrap the group key for a newcomer and post it (key-holder action). */
|
|
5614
|
+
async admit(memberId, identityEcdhPub) {
|
|
5615
|
+
await this.requireKeyring().admit(memberId, identityEcdhPub);
|
|
5616
|
+
}
|
|
5617
|
+
/** Rotate to a fresh epoch and re-wrap to the roster (rotate spaces). */
|
|
5618
|
+
async rotate(roster) {
|
|
5619
|
+
return this.requireKeyring().rotate(roster);
|
|
5620
|
+
}
|
|
5343
5621
|
// -- room-scoped files (no websocket needed) -------------------------------
|
|
5344
|
-
/** Encrypt + erasure-code + upload `file` to this room's shards. */
|
|
5345
5622
|
async putFile(file, metadata) {
|
|
5346
5623
|
return this.fileStorage().writeFileToShards({ data: file, metadata });
|
|
5347
5624
|
}
|
|
5348
|
-
/** Fetch + decode a file previously shared in this room. */
|
|
5349
5625
|
async getFile(manifest) {
|
|
5350
5626
|
return this.fileStorage().readFileFromShards(manifest);
|
|
5351
5627
|
}
|
|
@@ -5357,6 +5633,92 @@ class Room {
|
|
|
5357
5633
|
});
|
|
5358
5634
|
return new FileStorage({ shards });
|
|
5359
5635
|
}
|
|
5636
|
+
// -- internals -------------------------------------------------------------
|
|
5637
|
+
/** Wire the RAW_FRAME handler that drives fan-out events. */
|
|
5638
|
+
wireFanout() {
|
|
5639
|
+
if (this.rawWired || !this.channel)
|
|
5640
|
+
return;
|
|
5641
|
+
this.rawWired = true;
|
|
5642
|
+
this.channel.on(BroadcastChannelEvents.RAW_FRAME, (e) => {
|
|
5643
|
+
void this.onRawFrame(e.detail);
|
|
5644
|
+
});
|
|
5645
|
+
}
|
|
5646
|
+
async onRawFrame(frame) {
|
|
5647
|
+
const f = frame;
|
|
5648
|
+
if (!f || typeof f !== "object")
|
|
5649
|
+
return;
|
|
5650
|
+
if (typeof f.spaceMessage === "string" && this.cipher) {
|
|
5651
|
+
const decoded = await this.decodeFrame(f.spaceMessage, this.cipher, /*raw*/ true);
|
|
5652
|
+
if (decoded)
|
|
5653
|
+
this.emitEvent("message", decoded);
|
|
5654
|
+
return;
|
|
5655
|
+
}
|
|
5656
|
+
if (f.joinRequest && typeof f.joinRequest === "object") {
|
|
5657
|
+
const req = f.joinRequest;
|
|
5658
|
+
this.emitEvent("join-request", req);
|
|
5659
|
+
// Auto-admit: if we hold the group key, wrap it for the newcomer.
|
|
5660
|
+
if (this.deps.autoAdmit !== false && this.deps.keyring?.hasAnyKey() && req.memberId !== this.deps.myId()) {
|
|
5661
|
+
void this.deps.keyring.admit(req.memberId, req.identityEcdhPub).catch(() => { });
|
|
5662
|
+
}
|
|
5663
|
+
return;
|
|
5664
|
+
}
|
|
5665
|
+
if (f.keyringReady && this.deps.keyring) {
|
|
5666
|
+
// A key was wrapped for us — pull it.
|
|
5667
|
+
void this.deps.keyring.pullKeys().catch(() => { });
|
|
5668
|
+
}
|
|
5669
|
+
}
|
|
5670
|
+
/**
|
|
5671
|
+
* Decode one persisted/broadcast frame into a SpaceMessageEvent.
|
|
5672
|
+
* `isRawFrame` true means `input` is the already-extracted `spaceMessage`
|
|
5673
|
+
* packet string; false means it's the full `{spaceMessage, name, ...}` frame.
|
|
5674
|
+
*/
|
|
5675
|
+
async decodeFrame(input, cipher, isRawFrame = false) {
|
|
5676
|
+
try {
|
|
5677
|
+
let packetJson = input;
|
|
5678
|
+
if (!isRawFrame) {
|
|
5679
|
+
const outer = JSON.parse(input);
|
|
5680
|
+
if (typeof outer.spaceMessage !== "string")
|
|
5681
|
+
return null;
|
|
5682
|
+
packetJson = outer.spaceMessage;
|
|
5683
|
+
}
|
|
5684
|
+
const packet = Packet.deserialize(packetJson);
|
|
5685
|
+
if (!cipher.handles(packet.headers))
|
|
5686
|
+
return null;
|
|
5687
|
+
const serialized = await cipher.open(packet.headers);
|
|
5688
|
+
if (serialized === null)
|
|
5689
|
+
return null; // epoch key we don't hold
|
|
5690
|
+
const message = Message.deserialize(serialized);
|
|
5691
|
+
return {
|
|
5692
|
+
from: packet.source,
|
|
5693
|
+
channel: packet.subject,
|
|
5694
|
+
epoch: Number(packet.headers?.epoch ?? 0),
|
|
5695
|
+
contentType: packet.headers?.contentType,
|
|
5696
|
+
message,
|
|
5697
|
+
};
|
|
5698
|
+
}
|
|
5699
|
+
catch (err) {
|
|
5700
|
+
this.emitEvent("error", err);
|
|
5701
|
+
return null;
|
|
5702
|
+
}
|
|
5703
|
+
}
|
|
5704
|
+
emitEvent(type, detail) {
|
|
5705
|
+
this.events.dispatchEvent(new CustomEvent(type, { detail }));
|
|
5706
|
+
}
|
|
5707
|
+
requireKeyring() {
|
|
5708
|
+
if (!this.deps.keyring) {
|
|
5709
|
+
throw new Error("Space: this operation requires a keyring (open via client.space).");
|
|
5710
|
+
}
|
|
5711
|
+
return this.deps.keyring;
|
|
5712
|
+
}
|
|
5713
|
+
requireCipher(contentType) {
|
|
5714
|
+
if (!this.deps.keyring) {
|
|
5715
|
+
throw new Error("Space: fan-out messaging requires a keyring (open via client.space).");
|
|
5716
|
+
}
|
|
5717
|
+
return contentType ? new SpacePacketCipher(this.deps.keyring, contentType) : this.cipher;
|
|
5718
|
+
}
|
|
5719
|
+
}
|
|
5720
|
+
function delay(ms) {
|
|
5721
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
5360
5722
|
}
|
|
5361
5723
|
|
|
5362
5724
|
/**
|
|
@@ -5461,7 +5823,7 @@ class MessageNamespace {
|
|
|
5461
5823
|
* higher-level conveniences layered over the same space transport).
|
|
5462
5824
|
*/
|
|
5463
5825
|
room(roomName) {
|
|
5464
|
-
return new
|
|
5826
|
+
return new Space({
|
|
5465
5827
|
name: roomName,
|
|
5466
5828
|
wsBaseUrl: this.deps.wsBaseUrl,
|
|
5467
5829
|
httpBaseUrl: this.deps.http.baseUrl,
|
|
@@ -5537,6 +5899,443 @@ function parseMaybeJson(text) {
|
|
|
5537
5899
|
}
|
|
5538
5900
|
}
|
|
5539
5901
|
|
|
5902
|
+
/**
|
|
5903
|
+
* SpaceKeyring — the client's view of a space's group keys.
|
|
5904
|
+
*
|
|
5905
|
+
* Holds the per-epoch group keys this member possesses (in memory, optionally
|
|
5906
|
+
* mirrored to PersonalSpace), and orchestrates the server-blind distribution
|
|
5907
|
+
* dance against a {@link KeyringTransport}:
|
|
5908
|
+
*
|
|
5909
|
+
* - a newcomer posts a join request and pulls the wrapped key blob addressed
|
|
5910
|
+
* to it, unwrapping with its identity private key;
|
|
5911
|
+
* - an existing key-holder wraps the group key for a newcomer (ECIES to the
|
|
5912
|
+
* newcomer's identity public key) and posts the opaque blob;
|
|
5913
|
+
* - on membership change (rotate spaces) a fresh epoch key is minted and
|
|
5914
|
+
* re-wrapped to the current roster.
|
|
5915
|
+
*
|
|
5916
|
+
* The server only ever stores opaque {@link WrappedKey} blobs — it cannot read
|
|
5917
|
+
* the group key. Implements {@link EpochKeyProvider} so it can back a
|
|
5918
|
+
* {@link SpacePacketCipher} directly.
|
|
5919
|
+
*/
|
|
5920
|
+
/**
|
|
5921
|
+
* Reserved member id for the app's "keeper" — a trusted always-available member
|
|
5922
|
+
* (the accelerator's AppDOv1) that holds the group key and re-issues it to
|
|
5923
|
+
* newcomers, so a channel is joinable with no human online. Admitting this id
|
|
5924
|
+
* at channel creation hands the key to the keeper. MUST match the accelerator's
|
|
5925
|
+
* `KEEPER_MEMBER_ID`.
|
|
5926
|
+
*/
|
|
5927
|
+
const KEEPER_MEMBER_ID = "__keeper__";
|
|
5928
|
+
class SpaceKeyring {
|
|
5929
|
+
deps;
|
|
5930
|
+
keys = new Map();
|
|
5931
|
+
epoch = 0;
|
|
5932
|
+
policy;
|
|
5933
|
+
constructor(deps) {
|
|
5934
|
+
this.deps = deps;
|
|
5935
|
+
this.policy = deps.historyPolicy ?? "static";
|
|
5936
|
+
}
|
|
5937
|
+
// -- EpochKeyProvider ------------------------------------------------------
|
|
5938
|
+
currentEpoch() {
|
|
5939
|
+
return this.epoch;
|
|
5940
|
+
}
|
|
5941
|
+
keyForEpoch(epoch) {
|
|
5942
|
+
return this.keys.get(epoch);
|
|
5943
|
+
}
|
|
5944
|
+
hasAnyKey() {
|
|
5945
|
+
return this.keys.size > 0;
|
|
5946
|
+
}
|
|
5947
|
+
// -- lifecycle -------------------------------------------------------------
|
|
5948
|
+
/** Hydrate keys from the PersonalSpace cache, if available. Best-effort —
|
|
5949
|
+
* a cache miss/failure (e.g. not unlocked) must not block joining. */
|
|
5950
|
+
async loadFromCache() {
|
|
5951
|
+
if (!this.deps.cache)
|
|
5952
|
+
return false;
|
|
5953
|
+
let cached;
|
|
5954
|
+
try {
|
|
5955
|
+
cached = await this.deps.cache.loadKeys(this.deps.spaceId);
|
|
5956
|
+
}
|
|
5957
|
+
catch {
|
|
5958
|
+
return false;
|
|
5959
|
+
}
|
|
5960
|
+
if (!cached)
|
|
5961
|
+
return false;
|
|
5962
|
+
for (const [epochStr, b64] of Object.entries(cached)) {
|
|
5963
|
+
this.keys.set(Number(epochStr), fromBase64(b64));
|
|
5964
|
+
}
|
|
5965
|
+
this.epoch = Math.max(0, ...Array.from(this.keys.keys()));
|
|
5966
|
+
return this.keys.size > 0;
|
|
5967
|
+
}
|
|
5968
|
+
async persist() {
|
|
5969
|
+
if (!this.deps.cache)
|
|
5970
|
+
return;
|
|
5971
|
+
const out = {};
|
|
5972
|
+
for (const [epoch, key] of this.keys)
|
|
5973
|
+
out[String(epoch)] = toBase64(key);
|
|
5974
|
+
// Best-effort: caching is an optimization; a write failure (e.g. locked
|
|
5975
|
+
// storage) must not break key acquisition.
|
|
5976
|
+
try {
|
|
5977
|
+
await this.deps.cache.saveKeys(this.deps.spaceId, out);
|
|
5978
|
+
}
|
|
5979
|
+
catch {
|
|
5980
|
+
/* ignore */
|
|
5981
|
+
}
|
|
5982
|
+
}
|
|
5983
|
+
/** Creator path: mint epoch-0 group key for a brand-new space. */
|
|
5984
|
+
async bootstrapNew() {
|
|
5985
|
+
this.keys.set(0, generateSpaceKey());
|
|
5986
|
+
this.epoch = 0;
|
|
5987
|
+
await this.persist();
|
|
5988
|
+
}
|
|
5989
|
+
// -- newcomer: request + pull ---------------------------------------------
|
|
5990
|
+
/** Post a join request so an online key-holder can wrap a key for us. */
|
|
5991
|
+
async requestKey(desiredEpoch) {
|
|
5992
|
+
await this.deps.transport.postJoinRequest({
|
|
5993
|
+
memberId: this.deps.memberId,
|
|
5994
|
+
identityEcdhPub: this.deps.identityEcdhPub,
|
|
5995
|
+
desiredEpoch,
|
|
5996
|
+
});
|
|
5997
|
+
}
|
|
5998
|
+
/**
|
|
5999
|
+
* Pull and unwrap any blobs addressed to us. Returns the number of new
|
|
6000
|
+
* epoch keys obtained. Safe to call repeatedly (idempotent).
|
|
6001
|
+
*/
|
|
6002
|
+
async pullKeys() {
|
|
6003
|
+
const priv = this.deps.ownPrivateKey();
|
|
6004
|
+
if (!priv)
|
|
6005
|
+
throw new Error("SpaceKeyring: no identity private key to unwrap with");
|
|
6006
|
+
const blobs = await this.deps.transport.fetchBlobs(this.deps.memberId);
|
|
6007
|
+
let added = 0;
|
|
6008
|
+
for (const blob of blobs) {
|
|
6009
|
+
if (this.keys.has(blob.epoch))
|
|
6010
|
+
continue;
|
|
6011
|
+
try {
|
|
6012
|
+
const key = await unwrapSpaceKey(blob, priv);
|
|
6013
|
+
this.keys.set(blob.epoch, key);
|
|
6014
|
+
added++;
|
|
6015
|
+
}
|
|
6016
|
+
catch {
|
|
6017
|
+
// Not for us / tampered — skip this blob.
|
|
6018
|
+
}
|
|
6019
|
+
}
|
|
6020
|
+
if (added > 0) {
|
|
6021
|
+
this.epoch = Math.max(this.epoch, ...Array.from(this.keys.keys()));
|
|
6022
|
+
await this.persist();
|
|
6023
|
+
}
|
|
6024
|
+
return added;
|
|
6025
|
+
}
|
|
6026
|
+
// -- key-holder: admit + rotate -------------------------------------------
|
|
6027
|
+
/**
|
|
6028
|
+
* Wrap our group key(s) for a newcomer and post the blobs. In a `static`
|
|
6029
|
+
* space the newcomer receives every epoch we hold (full history); in a
|
|
6030
|
+
* `rotate` space they receive only the current epoch.
|
|
6031
|
+
*/
|
|
6032
|
+
async admit(targetMemberId, identityEcdhPub) {
|
|
6033
|
+
const recipient = await importEcdhPublicKey(identityEcdhPub);
|
|
6034
|
+
const epochs = this.policy === "static"
|
|
6035
|
+
? Array.from(this.keys.keys())
|
|
6036
|
+
: [this.epoch];
|
|
6037
|
+
for (const epoch of epochs) {
|
|
6038
|
+
const key = this.keys.get(epoch);
|
|
6039
|
+
if (!key)
|
|
6040
|
+
continue;
|
|
6041
|
+
const wrapped = await wrapSpaceKey(key, epoch, recipient);
|
|
6042
|
+
await this.deps.transport.postWrappedKey(targetMemberId, wrapped, this.deps.memberId);
|
|
6043
|
+
}
|
|
6044
|
+
}
|
|
6045
|
+
/**
|
|
6046
|
+
* Admit every outstanding join request (an online key-holder fulfilling
|
|
6047
|
+
* pending newcomers). Returns the ids admitted.
|
|
6048
|
+
*/
|
|
6049
|
+
async admitPending() {
|
|
6050
|
+
const pending = await this.deps.transport.fetchPending();
|
|
6051
|
+
const admitted = [];
|
|
6052
|
+
for (const req of pending) {
|
|
6053
|
+
if (req.memberId === this.deps.memberId)
|
|
6054
|
+
continue;
|
|
6055
|
+
await this.admit(req.memberId, req.identityEcdhPub);
|
|
6056
|
+
admitted.push(req.memberId);
|
|
6057
|
+
}
|
|
6058
|
+
return admitted;
|
|
6059
|
+
}
|
|
6060
|
+
/**
|
|
6061
|
+
* Rotate to a fresh epoch (rotate spaces). Mints a new group key, advances
|
|
6062
|
+
* the epoch, and re-wraps it to every member in `roster` (plus ourselves).
|
|
6063
|
+
*/
|
|
6064
|
+
async rotate(roster) {
|
|
6065
|
+
const members = roster ?? (await this.deps.transport.fetchRoster());
|
|
6066
|
+
const { epoch: nextEpoch } = await this.deps.transport.rotate(this.epoch + 1);
|
|
6067
|
+
const newKey = generateSpaceKey();
|
|
6068
|
+
this.keys.set(nextEpoch, newKey);
|
|
6069
|
+
this.epoch = nextEpoch;
|
|
6070
|
+
for (const m of members) {
|
|
6071
|
+
if (m.memberId === this.deps.memberId)
|
|
6072
|
+
continue;
|
|
6073
|
+
const recipient = await importEcdhPublicKey(m.identityEcdhPub);
|
|
6074
|
+
const wrapped = await wrapSpaceKey(newKey, nextEpoch, recipient);
|
|
6075
|
+
await this.deps.transport.postWrappedKey(m.memberId, wrapped, this.deps.memberId);
|
|
6076
|
+
}
|
|
6077
|
+
await this.persist();
|
|
6078
|
+
return nextEpoch;
|
|
6079
|
+
}
|
|
6080
|
+
}
|
|
6081
|
+
|
|
6082
|
+
/**
|
|
6083
|
+
* KeyringClient — HTTP implementation of {@link KeyringTransport} against the
|
|
6084
|
+
* accelerator's per-space keyring endpoints. All blobs are opaque to the
|
|
6085
|
+
* server; this client just moves them.
|
|
6086
|
+
*
|
|
6087
|
+
* POST /api/spaces/:id/keyring/join-request { ...JoinRequest }
|
|
6088
|
+
* GET /api/spaces/:id/keyring/blobs/:member -> WrappedKey[]
|
|
6089
|
+
* POST /api/spaces/:id/keyring/wrap { targetMemberId, fromMemberId, wrapped }
|
|
6090
|
+
* GET /api/spaces/:id/keyring/pending -> JoinRequest[]
|
|
6091
|
+
* GET /api/spaces/:id/keyring/roster -> { memberId, identityEcdhPub }[]
|
|
6092
|
+
* POST /api/spaces/:id/keyring/rotate { nextEpoch } -> { epoch }
|
|
6093
|
+
* GET /api/spaces/:id/metadata -> SpaceMetadata
|
|
6094
|
+
*/
|
|
6095
|
+
class KeyringClient {
|
|
6096
|
+
deps;
|
|
6097
|
+
constructor(deps) {
|
|
6098
|
+
this.deps = deps;
|
|
6099
|
+
}
|
|
6100
|
+
url(suffix) {
|
|
6101
|
+
return `${this.deps.httpBaseUrl}/api/spaces/${encodeURIComponent(this.deps.spaceId)}${suffix}`;
|
|
6102
|
+
}
|
|
6103
|
+
async post(suffix, body) {
|
|
6104
|
+
const res = await this.deps.fetch(this.url(suffix), {
|
|
6105
|
+
method: "POST",
|
|
6106
|
+
headers: { "Content-Type": "application/json" },
|
|
6107
|
+
body: JSON.stringify(body),
|
|
6108
|
+
});
|
|
6109
|
+
if (!res.ok)
|
|
6110
|
+
throw new Error(`KeyringClient POST ${suffix}: ${res.status} ${res.statusText}`);
|
|
6111
|
+
return (await res.json().catch(() => ({})));
|
|
6112
|
+
}
|
|
6113
|
+
async get(suffix) {
|
|
6114
|
+
const res = await this.deps.fetch(this.url(suffix));
|
|
6115
|
+
if (!res.ok)
|
|
6116
|
+
throw new Error(`KeyringClient GET ${suffix}: ${res.status} ${res.statusText}`);
|
|
6117
|
+
return (await res.json());
|
|
6118
|
+
}
|
|
6119
|
+
async postJoinRequest(req) {
|
|
6120
|
+
await this.post("/keyring/join-request", req);
|
|
6121
|
+
}
|
|
6122
|
+
async fetchBlobs(memberId) {
|
|
6123
|
+
const body = await this.get(`/keyring/blobs/${encodeURIComponent(memberId)}`);
|
|
6124
|
+
return body.blobs ?? [];
|
|
6125
|
+
}
|
|
6126
|
+
async postWrappedKey(targetMemberId, wrapped, fromMemberId) {
|
|
6127
|
+
await this.post("/keyring/wrap", { targetMemberId, fromMemberId, wrapped });
|
|
6128
|
+
}
|
|
6129
|
+
async fetchPending() {
|
|
6130
|
+
const body = await this.get("/keyring/pending");
|
|
6131
|
+
return body.pending ?? [];
|
|
6132
|
+
}
|
|
6133
|
+
async fetchRoster() {
|
|
6134
|
+
const body = await this.get("/keyring/roster");
|
|
6135
|
+
return body.roster ?? [];
|
|
6136
|
+
}
|
|
6137
|
+
async rotate(nextEpoch) {
|
|
6138
|
+
return this.post("/keyring/rotate", { nextEpoch });
|
|
6139
|
+
}
|
|
6140
|
+
async fetchMetadata() {
|
|
6141
|
+
try {
|
|
6142
|
+
return await this.get("/metadata");
|
|
6143
|
+
}
|
|
6144
|
+
catch {
|
|
6145
|
+
return null;
|
|
6146
|
+
}
|
|
6147
|
+
}
|
|
6148
|
+
}
|
|
6149
|
+
|
|
6150
|
+
/**
|
|
6151
|
+
* `client.space` — the fan-out group-encryption surface.
|
|
6152
|
+
*
|
|
6153
|
+
* const space = await client.space.createSpace({ historyPolicy: "static" });
|
|
6154
|
+
* await space.sendMessage({ text: "hello" });
|
|
6155
|
+
* space.onMessage(e => …);
|
|
6156
|
+
*
|
|
6157
|
+
* const joined = await client.space.joinSpace(space.id); // newcomer
|
|
6158
|
+
*
|
|
6159
|
+
* A space owns a client-generated keypair whose public key is its id, a
|
|
6160
|
+
* symmetric group key distributed server-blind via the keyring, and a
|
|
6161
|
+
* persisted message log. This namespace wires the {@link Space} handle to the
|
|
6162
|
+
* HTTP {@link KeyringClient}, the member's identity keys (from {@link KeyStore},
|
|
6163
|
+
* shared with the Double Ratchet path), and a PersonalSpace-backed key cache.
|
|
6164
|
+
*/
|
|
6165
|
+
/** Thrown by `joinChannel` when no channel with that name is registered. */
|
|
6166
|
+
class ChannelNotFoundError extends Error {
|
|
6167
|
+
channelName;
|
|
6168
|
+
constructor(channelName) {
|
|
6169
|
+
super(`Channel "${channelName}" does not exist. Create it first.`);
|
|
6170
|
+
this.channelName = channelName;
|
|
6171
|
+
this.name = "ChannelNotFoundError";
|
|
6172
|
+
}
|
|
6173
|
+
}
|
|
6174
|
+
/** Thrown by `createChannel` when the name is already registered. */
|
|
6175
|
+
class ChannelExistsError extends Error {
|
|
6176
|
+
channelName;
|
|
6177
|
+
constructor(channelName) {
|
|
6178
|
+
super(`Channel "${channelName}" already exists.`);
|
|
6179
|
+
this.channelName = channelName;
|
|
6180
|
+
this.name = "ChannelExistsError";
|
|
6181
|
+
}
|
|
6182
|
+
}
|
|
6183
|
+
class SpaceNamespace {
|
|
6184
|
+
deps;
|
|
6185
|
+
constructor(deps) {
|
|
6186
|
+
this.deps = deps;
|
|
6187
|
+
}
|
|
6188
|
+
/**
|
|
6189
|
+
* Create a new space: generate its keypair (public key = id), register
|
|
6190
|
+
* metadata, mint the initial group key, and connect. Returns the open Space.
|
|
6191
|
+
*/
|
|
6192
|
+
async createSpace(opts = {}) {
|
|
6193
|
+
const historyPolicy = opts.historyPolicy ?? "static";
|
|
6194
|
+
const identity = await generateSpaceIdentity();
|
|
6195
|
+
// Register the space (server records pubkey + policy; the DO is lazy).
|
|
6196
|
+
await this.deps.http.post(`/api/spaces/${encodeURIComponent(identity.id)}/metadata`, { spacePubKey: identity.id, historyPolicy });
|
|
6197
|
+
const space = await this.build(identity.id, historyPolicy);
|
|
6198
|
+
await space.create();
|
|
6199
|
+
return space;
|
|
6200
|
+
}
|
|
6201
|
+
/** Join an existing space by id (its public key). Resolves once a key arrives. */
|
|
6202
|
+
async joinSpace(spaceId, opts = {}) {
|
|
6203
|
+
const policy = await this.fetchPolicy(spaceId);
|
|
6204
|
+
const space = await this.build(spaceId, policy);
|
|
6205
|
+
await space.join({ timeoutMs: opts.timeoutMs });
|
|
6206
|
+
return space;
|
|
6207
|
+
}
|
|
6208
|
+
/** A handle to a space without auto-connecting (uses cached keys on demand). */
|
|
6209
|
+
async get(spaceId, historyPolicy = "static") {
|
|
6210
|
+
return this.build(spaceId, historyPolicy);
|
|
6211
|
+
}
|
|
6212
|
+
// -------------------------------------------------------------------------
|
|
6213
|
+
// Channels — named, app-scoped pointers to spaces. The name → space-id
|
|
6214
|
+
// directory is app-public config (server-readable, scoped to this app via
|
|
6215
|
+
// its key); the channel CONTENTS stay fan-out E2E-encrypted. Lets chat use
|
|
6216
|
+
// human room names (`general`) instead of raw pubkey ids.
|
|
6217
|
+
// -------------------------------------------------------------------------
|
|
6218
|
+
/** List this app's channels (name → space-id) for the key's environment. */
|
|
6219
|
+
async listChannels() {
|
|
6220
|
+
const body = await this.deps.http.get("/api/app/channels");
|
|
6221
|
+
return body.channels ?? [];
|
|
6222
|
+
}
|
|
6223
|
+
/** Resolve a channel name to its space id, or null if it doesn't exist. */
|
|
6224
|
+
async resolveChannel(name) {
|
|
6225
|
+
try {
|
|
6226
|
+
const body = await this.deps.http.get(`/api/app/channels/${encodeURIComponent(name)}`);
|
|
6227
|
+
return body.spaceId ?? null;
|
|
6228
|
+
}
|
|
6229
|
+
catch (err) {
|
|
6230
|
+
if (err instanceof HttpError && err.status === 404)
|
|
6231
|
+
return null;
|
|
6232
|
+
throw err;
|
|
6233
|
+
}
|
|
6234
|
+
}
|
|
6235
|
+
/**
|
|
6236
|
+
* Create a new channel: mint a fan-out space (the caller becomes its first
|
|
6237
|
+
* key-holder) and register `name → spaceId` in the app directory. Throws
|
|
6238
|
+
* {@link ChannelExistsError} if the name is already taken.
|
|
6239
|
+
*/
|
|
6240
|
+
async createChannel(name, opts = {}) {
|
|
6241
|
+
const space = await this.createSpace(opts);
|
|
6242
|
+
// Register name → id first so a name clash doesn't leave an admitted keeper.
|
|
6243
|
+
try {
|
|
6244
|
+
await this.deps.http.post("/api/app/channels", { name, spaceId: space.id });
|
|
6245
|
+
}
|
|
6246
|
+
catch (err) {
|
|
6247
|
+
if (err instanceof HttpError && err.status === 409) {
|
|
6248
|
+
throw new ChannelExistsError(name);
|
|
6249
|
+
}
|
|
6250
|
+
throw err;
|
|
6251
|
+
}
|
|
6252
|
+
// Admit the app keeper so the channel is joinable with no member online.
|
|
6253
|
+
// Best-effort: a channel still works (members admit each other) without it.
|
|
6254
|
+
try {
|
|
6255
|
+
const { pubkey } = await this.deps.http.get("/api/app/keeper");
|
|
6256
|
+
if (pubkey && space.keyring)
|
|
6257
|
+
await space.keyring.admit(KEEPER_MEMBER_ID, pubkey);
|
|
6258
|
+
}
|
|
6259
|
+
catch (err) {
|
|
6260
|
+
console.warn("[client.space] keeper admit failed (channel still usable):", err);
|
|
6261
|
+
}
|
|
6262
|
+
return space;
|
|
6263
|
+
}
|
|
6264
|
+
/**
|
|
6265
|
+
* Join an existing channel by name. Throws {@link ChannelNotFoundError} if
|
|
6266
|
+
* no channel with that name is registered (use {@link createChannel} first).
|
|
6267
|
+
*/
|
|
6268
|
+
async joinChannel(name, opts = {}) {
|
|
6269
|
+
const spaceId = await this.resolveChannel(name);
|
|
6270
|
+
if (!spaceId)
|
|
6271
|
+
throw new ChannelNotFoundError(name);
|
|
6272
|
+
return this.joinSpace(spaceId, opts);
|
|
6273
|
+
}
|
|
6274
|
+
// -------------------------------------------------------------------------
|
|
6275
|
+
// Internals
|
|
6276
|
+
// -------------------------------------------------------------------------
|
|
6277
|
+
async build(spaceId, historyPolicy) {
|
|
6278
|
+
const memberId = this.myId();
|
|
6279
|
+
const identityEcdhPub = await this.ensureIdentity(memberId);
|
|
6280
|
+
const transport = new KeyringClient({
|
|
6281
|
+
spaceId,
|
|
6282
|
+
httpBaseUrl: this.deps.http.baseUrl,
|
|
6283
|
+
fetch: this.deps.http.fetch,
|
|
6284
|
+
});
|
|
6285
|
+
const keyring = new SpaceKeyring({
|
|
6286
|
+
spaceId,
|
|
6287
|
+
memberId,
|
|
6288
|
+
identityEcdhPub,
|
|
6289
|
+
ownPrivateKey: () => KeyStore.getInstance().getKeyPair(memberId)?.privateKey ?? null,
|
|
6290
|
+
transport,
|
|
6291
|
+
cache: this.deps.cache,
|
|
6292
|
+
historyPolicy,
|
|
6293
|
+
});
|
|
6294
|
+
return new Space({
|
|
6295
|
+
name: spaceId,
|
|
6296
|
+
wsBaseUrl: this.deps.wsBaseUrl,
|
|
6297
|
+
httpBaseUrl: this.deps.http.baseUrl,
|
|
6298
|
+
fetch: this.deps.http.fetch,
|
|
6299
|
+
myId: () => memberId,
|
|
6300
|
+
fetchTicket: () => this.fetchTicket(),
|
|
6301
|
+
keyring,
|
|
6302
|
+
historyPolicy,
|
|
6303
|
+
});
|
|
6304
|
+
}
|
|
6305
|
+
/** Ensure the member has an identity ECDH keypair; return its public key. */
|
|
6306
|
+
async ensureIdentity(memberId) {
|
|
6307
|
+
const store = KeyStore.getInstance();
|
|
6308
|
+
if (!store.getKeyPair(memberId)) {
|
|
6309
|
+
await store.generateOwnKeyPair(memberId);
|
|
6310
|
+
}
|
|
6311
|
+
const pub = store.getKeyPair(memberId).publicKey;
|
|
6312
|
+
return exportEcdhPublicKey(pub);
|
|
6313
|
+
}
|
|
6314
|
+
async fetchPolicy(spaceId) {
|
|
6315
|
+
const meta = await new KeyringClient({
|
|
6316
|
+
spaceId,
|
|
6317
|
+
httpBaseUrl: this.deps.http.baseUrl,
|
|
6318
|
+
fetch: this.deps.http.fetch,
|
|
6319
|
+
}).fetchMetadata();
|
|
6320
|
+
return meta?.historyPolicy ?? "static";
|
|
6321
|
+
}
|
|
6322
|
+
async fetchTicket() {
|
|
6323
|
+
try {
|
|
6324
|
+
const res = await this.deps.http.post("/api/ws-ticket", {});
|
|
6325
|
+
return res?.ticket ?? null;
|
|
6326
|
+
}
|
|
6327
|
+
catch {
|
|
6328
|
+
return null;
|
|
6329
|
+
}
|
|
6330
|
+
}
|
|
6331
|
+
myId() {
|
|
6332
|
+
const id = this.deps.session.username;
|
|
6333
|
+
if (!id)
|
|
6334
|
+
throw new Error("client.space: not signed in — call client.auth.zk.login() first.");
|
|
6335
|
+
return id;
|
|
6336
|
+
}
|
|
6337
|
+
}
|
|
6338
|
+
|
|
5540
6339
|
/**
|
|
5541
6340
|
* `Client` — the single entry point to the Muhkoo SDK.
|
|
5542
6341
|
*
|
|
@@ -5577,6 +6376,8 @@ class Client {
|
|
|
5577
6376
|
storage;
|
|
5578
6377
|
/** Realtime messaging — `client.message.subscribe(...)`, etc. */
|
|
5579
6378
|
message;
|
|
6379
|
+
/** Fan-out group spaces — `client.space.createSpace(...)`, etc. */
|
|
6380
|
+
space;
|
|
5580
6381
|
session;
|
|
5581
6382
|
http;
|
|
5582
6383
|
constructor(options = {}) {
|
|
@@ -5597,6 +6398,19 @@ class Client {
|
|
|
5597
6398
|
this.auth = new AuthNamespace({ auth: authClient, circuits, session: this.session });
|
|
5598
6399
|
this.storage = new StorageNamespace({ http: this.http, session: this.session, wsBaseUrl });
|
|
5599
6400
|
this.message = new MessageNamespace({ http: this.http, session: this.session, wsBaseUrl });
|
|
6401
|
+
// Group keys are cached in the user's PersonalSpace (encrypted at rest
|
|
6402
|
+
// by StorageNamespace), so a returning member hydrates without a fresh
|
|
6403
|
+
// keyring round-trip. The server only ever sees the ciphertext.
|
|
6404
|
+
const spaceKeyCache = {
|
|
6405
|
+
loadKeys: (spaceId) => this.storage.get("space-keys", spaceId),
|
|
6406
|
+
saveKeys: (spaceId, keys) => this.storage.set("space-keys", spaceId, keys),
|
|
6407
|
+
};
|
|
6408
|
+
this.space = new SpaceNamespace({
|
|
6409
|
+
http: this.http,
|
|
6410
|
+
session: this.session,
|
|
6411
|
+
wsBaseUrl,
|
|
6412
|
+
cache: spaceKeyCache,
|
|
6413
|
+
});
|
|
5600
6414
|
}
|
|
5601
6415
|
/** The currently signed-in user, or `null`. */
|
|
5602
6416
|
get user() {
|
|
@@ -5608,6 +6422,48 @@ class Client {
|
|
|
5608
6422
|
}
|
|
5609
6423
|
}
|
|
5610
6424
|
|
|
6425
|
+
/**
|
|
6426
|
+
* PacketCipher — the strategy `Network` uses to seal/open a packet's payload.
|
|
6427
|
+
*
|
|
6428
|
+
* `Network` owns the wire shape (Message ↔ Packet, header placement, transport,
|
|
6429
|
+
* reconnect, checksum). The *encryption* is pluggable: the default
|
|
6430
|
+
* {@link DoubleRatchetCipher} reproduces the per-peer Double Ratchet behavior
|
|
6431
|
+
* Network has always had, while the Space layer supplies a group-key cipher.
|
|
6432
|
+
*
|
|
6433
|
+
* The sealed ciphertext always rides in `Packet.headers` (cleartext routing
|
|
6434
|
+
* fields stay readable to the server); `Packet.message` is left undefined on
|
|
6435
|
+
* the wire so the server never sees the real payload.
|
|
6436
|
+
*/
|
|
6437
|
+
/**
|
|
6438
|
+
* Default cipher: per-peer Double Ratchet. Reproduces Network's original
|
|
6439
|
+
* `{ encrypted: true, cipherMessage: <json> }` header convention exactly so
|
|
6440
|
+
* existing consumers are unaffected.
|
|
6441
|
+
*/
|
|
6442
|
+
class DoubleRatchetCipher {
|
|
6443
|
+
deps;
|
|
6444
|
+
constructor(deps) {
|
|
6445
|
+
this.deps = deps;
|
|
6446
|
+
}
|
|
6447
|
+
async seal(serializedMessage) {
|
|
6448
|
+
const sessionId = this.deps.getSessionId();
|
|
6449
|
+
if (!sessionId) {
|
|
6450
|
+
throw new Error("DoubleRatchetCipher: no active session to seal under");
|
|
6451
|
+
}
|
|
6452
|
+
const cipherMessage = await this.deps.ratchetManager.encrypt(this.deps.clientId, this.deps.serverId, sessionId, serializedMessage, false, // newDhKey — let the ratchet manage key rotation
|
|
6453
|
+
this.deps.sessionType);
|
|
6454
|
+
return { encrypted: true, cipherMessage: JSON.stringify(cipherMessage) };
|
|
6455
|
+
}
|
|
6456
|
+
handles(headers) {
|
|
6457
|
+
return !!(headers?.encrypted && headers?.cipherMessage);
|
|
6458
|
+
}
|
|
6459
|
+
async open(headers) {
|
|
6460
|
+
if (!this.handles(headers))
|
|
6461
|
+
return null;
|
|
6462
|
+
const cipherMessage = JSON.parse(headers.cipherMessage);
|
|
6463
|
+
return this.deps.ratchetManager.decrypt(cipherMessage, this.deps.isClient ?? true);
|
|
6464
|
+
}
|
|
6465
|
+
}
|
|
6466
|
+
|
|
5611
6467
|
const appLogger$1 = new Logger("connect", 'ERROR');
|
|
5612
6468
|
globalThis.appLogger = appLogger$1;
|
|
5613
6469
|
|
|
@@ -5632,5 +6488,5 @@ var bundledLoader = /*#__PURE__*/Object.freeze({
|
|
|
5632
6488
|
default: loadRsWasm
|
|
5633
6489
|
});
|
|
5634
6490
|
|
|
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 };
|
|
6491
|
+
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
6492
|
//# sourceMappingURL=index.js.map
|