@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/connect.d.ts CHANGED
@@ -2329,75 +2329,421 @@ declare class StorageNamespace {
2329
2329
  }
2330
2330
 
2331
2331
  /**
2332
- * `Room` — a client handle for one shared space (a chat-style room).
2332
+ * Shared types for the Space fan-out group-encryption layer.
2333
2333
  *
2334
- * A shared space carries two things, and `Room` exposes both:
2335
- * - **realtime E2E messaging** over a {@link BroadcastChannel} (group
2336
- * handshake, ratchet send/receive, roster frames);
2337
- * - **room-scoped file storage** via {@link FileStorage} over the room's
2338
- * shard endpoint.
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
+
2389
+ /**
2390
+ * PacketCipher — the strategy `Network` uses to seal/open a packet's payload.
2339
2391
  *
2340
- * Obtained from `client.message.room(name)`. The websocket isn't opened until
2341
- * `connect()` — and `putFile`/`getFile` don't need it at all (they're plain
2342
- * shard HTTP), so reading a shared file never opens a socket. The upgrade
2343
- * ticket + identity are resolved lazily inside `connect()`.
2392
+ * `Network` owns the wire shape (Message ↔ Packet, header placement, transport,
2393
+ * reconnect, checksum). The *encryption* is pluggable: the default
2394
+ * {@link DoubleRatchetCipher} reproduces the per-peer Double Ratchet behavior
2395
+ * Network has always had, while the Space layer supplies a group-key cipher.
2344
2396
  *
2345
- * `Room` intentionally mirrors the `BroadcastChannel` surface the chat hook
2346
- * already uses (`on`/`off`/`send`/`sendRaw`/`announce`/`peers`/`isConnected`)
2347
- * so adopting it is a drop-in swap for `new BroadcastChannel(...)`.
2397
+ * The sealed ciphertext always rides in `Packet.headers` (cleartext routing
2398
+ * fields stay readable to the server); `Packet.message` is left undefined on
2399
+ * the wire so the server never sees the real payload.
2400
+ */
2401
+
2402
+ type PacketHeaders = Packet["headers"];
2403
+ /** Header fields a cipher merges into the outgoing packet. */
2404
+ type SealedHeaders = Record<string, string | number | boolean>;
2405
+ interface PacketCipher {
2406
+ /**
2407
+ * Seal the serialized `Message`. Returns the header fields to merge into
2408
+ * the outgoing packet (these carry the ciphertext). `Network` clears
2409
+ * `packet.message` after calling this.
2410
+ */
2411
+ seal(serializedMessage: string, packet: Packet): Promise<SealedHeaders>;
2412
+ /** True if the inbound headers look like this cipher's sealed form. */
2413
+ handles(headers: PacketHeaders): boolean;
2414
+ /**
2415
+ * Open an inbound packet's headers back into a serialized `Message` string.
2416
+ * Returns `null` when the packet is not sealed for / not decryptable by us
2417
+ * (e.g. an epoch key we don't hold) — `Network` then leaves `packet.message`
2418
+ * untouched rather than throwing.
2419
+ */
2420
+ open(headers: PacketHeaders): Promise<string | null>;
2421
+ }
2422
+ interface DoubleRatchetCipherDeps {
2423
+ ratchetManager: DoubleRatchetManager;
2424
+ clientId: string;
2425
+ serverId: string;
2426
+ sessionType: "global" | "specific";
2427
+ /** Resolves the live session id (Network manages the session lifecycle). */
2428
+ getSessionId: () => string | null;
2429
+ /** Whether this side is the ratchet "client" (default true). */
2430
+ isClient?: boolean;
2431
+ }
2432
+ /**
2433
+ * Default cipher: per-peer Double Ratchet. Reproduces Network's original
2434
+ * `{ encrypted: true, cipherMessage: <json> }` header convention exactly so
2435
+ * existing consumers are unaffected.
2436
+ */
2437
+ declare class DoubleRatchetCipher implements PacketCipher {
2438
+ private readonly deps;
2439
+ constructor(deps: DoubleRatchetCipherDeps);
2440
+ seal(serializedMessage: string): Promise<SealedHeaders>;
2441
+ handles(headers: PacketHeaders): boolean;
2442
+ open(headers: PacketHeaders): Promise<string | null>;
2443
+ }
2444
+
2445
+ /**
2446
+ * SpacePacketCipher — the {@link PacketCipher} that seals packets with a
2447
+ * space's group key instead of a per-peer Double Ratchet.
2448
+ *
2449
+ * It pulls the active epoch's key from an {@link EpochKeyProvider} (the
2450
+ * SpaceKeyring), so this adapter stays decoupled from the keyring's transport
2451
+ * and caching concerns — it only needs "what key seals epoch N".
2452
+ */
2453
+
2454
+ /** Minimal view of the keyring the cipher needs. */
2455
+ interface EpochKeyProvider {
2456
+ /** Epoch new messages are sealed under (advances on rotation). */
2457
+ currentEpoch(): number;
2458
+ /** The 32-byte group key for an epoch, or undefined if we don't hold it. */
2459
+ keyForEpoch(epoch: number): Uint8Array | undefined;
2460
+ }
2461
+ declare class SpacePacketCipher implements PacketCipher {
2462
+ private readonly keys;
2463
+ private readonly contentType?;
2464
+ constructor(keys: EpochKeyProvider, contentType?: string | undefined);
2465
+ seal(serializedMessage: string): Promise<SealedHeaders>;
2466
+ handles(headers: PacketHeaders): boolean;
2467
+ /** Returns null for epochs we don't hold (surfaced as undecryptable). */
2468
+ open(headers: PacketHeaders): Promise<string | null>;
2469
+ }
2470
+
2471
+ /**
2472
+ * SpaceKeyring — the client's view of a space's group keys.
2473
+ *
2474
+ * Holds the per-epoch group keys this member possesses (in memory, optionally
2475
+ * mirrored to PersonalSpace), and orchestrates the server-blind distribution
2476
+ * dance against a {@link KeyringTransport}:
2477
+ *
2478
+ * - a newcomer posts a join request and pulls the wrapped key blob addressed
2479
+ * to it, unwrapping with its identity private key;
2480
+ * - an existing key-holder wraps the group key for a newcomer (ECIES to the
2481
+ * newcomer's identity public key) and posts the opaque blob;
2482
+ * - on membership change (rotate spaces) a fresh epoch key is minted and
2483
+ * re-wrapped to the current roster.
2484
+ *
2485
+ * The server only ever stores opaque {@link WrappedKey} blobs — it cannot read
2486
+ * the group key. Implements {@link EpochKeyProvider} so it can back a
2487
+ * {@link SpacePacketCipher} directly.
2488
+ */
2489
+
2490
+ /**
2491
+ * Reserved member id for the app's "keeper" — a trusted always-available member
2492
+ * (the accelerator's AppDOv1) that holds the group key and re-issues it to
2493
+ * newcomers, so a channel is joinable with no human online. Admitting this id
2494
+ * at channel creation hands the key to the keeper. MUST match the accelerator's
2495
+ * `KEEPER_MEMBER_ID`.
2496
+ */
2497
+ declare const KEEPER_MEMBER_ID = "__keeper__";
2498
+ /** Opaque-blob transport to the space's keyring (HTTP or in-memory test seam). */
2499
+ interface KeyringTransport {
2500
+ postJoinRequest(req: JoinRequest): Promise<void>;
2501
+ /** All wrapped blobs addressed to `memberId` (any epoch). */
2502
+ fetchBlobs(memberId: string): Promise<WrappedKey[]>;
2503
+ /** Post a wrapped key blob for `targetMemberId`. */
2504
+ postWrappedKey(targetMemberId: string, wrapped: WrappedKey, fromMemberId: string): Promise<void>;
2505
+ /** Outstanding join requests (for an online key-holder to fulfill). */
2506
+ fetchPending(): Promise<JoinRequest[]>;
2507
+ /** Current roster: member ids + their identity ECDH public keys. */
2508
+ fetchRoster(): Promise<Array<{
2509
+ memberId: string;
2510
+ identityEcdhPub: string;
2511
+ }>>;
2512
+ /** Advisory epoch bump; returns the agreed new epoch. */
2513
+ rotate(nextEpoch: number): Promise<{
2514
+ epoch: number;
2515
+ }>;
2516
+ fetchMetadata(): Promise<SpaceMetadata | null>;
2517
+ }
2518
+ /** PersonalSpace-backed cache of this member's group keys (epoch → base64 key). */
2519
+ interface SpaceKeyCache {
2520
+ loadKeys(spaceId: string): Promise<Record<string, string> | null>;
2521
+ saveKeys(spaceId: string, keys: Record<string, string>): Promise<void>;
2522
+ }
2523
+ interface SpaceKeyringDeps {
2524
+ spaceId: string;
2525
+ memberId: string;
2526
+ /** This member's identity ECDH public key, base64url JWK (for join requests). */
2527
+ identityEcdhPub: string;
2528
+ /** Resolves this member's identity ECDH private key (for unwrapping). */
2529
+ ownPrivateKey: () => CryptoKey | null;
2530
+ transport: KeyringTransport;
2531
+ cache?: SpaceKeyCache;
2532
+ historyPolicy?: HistoryPolicy;
2533
+ }
2534
+ declare class SpaceKeyring implements EpochKeyProvider {
2535
+ private readonly deps;
2536
+ private readonly keys;
2537
+ private epoch;
2538
+ private readonly policy;
2539
+ constructor(deps: SpaceKeyringDeps);
2540
+ currentEpoch(): number;
2541
+ keyForEpoch(epoch: number): Uint8Array | undefined;
2542
+ hasAnyKey(): boolean;
2543
+ /** Hydrate keys from the PersonalSpace cache, if available. Best-effort —
2544
+ * a cache miss/failure (e.g. not unlocked) must not block joining. */
2545
+ loadFromCache(): Promise<boolean>;
2546
+ private persist;
2547
+ /** Creator path: mint epoch-0 group key for a brand-new space. */
2548
+ bootstrapNew(): Promise<void>;
2549
+ /** Post a join request so an online key-holder can wrap a key for us. */
2550
+ requestKey(desiredEpoch?: number): Promise<void>;
2551
+ /**
2552
+ * Pull and unwrap any blobs addressed to us. Returns the number of new
2553
+ * epoch keys obtained. Safe to call repeatedly (idempotent).
2554
+ */
2555
+ pullKeys(): Promise<number>;
2556
+ /**
2557
+ * Wrap our group key(s) for a newcomer and post the blobs. In a `static`
2558
+ * space the newcomer receives every epoch we hold (full history); in a
2559
+ * `rotate` space they receive only the current epoch.
2560
+ */
2561
+ admit(targetMemberId: string, identityEcdhPub: string): Promise<void>;
2562
+ /**
2563
+ * Admit every outstanding join request (an online key-holder fulfilling
2564
+ * pending newcomers). Returns the ids admitted.
2565
+ */
2566
+ admitPending(): Promise<string[]>;
2567
+ /**
2568
+ * Rotate to a fresh epoch (rotate spaces). Mints a new group key, advances
2569
+ * the epoch, and re-wraps it to every member in `roster` (plus ourselves).
2570
+ */
2571
+ rotate(roster?: Array<{
2572
+ memberId: string;
2573
+ identityEcdhPub: string;
2574
+ }>): Promise<number>;
2575
+ }
2576
+
2577
+ /**
2578
+ * `Space` — a client handle for one shared space.
2579
+ *
2580
+ * A space is the unit everything is built on. It carries:
2581
+ * - **realtime transport** over a {@link BroadcastChannel} (group handshake,
2582
+ * roster frames, raw-frame passthrough);
2583
+ * - **fan-out group messaging** — `Message`s sealed once with the space's
2584
+ * group key (via {@link SpacePacketCipher}), wrapped in a cleartext
2585
+ * {@link Packet} envelope, broadcast AND persisted by the server as
2586
+ * history (the `spaceMessage` frame);
2587
+ * - **room-scoped file storage** via {@link FileStorage} over the shard
2588
+ * endpoint;
2589
+ * - **legacy per-peer Double Ratchet messaging** (`send`/`announce`) for the
2590
+ * ephemeral, no-history path — unchanged so existing callers (the chat hook
2591
+ * reached via `client.message.room(name)`) keep working.
2592
+ *
2593
+ * `Space` was formerly `Room`; `Room` remains exported as an alias. The
2594
+ * fan-out methods (`sendMessage`/`onMessage`/`history`/`join`/`admit`/`rotate`)
2595
+ * require a {@link SpaceKeyring} dep; without one only the transport + DR +
2596
+ * file surface is available.
2348
2597
  */
2349
2598
 
2350
2599
  type Listener = (e: CustomEvent) => void;
2351
- interface RoomFileMetadata {
2600
+ interface SpaceFileMetadata {
2352
2601
  name: string;
2353
2602
  type: string;
2354
2603
  path?: string;
2355
2604
  lastModified?: number;
2356
2605
  }
2357
- interface RoomDeps {
2606
+ /** A decrypted inbound fan-out message. */
2607
+ interface SpaceMessageEvent {
2608
+ from: string;
2609
+ channel: string;
2610
+ epoch: number;
2611
+ contentType?: string;
2612
+ message: Message;
2613
+ }
2614
+ interface SpaceDeps {
2615
+ /** Transport room id. For fan-out spaces this is the encoded space pubkey. */
2358
2616
  name: string;
2359
2617
  wsBaseUrl: string;
2360
2618
  httpBaseUrl: string;
2361
2619
  /** Header-injecting fetch from the client's HttpClient. */
2362
2620
  fetch: typeof fetch;
2363
- /** Resolves the user id used for the ratchet (the signed-in username). */
2621
+ /** Resolves the user id used for the ratchet / member id (signed-in username). */
2364
2622
  myId: () => string;
2365
2623
  /** Resolves a short-lived WS upgrade ticket (or null when keyless). */
2366
2624
  fetchTicket: () => Promise<string | null>;
2625
+ /** Optional group-key keyring — required for the fan-out methods. */
2626
+ keyring?: SpaceKeyring;
2627
+ /** History policy for this space (advisory; defaults to static). */
2628
+ historyPolicy?: HistoryPolicy;
2629
+ /**
2630
+ * When true (default), a connected member that holds the group key
2631
+ * automatically wraps it for any newcomer that posts a join request —
2632
+ * the "any online key-holder admits" model. Set false to gate membership
2633
+ * yourself via `onJoinRequest` + `admit`.
2634
+ */
2635
+ autoAdmit?: boolean;
2636
+ /**
2637
+ * Test seam — defaults to constructing a real {@link BroadcastChannel}.
2638
+ * A test can inject an in-memory channel to exercise the fan-out wire
2639
+ * without a live websocket.
2640
+ */
2641
+ createChannel?: (url: string, myId: string) => SpaceChannelLike;
2367
2642
  }
2368
- declare class Room {
2643
+ /** The subset of {@link BroadcastChannel} a {@link Space} drives. */
2644
+ interface SpaceChannelLike {
2645
+ on(event: string, handler: (e: CustomEvent) => void): void;
2646
+ off(event: string, handler: (e: CustomEvent) => void): void;
2647
+ connect(): Promise<void>;
2648
+ disconnect(): void;
2649
+ announce(): Promise<void>;
2650
+ send(plaintext: string): Promise<number>;
2651
+ sendRaw(frame: unknown): void;
2652
+ peers?(): string[];
2653
+ isConnected?(): boolean;
2654
+ }
2655
+ declare class Space {
2369
2656
  private readonly deps;
2370
2657
  readonly name: string;
2658
+ readonly historyPolicy: HistoryPolicy;
2371
2659
  private channel;
2372
2660
  private readonly pending;
2373
- constructor(deps: RoomDeps);
2374
- /** Open the room websocket (resolves the ticket + identity lazily). */
2661
+ private readonly cipher;
2662
+ /** Per-instance event bus for fan-out events (isolated across spaces). */
2663
+ private readonly events;
2664
+ private rawWired;
2665
+ constructor(deps: SpaceDeps);
2666
+ /** The encoded space id (same as the transport name for fan-out spaces). */
2667
+ get id(): string;
2668
+ /** The keyring backing this space's group keys, if any. */
2669
+ get keyring(): SpaceKeyring | undefined;
2670
+ /** Open the space websocket (resolves the ticket + identity lazily). */
2375
2671
  connect(): Promise<void>;
2376
2672
  disconnect(): void;
2377
- /** Subscribe to a channel event. Buffered until `connect()` if early. */
2673
+ /** Subscribe to a raw channel event. Buffered until `connect()` if early. */
2378
2674
  on(event: string, handler: Listener): void;
2379
2675
  off(event: string, handler: Listener): void;
2380
2676
  /** Advertise our keyExchange to the room (idempotent per connection). */
2381
2677
  announce(): Promise<void>;
2382
- /** E2E-encrypt `text` to every peer; returns the recipient count. */
2678
+ /** E2E-encrypt `text` to every peer via the Double Ratchet (no history). */
2383
2679
  send(text: string): Promise<number>;
2384
2680
  /** Send an arbitrary JSON frame (e.g. the `{name}` handshake). */
2385
2681
  sendRaw(frame: unknown): void;
2386
2682
  peers(): string[];
2387
2683
  isConnected(): boolean;
2388
2684
  /** The underlying channel, for advanced use. `null` before `connect()`. */
2389
- get raw(): BroadcastChannel | null;
2390
- /** Encrypt + erasure-code + upload `file` to this room's shards. */
2391
- putFile(file: File | Blob | Uint8Array, metadata: RoomFileMetadata): Promise<{
2685
+ get raw(): SpaceChannelLike | null;
2686
+ /**
2687
+ * Seal `payload` once with the current group key and broadcast it as a
2688
+ * persisted `spaceMessage`. Requires a keyring.
2689
+ */
2690
+ sendMessage(payload: unknown, opts?: {
2691
+ channel?: string;
2692
+ contentType?: string;
2693
+ }): Promise<void>;
2694
+ /** Subscribe to decrypted fan-out messages. Returns an unsubscribe fn. */
2695
+ onMessage(handler: (e: SpaceMessageEvent) => void): () => void;
2696
+ /** Subscribe to inbound join requests (for a key-holder to admit). */
2697
+ onJoinRequest(handler: (req: JoinRequest) => void): () => void;
2698
+ /**
2699
+ * Fetch + decrypt persisted history beyond the live backlog.
2700
+ * `before` is an opaque cursor returned as `nextCursor`.
2701
+ */
2702
+ history(opts?: {
2703
+ before?: string;
2704
+ limit?: number;
2705
+ }): Promise<{
2706
+ messages: SpaceMessageEvent[];
2707
+ nextCursor: string | null;
2708
+ }>;
2709
+ /**
2710
+ * Bring this member's group keys up to date: load from cache, and if we
2711
+ * hold nothing, request a key and poll until one arrives (or timeout).
2712
+ */
2713
+ join(opts?: {
2714
+ timeoutMs?: number;
2715
+ pollMs?: number;
2716
+ }): Promise<boolean>;
2717
+ /** Creator path: mint the initial group key, then connect. */
2718
+ create(): Promise<void>;
2719
+ /** Wrap the group key for a newcomer and post it (key-holder action). */
2720
+ admit(memberId: string, identityEcdhPub: string): Promise<void>;
2721
+ /** Rotate to a fresh epoch and re-wrap to the roster (rotate spaces). */
2722
+ rotate(roster?: Array<{
2723
+ memberId: string;
2724
+ identityEcdhPub: string;
2725
+ }>): Promise<number>;
2726
+ putFile(file: File | Blob | Uint8Array, metadata: SpaceFileMetadata): Promise<{
2392
2727
  manifest: FileManifest;
2393
2728
  stat: FileStat;
2394
2729
  }>;
2395
- /** Fetch + decode a file previously shared in this room. */
2396
2730
  getFile(manifest: FileManifest): Promise<{
2397
2731
  data: Uint8Array;
2398
2732
  stat: FileStat;
2399
2733
  }>;
2400
2734
  private fileStorage;
2735
+ /** Wire the RAW_FRAME handler that drives fan-out events. */
2736
+ private wireFanout;
2737
+ private onRawFrame;
2738
+ /**
2739
+ * Decode one persisted/broadcast frame into a SpaceMessageEvent.
2740
+ * `isRawFrame` true means `input` is the already-extracted `spaceMessage`
2741
+ * packet string; false means it's the full `{spaceMessage, name, ...}` frame.
2742
+ */
2743
+ private decodeFrame;
2744
+ private emitEvent;
2745
+ private requireKeyring;
2746
+ private requireCipher;
2401
2747
  }
2402
2748
 
2403
2749
  /**
@@ -2484,7 +2830,7 @@ declare class MessageNamespace {
2484
2830
  * primitive group chat is built on (`publish`/`subscribe`/`send` are the
2485
2831
  * higher-level conveniences layered over the same space transport).
2486
2832
  */
2487
- room(roomName: string): Room;
2833
+ room(roomName: string): Space;
2488
2834
  /** Close every open room connection (e.g. on logout). */
2489
2835
  disconnect(): void;
2490
2836
  private getRoom;
@@ -2494,6 +2840,86 @@ declare class MessageNamespace {
2494
2840
  private myId;
2495
2841
  }
2496
2842
 
2843
+ /**
2844
+ * `client.space` — the fan-out group-encryption surface.
2845
+ *
2846
+ * const space = await client.space.createSpace({ historyPolicy: "static" });
2847
+ * await space.sendMessage({ text: "hello" });
2848
+ * space.onMessage(e => …);
2849
+ *
2850
+ * const joined = await client.space.joinSpace(space.id); // newcomer
2851
+ *
2852
+ * A space owns a client-generated keypair whose public key is its id, a
2853
+ * symmetric group key distributed server-blind via the keyring, and a
2854
+ * persisted message log. This namespace wires the {@link Space} handle to the
2855
+ * HTTP {@link KeyringClient}, the member's identity keys (from {@link KeyStore},
2856
+ * shared with the Double Ratchet path), and a PersonalSpace-backed key cache.
2857
+ */
2858
+
2859
+ /** Thrown by `joinChannel` when no channel with that name is registered. */
2860
+ declare class ChannelNotFoundError extends Error {
2861
+ readonly channelName: string;
2862
+ constructor(channelName: string);
2863
+ }
2864
+ /** Thrown by `createChannel` when the name is already registered. */
2865
+ declare class ChannelExistsError extends Error {
2866
+ readonly channelName: string;
2867
+ constructor(channelName: string);
2868
+ }
2869
+ interface SpaceNamespaceDeps {
2870
+ http: HttpClient;
2871
+ session: SessionState;
2872
+ /** WebSocket base (ws/wss); derived from the accelerator baseUrl. */
2873
+ wsBaseUrl: string;
2874
+ /** Optional PersonalSpace-backed cache of group keys (encrypted at rest). */
2875
+ cache?: SpaceKeyCache;
2876
+ }
2877
+ declare class SpaceNamespace {
2878
+ private readonly deps;
2879
+ constructor(deps: SpaceNamespaceDeps);
2880
+ /**
2881
+ * Create a new space: generate its keypair (public key = id), register
2882
+ * metadata, mint the initial group key, and connect. Returns the open Space.
2883
+ */
2884
+ createSpace(opts?: {
2885
+ historyPolicy?: HistoryPolicy;
2886
+ }): Promise<Space>;
2887
+ /** Join an existing space by id (its public key). Resolves once a key arrives. */
2888
+ joinSpace(spaceId: string, opts?: {
2889
+ timeoutMs?: number;
2890
+ }): Promise<Space>;
2891
+ /** A handle to a space without auto-connecting (uses cached keys on demand). */
2892
+ get(spaceId: string, historyPolicy?: HistoryPolicy): Promise<Space>;
2893
+ /** List this app's channels (name → space-id) for the key's environment. */
2894
+ listChannels(): Promise<Array<{
2895
+ name: string;
2896
+ spaceId: string;
2897
+ }>>;
2898
+ /** Resolve a channel name to its space id, or null if it doesn't exist. */
2899
+ resolveChannel(name: string): Promise<string | null>;
2900
+ /**
2901
+ * Create a new channel: mint a fan-out space (the caller becomes its first
2902
+ * key-holder) and register `name → spaceId` in the app directory. Throws
2903
+ * {@link ChannelExistsError} if the name is already taken.
2904
+ */
2905
+ createChannel(name: string, opts?: {
2906
+ historyPolicy?: HistoryPolicy;
2907
+ }): Promise<Space>;
2908
+ /**
2909
+ * Join an existing channel by name. Throws {@link ChannelNotFoundError} if
2910
+ * no channel with that name is registered (use {@link createChannel} first).
2911
+ */
2912
+ joinChannel(name: string, opts?: {
2913
+ timeoutMs?: number;
2914
+ }): Promise<Space>;
2915
+ private build;
2916
+ /** Ensure the member has an identity ECDH keypair; return its public key. */
2917
+ private ensureIdentity;
2918
+ private fetchPolicy;
2919
+ private fetchTicket;
2920
+ private myId;
2921
+ }
2922
+
2497
2923
  /** The hosted Muhkoo Accelerator — the default {@link ClientOptions.baseUrl}. */
2498
2924
  declare const DEFAULT_BASE_URL = "https://api.muhkoo.dev";
2499
2925
  interface ClientOptions {
@@ -2535,6 +2961,8 @@ declare class Client {
2535
2961
  readonly storage: StorageNamespace;
2536
2962
  /** Realtime messaging — `client.message.subscribe(...)`, etc. */
2537
2963
  readonly message: MessageNamespace;
2964
+ /** Fan-out group spaces — `client.space.createSpace(...)`, etc. */
2965
+ readonly space: SpaceNamespace;
2538
2966
  private readonly session;
2539
2967
  private readonly http;
2540
2968
  constructor(options?: ClientOptions);
@@ -2544,5 +2972,45 @@ declare class Client {
2544
2972
  get isAuthenticated(): boolean;
2545
2973
  }
2546
2974
 
2547
- export { AES_GCM_IV_BYTES, AES_GCM_KEY_BYTES, AES_GCM_TAG_BYTES, AuthClient, AuthNamespace, AuthPublicInput, Authenticator, BroadcastChannel, BroadcastChannelEvents, ChunkCipher, Client, DEFAULT_BASE_URL, DoubleRatchet, DoubleRatchetManager, EncryptedSession, EventCore, EventCoreEvents, Field, Field as FieldElement, FileStorage, HashKnowledge, HttpClient, HttpError, KeyStore, LocalStorageSessionStore, MemorySessionStore, Message, Message as MessageDefault, MessageNamespace, PBKDF2_DEFAULT_ITERATIONS, PREIMAGE_POK_VERIFICATION_KEY, Packet, PersonalSpaceClient, Poseidon, PreimagePoK, ReedSolomonCodec, Room, SerializeMessage, SessionState, ShardClient, SharedSpaceClient, StorageNamespace, WSTransport, ZkAuth, buildCommitment, compilePrograms, decodeFromHex, decorators, decryptAesGcm, defaultCircuitUrls, defaultSessionStore, deriveAesKeyFromPassphrase, deriveBitsHkdf, deriveIdentity, encodeToHex, encryptAesGcm, exportPublicKeyBase64, exportPublicKeyHex, generateAuthProof, getSubtle, initBn128Wasm, initRsWasm, initializeCircuits, manifestToStat, poseidonHash, quickVerify, randomBytes, shardHash, signMessage, toField, unwrapWithPassphrase, verify, verifyGroth16, verifyHashKnowledge, verifyPreimagePoK, wrapWithPassphrase };
2548
- export type { ACL, ACLEntry, AuthClientOptions, AuthKeyPair, AuthSuccess, AuthToken, AuthType, AuthUser, Bn128WasmInstance, BroadcastChannelEvent, BroadcastChannelOptions, ChannelLike, ChunkKeyMaterial, ChunkManifest, CipherFrame, CipherMessage, CipherMessageHeader, CircuitBufferConfig, CircuitUrls, ClientOptions, DehydratedKeypair, DehydratedKeys, ECDHCurve, EncodeResult, FileManifest, FileStat, FileStorageOptions, Groth16Proof, HttpClientOptions, IncomingFrame, KeyExchangeFrame, KeyPair, Keypair, LoginOptions, MessageBody, MessageEvent, MessageHeaders, MessageNamespaceDeps, MessageOptions, MessageSubscription, MuhkooMessageEvent, OAuthProvider, PacketOptions, Participant, Permission, PermissionCheckRequest, PermissionCheckResult, PersonalSpaceClientOptions, PublicKeyJWK, RatchetState, ReceiveResult, RegisterParams, Role, RoomDeps, RoomFileMetadata, Session, SessionOptions, SessionStore, SetOptions, ShardClientOptions, SharedSpaceClientOptions, SnarkProof, StorageChangeEvent, StorageNamespaceDeps, StoredSession, Subscription, UserIdentity, VerificationKey, WSMessage, WSMessageType, WSTransportOptions, WrappedPayload, WriteFileInput, WriteToShardsInput, ZkAuthDeps, ZkAuthenticateRequest, ZkChallengeResponse, ZkCompiled, ZkIdentity, ZkRegisterRequest, ZkRegisterResponse };
2975
+ /**
2976
+ * KeyringClient — HTTP implementation of {@link KeyringTransport} against the
2977
+ * accelerator's per-space keyring endpoints. All blobs are opaque to the
2978
+ * server; this client just moves them.
2979
+ *
2980
+ * POST /api/spaces/:id/keyring/join-request { ...JoinRequest }
2981
+ * GET /api/spaces/:id/keyring/blobs/:member -> WrappedKey[]
2982
+ * POST /api/spaces/:id/keyring/wrap { targetMemberId, fromMemberId, wrapped }
2983
+ * GET /api/spaces/:id/keyring/pending -> JoinRequest[]
2984
+ * GET /api/spaces/:id/keyring/roster -> { memberId, identityEcdhPub }[]
2985
+ * POST /api/spaces/:id/keyring/rotate { nextEpoch } -> { epoch }
2986
+ * GET /api/spaces/:id/metadata -> SpaceMetadata
2987
+ */
2988
+
2989
+ interface KeyringClientDeps {
2990
+ spaceId: string;
2991
+ httpBaseUrl: string;
2992
+ /** Header-injecting fetch from the client's HttpClient. */
2993
+ fetch: typeof fetch;
2994
+ }
2995
+ declare class KeyringClient implements KeyringTransport {
2996
+ private readonly deps;
2997
+ constructor(deps: KeyringClientDeps);
2998
+ private url;
2999
+ private post;
3000
+ private get;
3001
+ postJoinRequest(req: JoinRequest): Promise<void>;
3002
+ fetchBlobs(memberId: string): Promise<WrappedKey[]>;
3003
+ postWrappedKey(targetMemberId: string, wrapped: WrappedKey, fromMemberId: string): Promise<void>;
3004
+ fetchPending(): Promise<JoinRequest[]>;
3005
+ fetchRoster(): Promise<Array<{
3006
+ memberId: string;
3007
+ identityEcdhPub: string;
3008
+ }>>;
3009
+ rotate(nextEpoch: number): Promise<{
3010
+ epoch: number;
3011
+ }>;
3012
+ fetchMetadata(): Promise<SpaceMetadata | null>;
3013
+ }
3014
+
3015
+ 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 };
3016
+ 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 };