@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/dist/connect.d.ts CHANGED
@@ -2329,75 +2329,452 @@ 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
+ /** 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
- * 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()`.
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
- * `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(...)`.
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 RoomFileMetadata {
2622
+ interface SpaceFileMetadata {
2352
2623
  name: string;
2353
2624
  type: string;
2354
2625
  path?: string;
2355
2626
  lastModified?: number;
2356
2627
  }
2357
- interface RoomDeps {
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 (the signed-in username). */
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 Room {
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
- constructor(deps: RoomDeps);
2374
- /** Open the room websocket (resolves the ticket + identity lazily). */
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; returns the recipient count. */
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(): BroadcastChannel | null;
2390
- /** Encrypt + erasure-code + upload `file` to this room's shards. */
2391
- putFile(file: File | Blob | Uint8Array, metadata: RoomFileMetadata): Promise<{
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): Room;
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
- 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 };
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 };