@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/connect.d.ts
CHANGED
|
@@ -2329,75 +2329,452 @@ declare class StorageNamespace {
|
|
|
2329
2329
|
}
|
|
2330
2330
|
|
|
2331
2331
|
/**
|
|
2332
|
-
*
|
|
2332
|
+
* Shared types for the Space fan-out group-encryption layer.
|
|
2333
2333
|
*
|
|
2334
|
-
* A
|
|
2335
|
-
*
|
|
2336
|
-
*
|
|
2337
|
-
*
|
|
2338
|
-
*
|
|
2334
|
+
* A Space is a group channel whose messages are sealed once with a symmetric
|
|
2335
|
+
* group key (`K_space`) rather than per-peer like the Double Ratchet. That
|
|
2336
|
+
* single ciphertext is decryptable by every member that holds the epoch key,
|
|
2337
|
+
* which is what lets the server persist + replay messages as history while
|
|
2338
|
+
* staying blind to their contents.
|
|
2339
|
+
*/
|
|
2340
|
+
/**
|
|
2341
|
+
* How a space treats history across membership changes.
|
|
2342
|
+
* - `static`: one group key for all time (epoch 0). New members can read the
|
|
2343
|
+
* entire history; a removed member keeps the key until you rotate manually.
|
|
2344
|
+
* - `rotate`: a new epoch key is minted on membership change. Members only
|
|
2345
|
+
* hold keys for the epochs they belonged to, so they cannot read messages
|
|
2346
|
+
* from epochs before they joined / after they left.
|
|
2347
|
+
*/
|
|
2348
|
+
type HistoryPolicy = "static" | "rotate";
|
|
2349
|
+
/**
|
|
2350
|
+
* An ECIES-wrapped copy of a group key, sealed to one member's identity ECDH
|
|
2351
|
+
* public key. Opaque to the server — useless without the member's private key.
|
|
2352
|
+
*/
|
|
2353
|
+
interface WrappedKey {
|
|
2354
|
+
/** Epoch this key belongs to (0 for static spaces). */
|
|
2355
|
+
epoch: number;
|
|
2356
|
+
/** Base64url-encoded JWK of the ephemeral ECDH public key used for the wrap. */
|
|
2357
|
+
ephemeralPub: string;
|
|
2358
|
+
/** Base64 AES-GCM IV (12 bytes). */
|
|
2359
|
+
iv: string;
|
|
2360
|
+
/** Base64 AES-GCM ciphertext: the wrapped 32-byte group key + tag. */
|
|
2361
|
+
ciphertext: string;
|
|
2362
|
+
/** Algorithm tag for forward-compat. */
|
|
2363
|
+
alg: string;
|
|
2364
|
+
}
|
|
2365
|
+
/** Cleartext space metadata the server records once at creation. */
|
|
2366
|
+
interface SpaceMetadata {
|
|
2367
|
+
/** Encoded space public key (the space's logical identity). */
|
|
2368
|
+
spaceId: string;
|
|
2369
|
+
historyPolicy: HistoryPolicy;
|
|
2370
|
+
/** Advisory monotonic epoch counter. */
|
|
2371
|
+
currentEpoch: number;
|
|
2372
|
+
}
|
|
2373
|
+
/**
|
|
2374
|
+
* A newcomer's request for the group key. The newcomer self-asserts its
|
|
2375
|
+
* identity public keys (trust-on-first-use at the client layer; the server is
|
|
2376
|
+
* blind and cannot validate them).
|
|
2377
|
+
*/
|
|
2378
|
+
interface JoinRequest {
|
|
2379
|
+
memberId: string;
|
|
2380
|
+
/** Base64url-encoded JWK of the member's identity ECDH public key. */
|
|
2381
|
+
identityEcdhPub: string;
|
|
2382
|
+
/** Base64url-encoded JWK of the member's identity ECDSA public key (optional). */
|
|
2383
|
+
identityEcdsaPub?: string;
|
|
2384
|
+
/** Epoch the newcomer wants a key for (defaults to current). */
|
|
2385
|
+
desiredEpoch?: number;
|
|
2386
|
+
ts?: number;
|
|
2387
|
+
}
|
|
2388
|
+
/** A space member's published identity keys (the keyring directory). */
|
|
2389
|
+
interface RosterMember {
|
|
2390
|
+
memberId: string;
|
|
2391
|
+
/** Base64url-encoded JWK of the member's identity ECDH public key. */
|
|
2392
|
+
identityEcdhPub: string;
|
|
2393
|
+
/** Base64url-encoded JWK of the member's identity ECDSA public key. */
|
|
2394
|
+
identityEcdsaPub?: string;
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
/**
|
|
2398
|
+
* PacketCipher — the strategy `Network` uses to seal/open a packet's payload.
|
|
2339
2399
|
*
|
|
2340
|
-
*
|
|
2341
|
-
*
|
|
2342
|
-
*
|
|
2343
|
-
*
|
|
2400
|
+
* `Network` owns the wire shape (Message ↔ Packet, header placement, transport,
|
|
2401
|
+
* reconnect, checksum). The *encryption* is pluggable: the default
|
|
2402
|
+
* {@link DoubleRatchetCipher} reproduces the per-peer Double Ratchet behavior
|
|
2403
|
+
* Network has always had, while the Space layer supplies a group-key cipher.
|
|
2344
2404
|
*
|
|
2345
|
-
*
|
|
2346
|
-
*
|
|
2347
|
-
*
|
|
2405
|
+
* The sealed ciphertext always rides in `Packet.headers` (cleartext routing
|
|
2406
|
+
* fields stay readable to the server); `Packet.message` is left undefined on
|
|
2407
|
+
* the wire so the server never sees the real payload.
|
|
2408
|
+
*/
|
|
2409
|
+
|
|
2410
|
+
type PacketHeaders = Packet["headers"];
|
|
2411
|
+
/** Header fields a cipher merges into the outgoing packet. */
|
|
2412
|
+
type SealedHeaders = Record<string, string | number | boolean>;
|
|
2413
|
+
interface PacketCipher {
|
|
2414
|
+
/**
|
|
2415
|
+
* Seal the serialized `Message`. Returns the header fields to merge into
|
|
2416
|
+
* the outgoing packet (these carry the ciphertext). `Network` clears
|
|
2417
|
+
* `packet.message` after calling this.
|
|
2418
|
+
*/
|
|
2419
|
+
seal(serializedMessage: string, packet: Packet): Promise<SealedHeaders>;
|
|
2420
|
+
/** True if the inbound headers look like this cipher's sealed form. */
|
|
2421
|
+
handles(headers: PacketHeaders): boolean;
|
|
2422
|
+
/**
|
|
2423
|
+
* Open an inbound packet's headers back into a serialized `Message` string.
|
|
2424
|
+
* Returns `null` when the packet is not sealed for / not decryptable by us
|
|
2425
|
+
* (e.g. an epoch key we don't hold) — `Network` then leaves `packet.message`
|
|
2426
|
+
* untouched rather than throwing.
|
|
2427
|
+
*/
|
|
2428
|
+
open(headers: PacketHeaders): Promise<string | null>;
|
|
2429
|
+
}
|
|
2430
|
+
interface DoubleRatchetCipherDeps {
|
|
2431
|
+
ratchetManager: DoubleRatchetManager;
|
|
2432
|
+
clientId: string;
|
|
2433
|
+
serverId: string;
|
|
2434
|
+
sessionType: "global" | "specific";
|
|
2435
|
+
/** Resolves the live session id (Network manages the session lifecycle). */
|
|
2436
|
+
getSessionId: () => string | null;
|
|
2437
|
+
/** Whether this side is the ratchet "client" (default true). */
|
|
2438
|
+
isClient?: boolean;
|
|
2439
|
+
}
|
|
2440
|
+
/**
|
|
2441
|
+
* Default cipher: per-peer Double Ratchet. Reproduces Network's original
|
|
2442
|
+
* `{ encrypted: true, cipherMessage: <json> }` header convention exactly so
|
|
2443
|
+
* existing consumers are unaffected.
|
|
2444
|
+
*/
|
|
2445
|
+
declare class DoubleRatchetCipher implements PacketCipher {
|
|
2446
|
+
private readonly deps;
|
|
2447
|
+
constructor(deps: DoubleRatchetCipherDeps);
|
|
2448
|
+
seal(serializedMessage: string): Promise<SealedHeaders>;
|
|
2449
|
+
handles(headers: PacketHeaders): boolean;
|
|
2450
|
+
open(headers: PacketHeaders): Promise<string | null>;
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
/**
|
|
2454
|
+
* SpacePacketCipher — the {@link PacketCipher} that seals packets with a
|
|
2455
|
+
* space's group key instead of a per-peer Double Ratchet.
|
|
2456
|
+
*
|
|
2457
|
+
* It pulls the active epoch's key from an {@link EpochKeyProvider} (the
|
|
2458
|
+
* SpaceKeyring), so this adapter stays decoupled from the keyring's transport
|
|
2459
|
+
* and caching concerns — it only needs "what key seals epoch N".
|
|
2460
|
+
*/
|
|
2461
|
+
|
|
2462
|
+
/** Minimal view of the keyring the cipher needs. */
|
|
2463
|
+
interface EpochKeyProvider {
|
|
2464
|
+
/** Epoch new messages are sealed under (advances on rotation). */
|
|
2465
|
+
currentEpoch(): number;
|
|
2466
|
+
/** The 32-byte group key for an epoch, or undefined if we don't hold it. */
|
|
2467
|
+
keyForEpoch(epoch: number): Uint8Array | undefined;
|
|
2468
|
+
}
|
|
2469
|
+
declare class SpacePacketCipher implements PacketCipher {
|
|
2470
|
+
private readonly keys;
|
|
2471
|
+
private readonly contentType?;
|
|
2472
|
+
constructor(keys: EpochKeyProvider, contentType?: string | undefined);
|
|
2473
|
+
seal(serializedMessage: string): Promise<SealedHeaders>;
|
|
2474
|
+
handles(headers: PacketHeaders): boolean;
|
|
2475
|
+
/** Returns null for epochs we don't hold (surfaced as undecryptable). */
|
|
2476
|
+
open(headers: PacketHeaders): Promise<string | null>;
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
/**
|
|
2480
|
+
* SpaceKeyring — the client's view of a space's group keys.
|
|
2481
|
+
*
|
|
2482
|
+
* Holds the per-epoch group keys this member possesses (in memory, optionally
|
|
2483
|
+
* mirrored to PersonalSpace), and orchestrates the server-blind distribution
|
|
2484
|
+
* dance against a {@link KeyringTransport}:
|
|
2485
|
+
*
|
|
2486
|
+
* - a newcomer posts a join request and pulls the wrapped key blob addressed
|
|
2487
|
+
* to it, unwrapping with its identity private key;
|
|
2488
|
+
* - an existing key-holder wraps the group key for a newcomer (ECIES to the
|
|
2489
|
+
* newcomer's identity public key) and posts the opaque blob;
|
|
2490
|
+
* - on membership change (rotate spaces) a fresh epoch key is minted and
|
|
2491
|
+
* re-wrapped to the current roster.
|
|
2492
|
+
*
|
|
2493
|
+
* The server only ever stores opaque {@link WrappedKey} blobs — it cannot read
|
|
2494
|
+
* the group key. Implements {@link EpochKeyProvider} so it can back a
|
|
2495
|
+
* {@link SpacePacketCipher} directly.
|
|
2496
|
+
*/
|
|
2497
|
+
|
|
2498
|
+
/**
|
|
2499
|
+
* Reserved member id for the app's "keeper" — a trusted always-available member
|
|
2500
|
+
* (the accelerator's AppDOv1) that holds the group key and re-issues it to
|
|
2501
|
+
* newcomers, so a channel is joinable with no human online. Admitting this id
|
|
2502
|
+
* at channel creation hands the key to the keeper. MUST match the accelerator's
|
|
2503
|
+
* `KEEPER_MEMBER_ID`.
|
|
2504
|
+
*/
|
|
2505
|
+
declare const KEEPER_MEMBER_ID = "__keeper__";
|
|
2506
|
+
/** Opaque-blob transport to the space's keyring (HTTP or in-memory test seam). */
|
|
2507
|
+
interface KeyringTransport {
|
|
2508
|
+
postJoinRequest(req: JoinRequest): Promise<void>;
|
|
2509
|
+
/** All wrapped blobs addressed to `memberId` (any epoch). */
|
|
2510
|
+
fetchBlobs(memberId: string): Promise<WrappedKey[]>;
|
|
2511
|
+
/** Post a wrapped key blob for `targetMemberId`. */
|
|
2512
|
+
postWrappedKey(targetMemberId: string, wrapped: WrappedKey, fromMemberId: string): Promise<void>;
|
|
2513
|
+
/** Outstanding join requests (for an online key-holder to fulfill). */
|
|
2514
|
+
fetchPending(): Promise<JoinRequest[]>;
|
|
2515
|
+
/** Current roster: member ids + their identity ECDH/ECDSA public keys. */
|
|
2516
|
+
fetchRoster(): Promise<RosterMember[]>;
|
|
2517
|
+
/** Advisory epoch bump; returns the agreed new epoch. */
|
|
2518
|
+
rotate(nextEpoch: number): Promise<{
|
|
2519
|
+
epoch: number;
|
|
2520
|
+
}>;
|
|
2521
|
+
/** Add `username` to a private channel's membership allowlist. */
|
|
2522
|
+
invite(username: string): Promise<void>;
|
|
2523
|
+
fetchMetadata(): Promise<SpaceMetadata | null>;
|
|
2524
|
+
}
|
|
2525
|
+
/** PersonalSpace-backed cache of this member's group keys (epoch → base64 key). */
|
|
2526
|
+
interface SpaceKeyCache {
|
|
2527
|
+
loadKeys(spaceId: string): Promise<Record<string, string> | null>;
|
|
2528
|
+
saveKeys(spaceId: string, keys: Record<string, string>): Promise<void>;
|
|
2529
|
+
}
|
|
2530
|
+
interface SpaceKeyringDeps {
|
|
2531
|
+
spaceId: string;
|
|
2532
|
+
memberId: string;
|
|
2533
|
+
/** This member's identity ECDH public key, base64url JWK (for join requests). */
|
|
2534
|
+
identityEcdhPub: string;
|
|
2535
|
+
/** This member's identity ECDSA public key, base64url JWK (published for
|
|
2536
|
+
* sender-signature verification). */
|
|
2537
|
+
identityEcdsaPub?: string;
|
|
2538
|
+
/** Resolves this member's identity ECDH private key (for unwrapping). */
|
|
2539
|
+
ownPrivateKey: () => CryptoKey | null;
|
|
2540
|
+
transport: KeyringTransport;
|
|
2541
|
+
cache?: SpaceKeyCache;
|
|
2542
|
+
historyPolicy?: HistoryPolicy;
|
|
2543
|
+
}
|
|
2544
|
+
declare class SpaceKeyring implements EpochKeyProvider {
|
|
2545
|
+
private readonly deps;
|
|
2546
|
+
private readonly keys;
|
|
2547
|
+
private epoch;
|
|
2548
|
+
private readonly policy;
|
|
2549
|
+
/** Cached member directory: memberId → imported ECDSA verify key. */
|
|
2550
|
+
private readonly memberEcdsa;
|
|
2551
|
+
constructor(deps: SpaceKeyringDeps);
|
|
2552
|
+
/** The cached ECDSA verify key for a member, if known. */
|
|
2553
|
+
ecdsaKeyFor(memberId: string): CryptoKey | undefined;
|
|
2554
|
+
/** Refresh the member directory (memberId → ECDSA key) from the roster. */
|
|
2555
|
+
refreshDirectory(): Promise<void>;
|
|
2556
|
+
currentEpoch(): number;
|
|
2557
|
+
keyForEpoch(epoch: number): Uint8Array | undefined;
|
|
2558
|
+
hasAnyKey(): boolean;
|
|
2559
|
+
/** Hydrate keys from the PersonalSpace cache, if available. Best-effort —
|
|
2560
|
+
* a cache miss/failure (e.g. not unlocked) must not block joining. */
|
|
2561
|
+
loadFromCache(): Promise<boolean>;
|
|
2562
|
+
private persist;
|
|
2563
|
+
/** Creator path: mint epoch-0 group key for a brand-new space. */
|
|
2564
|
+
bootstrapNew(): Promise<void>;
|
|
2565
|
+
/**
|
|
2566
|
+
* Post a join request so an online key-holder can wrap a key for us. Also
|
|
2567
|
+
* publishes our identity keys in the member directory (so others can verify
|
|
2568
|
+
* our message signatures, and the keeper can wrap to us).
|
|
2569
|
+
*/
|
|
2570
|
+
requestKey(desiredEpoch?: number): Promise<void>;
|
|
2571
|
+
/**
|
|
2572
|
+
* Pull and unwrap any blobs addressed to us. Returns the number of new
|
|
2573
|
+
* epoch keys obtained. Safe to call repeatedly (idempotent).
|
|
2574
|
+
*/
|
|
2575
|
+
pullKeys(): Promise<number>;
|
|
2576
|
+
/**
|
|
2577
|
+
* Wrap our group key(s) for a newcomer and post the blobs. In a `static`
|
|
2578
|
+
* space the newcomer receives every epoch we hold (full history); in a
|
|
2579
|
+
* `rotate` space they receive only the current epoch.
|
|
2580
|
+
*/
|
|
2581
|
+
admit(targetMemberId: string, identityEcdhPub: string): Promise<void>;
|
|
2582
|
+
/**
|
|
2583
|
+
* Admit every outstanding join request (an online key-holder fulfilling
|
|
2584
|
+
* pending newcomers). Returns the ids admitted.
|
|
2585
|
+
*/
|
|
2586
|
+
admitPending(): Promise<string[]>;
|
|
2587
|
+
/**
|
|
2588
|
+
* Rotate to a fresh epoch (rotate spaces). Mints a new group key, advances
|
|
2589
|
+
* the epoch, and re-wraps it to every member in `roster` (plus ourselves).
|
|
2590
|
+
*/
|
|
2591
|
+
rotate(roster?: Array<{
|
|
2592
|
+
memberId: string;
|
|
2593
|
+
identityEcdhPub: string;
|
|
2594
|
+
}>): Promise<number>;
|
|
2595
|
+
/** Add `username` to a private channel's membership allowlist (member-only). */
|
|
2596
|
+
invite(username: string): Promise<void>;
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
/**
|
|
2600
|
+
* `Space` — a client handle for one shared space.
|
|
2601
|
+
*
|
|
2602
|
+
* A space is the unit everything is built on. It carries:
|
|
2603
|
+
* - **realtime transport** over a {@link BroadcastChannel} (group handshake,
|
|
2604
|
+
* roster frames, raw-frame passthrough);
|
|
2605
|
+
* - **fan-out group messaging** — `Message`s sealed once with the space's
|
|
2606
|
+
* group key (via {@link SpacePacketCipher}), wrapped in a cleartext
|
|
2607
|
+
* {@link Packet} envelope, broadcast AND persisted by the server as
|
|
2608
|
+
* history (the `spaceMessage` frame);
|
|
2609
|
+
* - **room-scoped file storage** via {@link FileStorage} over the shard
|
|
2610
|
+
* endpoint;
|
|
2611
|
+
* - **legacy per-peer Double Ratchet messaging** (`send`/`announce`) for the
|
|
2612
|
+
* ephemeral, no-history path — unchanged so existing callers (the chat hook
|
|
2613
|
+
* reached via `client.message.room(name)`) keep working.
|
|
2614
|
+
*
|
|
2615
|
+
* `Space` was formerly `Room`; `Room` remains exported as an alias. The
|
|
2616
|
+
* fan-out methods (`sendMessage`/`onMessage`/`history`/`join`/`admit`/`rotate`)
|
|
2617
|
+
* require a {@link SpaceKeyring} dep; without one only the transport + DR +
|
|
2618
|
+
* file surface is available.
|
|
2348
2619
|
*/
|
|
2349
2620
|
|
|
2350
2621
|
type Listener = (e: CustomEvent) => void;
|
|
2351
|
-
interface
|
|
2622
|
+
interface SpaceFileMetadata {
|
|
2352
2623
|
name: string;
|
|
2353
2624
|
type: string;
|
|
2354
2625
|
path?: string;
|
|
2355
2626
|
lastModified?: number;
|
|
2356
2627
|
}
|
|
2357
|
-
|
|
2628
|
+
/** A decrypted inbound fan-out message. */
|
|
2629
|
+
interface SpaceMessageEvent {
|
|
2630
|
+
from: string;
|
|
2631
|
+
channel: string;
|
|
2632
|
+
epoch: number;
|
|
2633
|
+
contentType?: string;
|
|
2634
|
+
message: Message;
|
|
2635
|
+
}
|
|
2636
|
+
interface SpaceDeps {
|
|
2637
|
+
/** Transport room id. For fan-out spaces this is the encoded space pubkey. */
|
|
2358
2638
|
name: string;
|
|
2359
2639
|
wsBaseUrl: string;
|
|
2360
2640
|
httpBaseUrl: string;
|
|
2361
2641
|
/** Header-injecting fetch from the client's HttpClient. */
|
|
2362
2642
|
fetch: typeof fetch;
|
|
2363
|
-
/** Resolves the user id used for the ratchet (
|
|
2643
|
+
/** Resolves the user id used for the ratchet / member id (signed-in username). */
|
|
2364
2644
|
myId: () => string;
|
|
2365
2645
|
/** Resolves a short-lived WS upgrade ticket (or null when keyless). */
|
|
2366
2646
|
fetchTicket: () => Promise<string | null>;
|
|
2647
|
+
/** Optional group-key keyring — required for the fan-out methods. */
|
|
2648
|
+
keyring?: SpaceKeyring;
|
|
2649
|
+
/** History policy for this space (advisory; defaults to static). */
|
|
2650
|
+
historyPolicy?: HistoryPolicy;
|
|
2651
|
+
/**
|
|
2652
|
+
* When true (default), a connected member that holds the group key
|
|
2653
|
+
* automatically wraps it for any newcomer that posts a join request —
|
|
2654
|
+
* the "any online key-holder admits" model. Set false to gate membership
|
|
2655
|
+
* yourself via `onJoinRequest` + `admit`.
|
|
2656
|
+
*/
|
|
2657
|
+
autoAdmit?: boolean;
|
|
2658
|
+
/**
|
|
2659
|
+
* Test seam — defaults to constructing a real {@link BroadcastChannel}.
|
|
2660
|
+
* A test can inject an in-memory channel to exercise the fan-out wire
|
|
2661
|
+
* without a live websocket.
|
|
2662
|
+
*/
|
|
2663
|
+
createChannel?: (url: string, myId: string) => SpaceChannelLike;
|
|
2664
|
+
}
|
|
2665
|
+
/** The subset of {@link BroadcastChannel} a {@link Space} drives. */
|
|
2666
|
+
interface SpaceChannelLike {
|
|
2667
|
+
on(event: string, handler: (e: CustomEvent) => void): void;
|
|
2668
|
+
off(event: string, handler: (e: CustomEvent) => void): void;
|
|
2669
|
+
connect(): Promise<void>;
|
|
2670
|
+
disconnect(): void;
|
|
2671
|
+
announce(): Promise<void>;
|
|
2672
|
+
send(plaintext: string): Promise<number>;
|
|
2673
|
+
sendRaw(frame: unknown): void;
|
|
2674
|
+
peers?(): string[];
|
|
2675
|
+
isConnected?(): boolean;
|
|
2367
2676
|
}
|
|
2368
|
-
declare class
|
|
2677
|
+
declare class Space {
|
|
2369
2678
|
private readonly deps;
|
|
2370
2679
|
readonly name: string;
|
|
2680
|
+
readonly historyPolicy: HistoryPolicy;
|
|
2371
2681
|
private channel;
|
|
2372
2682
|
private readonly pending;
|
|
2373
|
-
|
|
2374
|
-
/**
|
|
2683
|
+
private readonly cipher;
|
|
2684
|
+
/** Per-instance event bus for fan-out events (isolated across spaces). */
|
|
2685
|
+
private readonly events;
|
|
2686
|
+
private rawWired;
|
|
2687
|
+
constructor(deps: SpaceDeps);
|
|
2688
|
+
/** The encoded space id (same as the transport name for fan-out spaces). */
|
|
2689
|
+
get id(): string;
|
|
2690
|
+
/** The keyring backing this space's group keys, if any. */
|
|
2691
|
+
get keyring(): SpaceKeyring | undefined;
|
|
2692
|
+
/** Open the space websocket (resolves the ticket + identity lazily). */
|
|
2375
2693
|
connect(): Promise<void>;
|
|
2376
2694
|
disconnect(): void;
|
|
2377
|
-
/** Subscribe to a channel event. Buffered until `connect()` if early. */
|
|
2695
|
+
/** Subscribe to a raw channel event. Buffered until `connect()` if early. */
|
|
2378
2696
|
on(event: string, handler: Listener): void;
|
|
2379
2697
|
off(event: string, handler: Listener): void;
|
|
2380
2698
|
/** Advertise our keyExchange to the room (idempotent per connection). */
|
|
2381
2699
|
announce(): Promise<void>;
|
|
2382
|
-
/** E2E-encrypt `text` to every peer
|
|
2700
|
+
/** E2E-encrypt `text` to every peer via the Double Ratchet (no history). */
|
|
2383
2701
|
send(text: string): Promise<number>;
|
|
2384
2702
|
/** Send an arbitrary JSON frame (e.g. the `{name}` handshake). */
|
|
2385
2703
|
sendRaw(frame: unknown): void;
|
|
2386
2704
|
peers(): string[];
|
|
2387
2705
|
isConnected(): boolean;
|
|
2388
2706
|
/** The underlying channel, for advanced use. `null` before `connect()`. */
|
|
2389
|
-
get raw():
|
|
2390
|
-
/**
|
|
2391
|
-
|
|
2707
|
+
get raw(): SpaceChannelLike | null;
|
|
2708
|
+
/**
|
|
2709
|
+
* Seal `payload` once with the current group key and broadcast it as a
|
|
2710
|
+
* persisted `spaceMessage`. Requires a keyring.
|
|
2711
|
+
*/
|
|
2712
|
+
sendMessage(payload: unknown, opts?: {
|
|
2713
|
+
channel?: string;
|
|
2714
|
+
contentType?: string;
|
|
2715
|
+
}): Promise<void>;
|
|
2716
|
+
/** Subscribe to decrypted fan-out messages. Returns an unsubscribe fn. */
|
|
2717
|
+
onMessage(handler: (e: SpaceMessageEvent) => void): () => void;
|
|
2718
|
+
/** Subscribe to inbound join requests (for a key-holder to admit). */
|
|
2719
|
+
onJoinRequest(handler: (req: JoinRequest) => void): () => void;
|
|
2720
|
+
/**
|
|
2721
|
+
* Fetch + decrypt persisted history beyond the live backlog.
|
|
2722
|
+
* `before` is an opaque cursor returned as `nextCursor`.
|
|
2723
|
+
*/
|
|
2724
|
+
history(opts?: {
|
|
2725
|
+
before?: string;
|
|
2726
|
+
limit?: number;
|
|
2727
|
+
}): Promise<{
|
|
2728
|
+
messages: SpaceMessageEvent[];
|
|
2729
|
+
nextCursor: string | null;
|
|
2730
|
+
}>;
|
|
2731
|
+
/**
|
|
2732
|
+
* Bring this member's group keys up to date: load from cache, and if we
|
|
2733
|
+
* hold nothing, request a key and poll until one arrives (or timeout).
|
|
2734
|
+
*/
|
|
2735
|
+
join(opts?: {
|
|
2736
|
+
timeoutMs?: number;
|
|
2737
|
+
pollMs?: number;
|
|
2738
|
+
}): Promise<boolean>;
|
|
2739
|
+
/** Creator path: mint the initial group key, then connect. */
|
|
2740
|
+
create(): Promise<void>;
|
|
2741
|
+
/** Wrap the group key for a newcomer and post it (key-holder action). */
|
|
2742
|
+
admit(memberId: string, identityEcdhPub: string): Promise<void>;
|
|
2743
|
+
/** Invite a user to this (private) channel's membership allowlist. */
|
|
2744
|
+
invite(username: string): Promise<void>;
|
|
2745
|
+
/** Rotate to a fresh epoch and re-wrap to the roster (rotate spaces). */
|
|
2746
|
+
rotate(roster?: Array<{
|
|
2747
|
+
memberId: string;
|
|
2748
|
+
identityEcdhPub: string;
|
|
2749
|
+
}>): Promise<number>;
|
|
2750
|
+
putFile(file: File | Blob | Uint8Array, metadata: SpaceFileMetadata): Promise<{
|
|
2392
2751
|
manifest: FileManifest;
|
|
2393
2752
|
stat: FileStat;
|
|
2394
2753
|
}>;
|
|
2395
|
-
/** Fetch + decode a file previously shared in this room. */
|
|
2396
2754
|
getFile(manifest: FileManifest): Promise<{
|
|
2397
2755
|
data: Uint8Array;
|
|
2398
2756
|
stat: FileStat;
|
|
2399
2757
|
}>;
|
|
2400
2758
|
private fileStorage;
|
|
2759
|
+
/** Wire the RAW_FRAME handler that drives fan-out events. */
|
|
2760
|
+
private wireFanout;
|
|
2761
|
+
private onRawFrame;
|
|
2762
|
+
/**
|
|
2763
|
+
* Decode one persisted/broadcast frame into a SpaceMessageEvent.
|
|
2764
|
+
* `isRawFrame` true means `input` is the already-extracted `spaceMessage`
|
|
2765
|
+
* packet string; false means it's the full `{spaceMessage, name, ...}` frame.
|
|
2766
|
+
*/
|
|
2767
|
+
private decodeFrame;
|
|
2768
|
+
/**
|
|
2769
|
+
* True if `packet` carries a valid signature by `source`'s identity key.
|
|
2770
|
+
* On a directory cache miss, refresh once (a new member may have just
|
|
2771
|
+
* joined) before giving up. Without a keyring (legacy handle) there's
|
|
2772
|
+
* nothing to verify against → reject.
|
|
2773
|
+
*/
|
|
2774
|
+
private verifySender;
|
|
2775
|
+
private emitEvent;
|
|
2776
|
+
private requireKeyring;
|
|
2777
|
+
private requireCipher;
|
|
2401
2778
|
}
|
|
2402
2779
|
|
|
2403
2780
|
/**
|
|
@@ -2484,7 +2861,7 @@ declare class MessageNamespace {
|
|
|
2484
2861
|
* primitive group chat is built on (`publish`/`subscribe`/`send` are the
|
|
2485
2862
|
* higher-level conveniences layered over the same space transport).
|
|
2486
2863
|
*/
|
|
2487
|
-
room(roomName: string):
|
|
2864
|
+
room(roomName: string): Space;
|
|
2488
2865
|
/** Close every open room connection (e.g. on logout). */
|
|
2489
2866
|
disconnect(): void;
|
|
2490
2867
|
private getRoom;
|
|
@@ -2494,6 +2871,88 @@ declare class MessageNamespace {
|
|
|
2494
2871
|
private myId;
|
|
2495
2872
|
}
|
|
2496
2873
|
|
|
2874
|
+
/**
|
|
2875
|
+
* `client.space` — the fan-out group-encryption surface.
|
|
2876
|
+
*
|
|
2877
|
+
* const space = await client.space.createSpace({ historyPolicy: "static" });
|
|
2878
|
+
* await space.sendMessage({ text: "hello" });
|
|
2879
|
+
* space.onMessage(e => …);
|
|
2880
|
+
*
|
|
2881
|
+
* const joined = await client.space.joinSpace(space.id); // newcomer
|
|
2882
|
+
*
|
|
2883
|
+
* A space owns a client-generated keypair whose public key is its id, a
|
|
2884
|
+
* symmetric group key distributed server-blind via the keyring, and a
|
|
2885
|
+
* persisted message log. This namespace wires the {@link Space} handle to the
|
|
2886
|
+
* HTTP {@link KeyringClient}, the member's identity keys (from {@link KeyStore},
|
|
2887
|
+
* shared with the Double Ratchet path), and a PersonalSpace-backed key cache.
|
|
2888
|
+
*/
|
|
2889
|
+
|
|
2890
|
+
/** Thrown by `joinChannel` when no channel with that name is registered. */
|
|
2891
|
+
declare class ChannelNotFoundError extends Error {
|
|
2892
|
+
readonly channelName: string;
|
|
2893
|
+
constructor(channelName: string);
|
|
2894
|
+
}
|
|
2895
|
+
/** Thrown by `createChannel` when the name is already registered. */
|
|
2896
|
+
declare class ChannelExistsError extends Error {
|
|
2897
|
+
readonly channelName: string;
|
|
2898
|
+
constructor(channelName: string);
|
|
2899
|
+
}
|
|
2900
|
+
interface SpaceNamespaceDeps {
|
|
2901
|
+
http: HttpClient;
|
|
2902
|
+
session: SessionState;
|
|
2903
|
+
/** WebSocket base (ws/wss); derived from the accelerator baseUrl. */
|
|
2904
|
+
wsBaseUrl: string;
|
|
2905
|
+
/** Optional PersonalSpace-backed cache of group keys (encrypted at rest). */
|
|
2906
|
+
cache?: SpaceKeyCache;
|
|
2907
|
+
}
|
|
2908
|
+
declare class SpaceNamespace {
|
|
2909
|
+
private readonly deps;
|
|
2910
|
+
constructor(deps: SpaceNamespaceDeps);
|
|
2911
|
+
/**
|
|
2912
|
+
* Create a new space: generate its keypair (public key = id), register
|
|
2913
|
+
* metadata, mint the initial group key, and connect. Returns the open Space.
|
|
2914
|
+
*/
|
|
2915
|
+
createSpace(opts?: {
|
|
2916
|
+
historyPolicy?: HistoryPolicy;
|
|
2917
|
+
private?: boolean;
|
|
2918
|
+
}): Promise<Space>;
|
|
2919
|
+
/** Join an existing space by id (its public key). Resolves once a key arrives. */
|
|
2920
|
+
joinSpace(spaceId: string, opts?: {
|
|
2921
|
+
timeoutMs?: number;
|
|
2922
|
+
}): Promise<Space>;
|
|
2923
|
+
/** A handle to a space without auto-connecting (uses cached keys on demand). */
|
|
2924
|
+
get(spaceId: string, historyPolicy?: HistoryPolicy): Promise<Space>;
|
|
2925
|
+
/** List this app's channels (name → space-id) for the key's environment. */
|
|
2926
|
+
listChannels(): Promise<Array<{
|
|
2927
|
+
name: string;
|
|
2928
|
+
spaceId: string;
|
|
2929
|
+
}>>;
|
|
2930
|
+
/** Resolve a channel name to its space id, or null if it doesn't exist. */
|
|
2931
|
+
resolveChannel(name: string): Promise<string | null>;
|
|
2932
|
+
/**
|
|
2933
|
+
* Create a new channel: mint a fan-out space (the caller becomes its first
|
|
2934
|
+
* key-holder) and register `name → spaceId` in the app directory. Throws
|
|
2935
|
+
* {@link ChannelExistsError} if the name is already taken.
|
|
2936
|
+
*/
|
|
2937
|
+
createChannel(name: string, opts?: {
|
|
2938
|
+
historyPolicy?: HistoryPolicy;
|
|
2939
|
+
private?: boolean;
|
|
2940
|
+
}): Promise<Space>;
|
|
2941
|
+
/**
|
|
2942
|
+
* Join an existing channel by name. Throws {@link ChannelNotFoundError} if
|
|
2943
|
+
* no channel with that name is registered (use {@link createChannel} first).
|
|
2944
|
+
*/
|
|
2945
|
+
joinChannel(name: string, opts?: {
|
|
2946
|
+
timeoutMs?: number;
|
|
2947
|
+
}): Promise<Space>;
|
|
2948
|
+
private build;
|
|
2949
|
+
/** Ensure the member has an identity keypair; return its public ECDH + ECDSA keys. */
|
|
2950
|
+
private ensureIdentity;
|
|
2951
|
+
private fetchPolicy;
|
|
2952
|
+
private fetchTicket;
|
|
2953
|
+
private myId;
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2497
2956
|
/** The hosted Muhkoo Accelerator — the default {@link ClientOptions.baseUrl}. */
|
|
2498
2957
|
declare const DEFAULT_BASE_URL = "https://api.muhkoo.dev";
|
|
2499
2958
|
interface ClientOptions {
|
|
@@ -2535,6 +2994,8 @@ declare class Client {
|
|
|
2535
2994
|
readonly storage: StorageNamespace;
|
|
2536
2995
|
/** Realtime messaging — `client.message.subscribe(...)`, etc. */
|
|
2537
2996
|
readonly message: MessageNamespace;
|
|
2997
|
+
/** Fan-out group spaces — `client.space.createSpace(...)`, etc. */
|
|
2998
|
+
readonly space: SpaceNamespace;
|
|
2538
2999
|
private readonly session;
|
|
2539
3000
|
private readonly http;
|
|
2540
3001
|
constructor(options?: ClientOptions);
|
|
@@ -2544,5 +3005,43 @@ declare class Client {
|
|
|
2544
3005
|
get isAuthenticated(): boolean;
|
|
2545
3006
|
}
|
|
2546
3007
|
|
|
2547
|
-
|
|
2548
|
-
|
|
3008
|
+
/**
|
|
3009
|
+
* KeyringClient — HTTP implementation of {@link KeyringTransport} against the
|
|
3010
|
+
* accelerator's per-space keyring endpoints. All blobs are opaque to the
|
|
3011
|
+
* server; this client just moves them.
|
|
3012
|
+
*
|
|
3013
|
+
* POST /api/spaces/:id/keyring/join-request { ...JoinRequest }
|
|
3014
|
+
* GET /api/spaces/:id/keyring/blobs/:member -> WrappedKey[]
|
|
3015
|
+
* POST /api/spaces/:id/keyring/wrap { targetMemberId, fromMemberId, wrapped }
|
|
3016
|
+
* GET /api/spaces/:id/keyring/pending -> JoinRequest[]
|
|
3017
|
+
* GET /api/spaces/:id/keyring/roster -> { memberId, identityEcdhPub }[]
|
|
3018
|
+
* POST /api/spaces/:id/keyring/rotate { nextEpoch } -> { epoch }
|
|
3019
|
+
* GET /api/spaces/:id/metadata -> SpaceMetadata
|
|
3020
|
+
*/
|
|
3021
|
+
|
|
3022
|
+
interface KeyringClientDeps {
|
|
3023
|
+
spaceId: string;
|
|
3024
|
+
httpBaseUrl: string;
|
|
3025
|
+
/** Header-injecting fetch from the client's HttpClient. */
|
|
3026
|
+
fetch: typeof fetch;
|
|
3027
|
+
}
|
|
3028
|
+
declare class KeyringClient implements KeyringTransport {
|
|
3029
|
+
private readonly deps;
|
|
3030
|
+
constructor(deps: KeyringClientDeps);
|
|
3031
|
+
private url;
|
|
3032
|
+
private post;
|
|
3033
|
+
private get;
|
|
3034
|
+
postJoinRequest(req: JoinRequest): Promise<void>;
|
|
3035
|
+
fetchBlobs(memberId: string): Promise<WrappedKey[]>;
|
|
3036
|
+
postWrappedKey(targetMemberId: string, wrapped: WrappedKey, fromMemberId: string): Promise<void>;
|
|
3037
|
+
fetchPending(): Promise<JoinRequest[]>;
|
|
3038
|
+
fetchRoster(): Promise<RosterMember[]>;
|
|
3039
|
+
rotate(nextEpoch: number): Promise<{
|
|
3040
|
+
epoch: number;
|
|
3041
|
+
}>;
|
|
3042
|
+
invite(username: string): Promise<void>;
|
|
3043
|
+
fetchMetadata(): Promise<SpaceMetadata | null>;
|
|
3044
|
+
}
|
|
3045
|
+
|
|
3046
|
+
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 };
|
|
3047
|
+
export type { ACL, ACLEntry, AuthClientOptions, AuthKeyPair, AuthSuccess, AuthToken, AuthType, AuthUser, Bn128WasmInstance, BroadcastChannelEvent, BroadcastChannelOptions, ChannelLike, ChunkKeyMaterial, ChunkManifest, CipherFrame, CipherMessage, CipherMessageHeader, CircuitBufferConfig, CircuitUrls, ClientOptions, DehydratedKeypair, DehydratedKeys, DoubleRatchetCipherDeps, ECDHCurve, EncodeResult, EpochKeyProvider, FileManifest, FileStat, FileStorageOptions, Groth16Proof, HttpClientOptions, IncomingFrame, KeyExchangeFrame, KeyPair, Keypair, KeyringClientDeps, KeyringTransport, LoginOptions, MessageBody, MessageEvent, MessageHeaders, MessageNamespaceDeps, MessageOptions, MessageSubscription, MuhkooMessageEvent, OAuthProvider, PacketCipher, PacketHeaders, PacketOptions, Participant, Permission, PermissionCheckRequest, PermissionCheckResult, PersonalSpaceClientOptions, PublicKeyJWK, RatchetState, ReceiveResult, RegisterParams, Role, SpaceDeps as RoomDeps, SpaceFileMetadata as RoomFileMetadata, SealedHeaders, Session, SessionOptions, SessionStore, SetOptions, ShardClientOptions, SharedSpaceClientOptions, SnarkProof, SpaceChannelLike, SpaceDeps, SpaceFileMetadata, SpaceKeyCache, SpaceKeyringDeps, SpaceMessageEvent, SpaceNamespaceDeps, StorageChangeEvent, StorageNamespaceDeps, StoredSession, Subscription, UserIdentity, VerificationKey, WSMessage, WSMessageType, WSTransportOptions, WrappedPayload, WriteFileInput, WriteToShardsInput, ZkAuthDeps, ZkAuthenticateRequest, ZkChallengeResponse, ZkCompiled, ZkIdentity, ZkRegisterRequest, ZkRegisterResponse };
|