@abraca/dabra 1.0.1 → 1.0.3

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/index.d.ts CHANGED
@@ -391,6 +391,23 @@ declare class AbracadabraClient {
391
391
  revokeInvite(code: string): Promise<void>;
392
392
  /** Redeem an invite code for the currently authenticated user. */
393
393
  redeemInvite(code: string): Promise<void>;
394
+ /** List spaces visible to the caller. No auth required for public spaces. */
395
+ listSpaces(): Promise<SpaceMeta[]>;
396
+ /** Get a single space by ID. */
397
+ getSpace(spaceId: string): Promise<SpaceMeta>;
398
+ /** Get the hub space, or null if none is configured. */
399
+ getHubSpace(): Promise<SpaceMeta | null>;
400
+ /** Create a new space (auth required). */
401
+ createSpace(opts: {
402
+ name: string;
403
+ description?: string;
404
+ visibility?: SpaceMeta["visibility"];
405
+ id?: string;
406
+ }): Promise<SpaceMeta>;
407
+ /** Update an existing space (Owner or admin required). */
408
+ updateSpace(spaceId: string, opts: Partial<Pick<SpaceMeta, "name" | "description" | "visibility" | "is_hub">>): Promise<SpaceMeta>;
409
+ /** Delete a space and its root document (Owner or admin required). */
410
+ deleteSpace(spaceId: string): Promise<void>;
394
411
  /** Health check — no auth required. */
395
412
  health(): Promise<HealthStatus>;
396
413
  /**
@@ -398,6 +415,12 @@ declare class AbracadabraClient {
398
415
  * No auth required.
399
416
  */
400
417
  serverInfo(): Promise<ServerInfo>;
418
+ /**
419
+ * Fetch ICE server configuration for WebRTC peer connections.
420
+ * Falls back to default Google STUN server if the endpoint is unavailable.
421
+ * No auth required.
422
+ */
423
+ getIceServers(): Promise<RTCIceServer[]>;
401
424
  private request;
402
425
  private toError;
403
426
  private loadPersistedToken;
@@ -964,6 +987,17 @@ interface SearchResult {
964
987
  /** Number of matching trigrams — higher is better. */
965
988
  score: number;
966
989
  }
990
+ interface SpaceMeta {
991
+ id: string;
992
+ doc_id: string;
993
+ name: string;
994
+ description: string | null;
995
+ visibility: "public" | "private" | "invite";
996
+ is_hub: boolean;
997
+ owner_id: string | null;
998
+ created_at: number;
999
+ updated_at: number;
1000
+ }
967
1001
  interface InviteRow {
968
1002
  code: string;
969
1003
  createdBy: string | null;
@@ -1563,4 +1597,358 @@ declare class BackgroundSyncManager extends EventEmitter {
1563
1597
  private _walkXml;
1564
1598
  }
1565
1599
  //#endregion
1566
- export { AbracadabraBaseProvider, AbracadabraBaseProviderConfiguration, AbracadabraClient, AbracadabraClientConfig, AbracadabraOutgoingMessageArguments, AbracadabraProvider, AbracadabraProviderConfiguration, AbracadabraWS, AbracadabraWSConfiguration, AbracadabraWebSocketConn, AuthMessageType, AuthorizedScope, AwarenessError, BackgroundSyncManager, type BackgroundSyncManagerOptions, BackgroundSyncPersistence, CloseEvent, CompleteAbracadabraBaseProviderConfiguration, CompleteAbracadabraWSConfiguration, CompleteHocuspocusProviderConfiguration, CompleteHocuspocusProviderWebsocketConfiguration, ConnectionTimeout, Constructable, ConstructableOutgoingMessage, CryptoIdentity, CryptoIdentityKeystore, type DocEncryptionInfo, DocKeyManager, type DocSyncState, DocumentCache, type DocumentCacheOptions, DocumentMeta, E2EAbracadabraProvider, E2EOfflineStore, EffectiveRole, EncryptedYMap, EncryptedYText, FileBlobStore, Forbidden, HealthStatus, HocusPocusWebSocket, HocuspocusProvider, HocuspocusProviderConfiguration, HocuspocusProviderWebsocket, HocuspocusProviderWebsocketConfiguration, HocuspocusWebSocket, InviteRow, MessageTooBig, MessageType, OfflineStore, OutgoingMessageArguments, OutgoingMessageInterface, PendingSubdoc, PermissionEntry, PublicKeyInfo, ResetConnection, SearchIndex, SearchResult, ServerInfo, StatesArray, SubdocMessage, SubdocRegisteredEvent, Unauthorized, UploadInfo, UploadMeta, UploadQueueEntry, UploadQueueStatus, UserProfile, WebSocketStatus, WsReadyStates, attachUpdatedAtObserver, awarenessStatesToArray, decryptField, encryptField, makeEncryptedYMap, makeEncryptedYText, onAuthenticatedParameters, onAuthenticationFailedParameters, onAwarenessChangeParameters, onAwarenessUpdateParameters, onCloseParameters, onDisconnectParameters, onMessageParameters, onOpenParameters, onOutgoingMessageParameters, onStatelessParameters, onStatusParameters, onSubdocLoadedParameters, onSubdocRegisteredParameters, onSyncedParameters, onUnsyncedChangesParameters, readAuthMessage, writeAuthenticated, writeAuthentication, writePermissionDenied, writeTokenSyncRequest };
1600
+ //#region packages/provider/src/webrtc/DataChannelRouter.d.ts
1601
+ declare class DataChannelRouter extends EventEmitter {
1602
+ private connection;
1603
+ private channels;
1604
+ constructor(connection: RTCPeerConnection);
1605
+ /** Create a named data channel (initiator side). */
1606
+ createChannel(name: string, options?: RTCDataChannelInit): RTCDataChannel;
1607
+ /** Create the standard set of channels for Abracadabra WebRTC. */
1608
+ createDefaultChannels(opts: {
1609
+ enableDocSync: boolean;
1610
+ enableAwareness: boolean;
1611
+ enableFileTransfer: boolean;
1612
+ }): void;
1613
+ getChannel(name: string): RTCDataChannel | null;
1614
+ isOpen(name: string): boolean;
1615
+ private registerChannel;
1616
+ close(): void;
1617
+ destroy(): void;
1618
+ }
1619
+ //#endregion
1620
+ //#region packages/provider/src/webrtc/types.d.ts
1621
+ type SignalingIncoming = {
1622
+ type: "join";
1623
+ } | {
1624
+ type: "leave";
1625
+ } | {
1626
+ type: "offer";
1627
+ to: string;
1628
+ sdp: string;
1629
+ } | {
1630
+ type: "answer";
1631
+ to: string;
1632
+ sdp: string;
1633
+ } | {
1634
+ type: "ice";
1635
+ to: string;
1636
+ candidate: string;
1637
+ } | {
1638
+ type: "mute";
1639
+ muted: boolean;
1640
+ } | {
1641
+ type: "media-state";
1642
+ video: boolean;
1643
+ screen: boolean;
1644
+ } | {
1645
+ type: "profile";
1646
+ name: string;
1647
+ color: string;
1648
+ } | {
1649
+ type: "pong";
1650
+ };
1651
+ type SignalingOutgoing = {
1652
+ type: "welcome";
1653
+ peer_id: string;
1654
+ peers: PeerInfo[];
1655
+ } | {
1656
+ type: "joined";
1657
+ peer_id: string;
1658
+ user_id: string;
1659
+ muted: boolean;
1660
+ video: boolean;
1661
+ screen: boolean;
1662
+ name: string | null;
1663
+ color: string | null;
1664
+ } | {
1665
+ type: "left";
1666
+ peer_id: string;
1667
+ } | {
1668
+ type: "offer";
1669
+ from: string;
1670
+ sdp: string;
1671
+ } | {
1672
+ type: "answer";
1673
+ from: string;
1674
+ sdp: string;
1675
+ } | {
1676
+ type: "ice";
1677
+ from: string;
1678
+ candidate: string;
1679
+ } | {
1680
+ type: "mute";
1681
+ peer_id: string;
1682
+ muted: boolean;
1683
+ } | {
1684
+ type: "media-state";
1685
+ peer_id: string;
1686
+ video: boolean;
1687
+ screen: boolean;
1688
+ } | {
1689
+ type: "profile";
1690
+ peer_id: string;
1691
+ name: string;
1692
+ color: string;
1693
+ } | {
1694
+ type: "ping";
1695
+ } | {
1696
+ type: "error";
1697
+ code: string;
1698
+ message: string;
1699
+ };
1700
+ interface PeerInfo {
1701
+ peer_id: string;
1702
+ user_id: string;
1703
+ muted: boolean;
1704
+ video: boolean;
1705
+ screen: boolean;
1706
+ name: string | null;
1707
+ color: string | null;
1708
+ }
1709
+ interface PeerState extends PeerInfo {
1710
+ connectionState: RTCPeerConnectionState | "new";
1711
+ }
1712
+ interface FileTransferMeta {
1713
+ transferId: string;
1714
+ filename: string;
1715
+ mimeType: string;
1716
+ totalSize: number;
1717
+ chunkSize: number;
1718
+ totalChunks: number;
1719
+ }
1720
+ type FileTransferStatus = "pending" | "sending" | "receiving" | "complete" | "cancelled" | "error";
1721
+ declare const CHANNEL_NAMES: {
1722
+ readonly YJS_SYNC: "yjs-sync";
1723
+ readonly AWARENESS: "awareness";
1724
+ readonly FILE_TRANSFER: "file-transfer";
1725
+ readonly CUSTOM: "custom";
1726
+ };
1727
+ interface AbracadabraWebRTCConfiguration {
1728
+ /** Document ID for the signaling room. */
1729
+ docId: string;
1730
+ /** Server base URL (http/https). Signaling URL derived automatically. */
1731
+ url: string;
1732
+ /** JWT token or async token factory. */
1733
+ token: string | (() => string) | (() => Promise<string>);
1734
+ /** Optional Y.Doc to sync over data channels (hybrid mode). */
1735
+ document?: InstanceType<typeof Y.Doc> | null;
1736
+ /** Optional Awareness instance for presence sync over data channels. */
1737
+ awareness?: InstanceType<typeof Awareness> | null;
1738
+ /** ICE server configuration. Defaults to Google STUN. */
1739
+ iceServers?: RTCIceServer[];
1740
+ /** Display name for this peer. */
1741
+ displayName?: string;
1742
+ /** Color identifier for this peer. */
1743
+ color?: string;
1744
+ /** Enable Y.js document sync over data channels. Default: true when document is provided. */
1745
+ enableDocSync?: boolean;
1746
+ /** Enable awareness sync over data channels. Default: true when awareness is provided. */
1747
+ enableAwarenessSync?: boolean;
1748
+ /** Enable file transfer channel. Default: false. */
1749
+ enableFileTransfer?: boolean;
1750
+ /** Max file chunk size in bytes. Default: 16384 (16KB). */
1751
+ fileChunkSize?: number;
1752
+ /** Auto-connect on construction. Default: true. */
1753
+ autoConnect?: boolean;
1754
+ /** WebSocket polyfill for signaling (e.g. for Node.js). */
1755
+ WebSocketPolyfill?: any;
1756
+ }
1757
+ declare const DEFAULT_ICE_SERVERS: RTCIceServer[];
1758
+ declare const DEFAULT_FILE_CHUNK_SIZE = 16384;
1759
+ //#endregion
1760
+ //#region packages/provider/src/webrtc/FileTransferChannel.d.ts
1761
+ /**
1762
+ * Handle for tracking a file transfer in progress.
1763
+ */
1764
+ declare class FileTransferHandle extends EventEmitter {
1765
+ readonly transferId: string;
1766
+ progress: number;
1767
+ status: FileTransferStatus;
1768
+ private abortController;
1769
+ constructor(transferId: string);
1770
+ cancel(): void;
1771
+ get signal(): AbortSignal;
1772
+ /** @internal */
1773
+ _setProgress(p: number): void;
1774
+ /** @internal */
1775
+ _setStatus(s: FileTransferStatus): void;
1776
+ }
1777
+ /**
1778
+ * Chunked binary file transfer over a dedicated WebRTC data channel.
1779
+ */
1780
+ declare class FileTransferChannel extends EventEmitter {
1781
+ private readonly router;
1782
+ private receives;
1783
+ private chunkSize;
1784
+ private channelMessageHandler;
1785
+ constructor(router: DataChannelRouter, chunkSize?: number);
1786
+ /** Send a file to a peer. Returns a handle for tracking progress. */
1787
+ send(file: File | Blob, filename: string): Promise<FileTransferHandle>;
1788
+ private handleMessage;
1789
+ private handleStart;
1790
+ private handleChunk;
1791
+ private handleComplete;
1792
+ private handleCancel;
1793
+ destroy(): void;
1794
+ }
1795
+ //#endregion
1796
+ //#region packages/provider/src/webrtc/AbracadabraWebRTC.d.ts
1797
+ /**
1798
+ * Optional WebRTC provider for peer-to-peer Y.js sync, awareness, and file transfer.
1799
+ *
1800
+ * Uses the server's signaling endpoint (`/ws/:doc_id/signaling`) for connection
1801
+ * negotiation, then establishes direct data channels between peers. Designed to
1802
+ * work alongside `AbracadabraProvider` — the server remains the persistence layer,
1803
+ * while WebRTC provides low-latency P2P sync.
1804
+ *
1805
+ * Falls back to a no-op when `RTCPeerConnection` is unavailable (e.g. Node.js).
1806
+ */
1807
+ declare class AbracadabraWebRTC extends EventEmitter {
1808
+ private signaling;
1809
+ private peerConnections;
1810
+ private yjsChannels;
1811
+ private fileChannels;
1812
+ private readonly config;
1813
+ readonly peers: Map<string, PeerState>;
1814
+ localPeerId: string | null;
1815
+ isConnected: boolean;
1816
+ constructor(configuration: AbracadabraWebRTCConfiguration);
1817
+ /**
1818
+ * Create an AbracadabraWebRTC instance from an existing provider,
1819
+ * reusing its document, awareness, URL, and token.
1820
+ */
1821
+ static fromProvider(provider: AbracadabraProvider, options?: Partial<AbracadabraWebRTCConfiguration>): AbracadabraWebRTC;
1822
+ connect(): Promise<void>;
1823
+ disconnect(): void;
1824
+ destroy(): void;
1825
+ setMuted(muted: boolean): void;
1826
+ setMediaState(video: boolean, screen: boolean): void;
1827
+ setProfile(name: string, color: string): void;
1828
+ /**
1829
+ * Send a file to a specific peer. Returns a handle for tracking progress.
1830
+ */
1831
+ sendFile(peerId: string, file: File | Blob, filename: string): Promise<FileTransferHandle | null>;
1832
+ /**
1833
+ * Send a file to all connected peers. Returns an array of handles.
1834
+ */
1835
+ broadcastFile(file: File | Blob, filename: string): Promise<FileTransferHandle[]>;
1836
+ /**
1837
+ * Send a custom string message to a specific peer via a data channel.
1838
+ */
1839
+ sendCustomMessage(peerId: string, payload: string): void;
1840
+ /**
1841
+ * Send a custom string message to all connected peers.
1842
+ */
1843
+ broadcastCustomMessage(payload: string): void;
1844
+ private addPeer;
1845
+ private removePeer;
1846
+ private removeAllPeers;
1847
+ private createPeerConnection;
1848
+ private attachDataHandlers;
1849
+ private initiateConnection;
1850
+ private handleOffer;
1851
+ private buildSignalingUrl;
1852
+ }
1853
+ //#endregion
1854
+ //#region packages/provider/src/webrtc/SignalingSocket.d.ts
1855
+ interface SignalingSocketConfiguration {
1856
+ /** WebSocket URL for the signaling endpoint. */
1857
+ url: string;
1858
+ /** JWT token or async token factory for auth. */
1859
+ token: string | (() => string) | (() => Promise<string>);
1860
+ /** Auto-connect on construction. Default: true. */
1861
+ autoConnect?: boolean;
1862
+ /** WebSocket polyfill (e.g. for Node.js). */
1863
+ WebSocketPolyfill?: any;
1864
+ /** Retry delay in ms. Default: 1000. */
1865
+ delay?: number;
1866
+ /** Retry factor. Default: 2. */
1867
+ factor?: number;
1868
+ /** Min retry delay. Default: 1000. */
1869
+ minDelay?: number;
1870
+ /** Max retry delay. Default: 30000. */
1871
+ maxDelay?: number;
1872
+ /** Randomize delay. Default: true. */
1873
+ jitter?: boolean;
1874
+ /** Max retry attempts (0 = unlimited). Default: 0. */
1875
+ maxAttempts?: number;
1876
+ }
1877
+ declare class SignalingSocket extends EventEmitter {
1878
+ private ws;
1879
+ private wsHandlers;
1880
+ private shouldConnect;
1881
+ private cancelRetry?;
1882
+ private connectionAttempt;
1883
+ private readonly config;
1884
+ localPeerId: string | null;
1885
+ isConnected: boolean;
1886
+ constructor(configuration: SignalingSocketConfiguration);
1887
+ private getToken;
1888
+ connect(): Promise<void>;
1889
+ private createConnection;
1890
+ private handleMessage;
1891
+ private sendRaw;
1892
+ sendOffer(to: string, sdp: string): void;
1893
+ sendAnswer(to: string, sdp: string): void;
1894
+ sendIce(to: string, candidate: string): void;
1895
+ sendMute(muted: boolean): void;
1896
+ sendMediaState(video: boolean, screen: boolean): void;
1897
+ sendProfile(name: string, color: string): void;
1898
+ sendLeave(): void;
1899
+ disconnect(): void;
1900
+ destroy(): void;
1901
+ private cleanup;
1902
+ }
1903
+ //#endregion
1904
+ //#region packages/provider/src/webrtc/PeerConnection.d.ts
1905
+ declare class PeerConnection extends EventEmitter {
1906
+ readonly connection: RTCPeerConnection;
1907
+ readonly router: DataChannelRouter;
1908
+ readonly peerId: string;
1909
+ private pendingCandidates;
1910
+ private hasRemoteDescription;
1911
+ constructor(peerId: string, iceServers: RTCIceServer[]);
1912
+ get connectionState(): RTCPeerConnectionState;
1913
+ get iceConnectionState(): RTCIceConnectionState;
1914
+ /** Create an SDP offer (initiator side). */
1915
+ createOffer(iceRestart?: boolean): Promise<string>;
1916
+ /** Set a remote offer and create an answer (receiver side). Returns the SDP answer. */
1917
+ setRemoteOffer(sdp: string): Promise<string>;
1918
+ /** Set the remote answer (initiator side). */
1919
+ setRemoteAnswer(sdp: string): Promise<void>;
1920
+ /** Add a remote ICE candidate. Queues if remote description not yet set. */
1921
+ addIceCandidate(candidateJson: string): Promise<void>;
1922
+ private flushPendingCandidates;
1923
+ close(): void;
1924
+ destroy(): void;
1925
+ }
1926
+ //#endregion
1927
+ //#region packages/provider/src/webrtc/YjsDataChannel.d.ts
1928
+ /**
1929
+ * Handles Y.js document sync and awareness over WebRTC data channels.
1930
+ *
1931
+ * Uses the same y-protocols/sync encoding as the WebSocket provider but
1932
+ * transported over RTCDataChannel instead. A unique origin is used to
1933
+ * prevent echo loops with the server-based provider.
1934
+ */
1935
+ declare class YjsDataChannel {
1936
+ private readonly document;
1937
+ private readonly awareness;
1938
+ private readonly router;
1939
+ private docUpdateHandler;
1940
+ private awarenessUpdateHandler;
1941
+ private channelOpenHandler;
1942
+ private channelMessageHandler;
1943
+ constructor(document: Y.Doc, awareness: Awareness | null, router: DataChannelRouter);
1944
+ /** Start listening for Y.js updates and data channel messages. */
1945
+ attach(): void;
1946
+ /** Stop listening and clean up handlers. */
1947
+ detach(): void;
1948
+ destroy(): void;
1949
+ private sendSyncStep1;
1950
+ private handleSyncMessage;
1951
+ private handleAwarenessMessage;
1952
+ }
1953
+ //#endregion
1954
+ export { AbracadabraBaseProvider, AbracadabraBaseProviderConfiguration, AbracadabraClient, AbracadabraClientConfig, AbracadabraOutgoingMessageArguments, AbracadabraProvider, AbracadabraProviderConfiguration, AbracadabraWS, AbracadabraWSConfiguration, AbracadabraWebRTC, type AbracadabraWebRTCConfiguration, AbracadabraWebSocketConn, AuthMessageType, AuthorizedScope, AwarenessError, BackgroundSyncManager, type BackgroundSyncManagerOptions, BackgroundSyncPersistence, CHANNEL_NAMES, CloseEvent, CompleteAbracadabraBaseProviderConfiguration, CompleteAbracadabraWSConfiguration, CompleteHocuspocusProviderConfiguration, CompleteHocuspocusProviderWebsocketConfiguration, ConnectionTimeout, Constructable, ConstructableOutgoingMessage, CryptoIdentity, CryptoIdentityKeystore, DEFAULT_FILE_CHUNK_SIZE, DEFAULT_ICE_SERVERS, DataChannelRouter, type DocEncryptionInfo, DocKeyManager, type DocSyncState, DocumentCache, type DocumentCacheOptions, DocumentMeta, E2EAbracadabraProvider, E2EOfflineStore, EffectiveRole, EncryptedYMap, EncryptedYText, FileBlobStore, FileTransferChannel, FileTransferHandle, type FileTransferMeta, type FileTransferStatus, Forbidden, HealthStatus, HocusPocusWebSocket, HocuspocusProvider, HocuspocusProviderConfiguration, HocuspocusProviderWebsocket, HocuspocusProviderWebsocketConfiguration, HocuspocusWebSocket, InviteRow, MessageTooBig, MessageType, OfflineStore, OutgoingMessageArguments, OutgoingMessageInterface, PeerConnection, type PeerInfo, type PeerState, PendingSubdoc, PermissionEntry, PublicKeyInfo, ResetConnection, SearchIndex, SearchResult, ServerInfo, type SignalingIncoming, type SignalingOutgoing, SignalingSocket, SpaceMeta, StatesArray, SubdocMessage, SubdocRegisteredEvent, Unauthorized, UploadInfo, UploadMeta, UploadQueueEntry, UploadQueueStatus, UserProfile, WebSocketStatus, WsReadyStates, YjsDataChannel, attachUpdatedAtObserver, awarenessStatesToArray, decryptField, encryptField, makeEncryptedYMap, makeEncryptedYText, onAuthenticatedParameters, onAuthenticationFailedParameters, onAwarenessChangeParameters, onAwarenessUpdateParameters, onCloseParameters, onDisconnectParameters, onMessageParameters, onOpenParameters, onOutgoingMessageParameters, onStatelessParameters, onStatusParameters, onSubdocLoadedParameters, onSubdocRegisteredParameters, onSyncedParameters, onUnsyncedChangesParameters, readAuthMessage, writeAuthenticated, writeAuthentication, writePermissionDenied, writeTokenSyncRequest };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abraca/dabra",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "abracadabra provider",
5
5
  "keywords": [
6
6
  "abracadabra",
@@ -8,6 +8,7 @@ import type {
8
8
  HealthStatus,
9
9
  ServerInfo,
10
10
  InviteRow,
11
+ SpaceMeta,
11
12
  } from "./types.ts";
12
13
  import type { DocEncryptionInfo } from "./types.ts";
13
14
  import type { DocumentCache } from "./DocumentCache.ts";
@@ -472,6 +473,54 @@ export class AbracadabraClient {
472
473
  await this.request("POST", "/invites/redeem", { body: { code } });
473
474
  }
474
475
 
476
+ // ── Spaces ───────────────────────────────────────────────────────────────
477
+
478
+ /** List spaces visible to the caller. No auth required for public spaces. */
479
+ async listSpaces(): Promise<SpaceMeta[]> {
480
+ const res = await this.request<{ spaces: SpaceMeta[] }>("GET", "/spaces", { auth: false });
481
+ return res.spaces;
482
+ }
483
+
484
+ /** Get a single space by ID. */
485
+ async getSpace(spaceId: string): Promise<SpaceMeta> {
486
+ return this.request<SpaceMeta>("GET", `/spaces/${encodeURIComponent(spaceId)}`, { auth: false });
487
+ }
488
+
489
+ /** Get the hub space, or null if none is configured. */
490
+ async getHubSpace(): Promise<SpaceMeta | null> {
491
+ try {
492
+ return await this.request<SpaceMeta>("GET", "/spaces/hub", { auth: false });
493
+ } catch (e: unknown) {
494
+ if (typeof e === "object" && e !== null && "status" in e && (e as { status: number }).status === 404) {
495
+ return null;
496
+ }
497
+ throw e;
498
+ }
499
+ }
500
+
501
+ /** Create a new space (auth required). */
502
+ async createSpace(opts: {
503
+ name: string;
504
+ description?: string;
505
+ visibility?: SpaceMeta["visibility"];
506
+ id?: string;
507
+ }): Promise<SpaceMeta> {
508
+ return this.request<SpaceMeta>("POST", "/spaces", { body: opts });
509
+ }
510
+
511
+ /** Update an existing space (Owner or admin required). */
512
+ async updateSpace(
513
+ spaceId: string,
514
+ opts: Partial<Pick<SpaceMeta, "name" | "description" | "visibility" | "is_hub">>,
515
+ ): Promise<SpaceMeta> {
516
+ return this.request<SpaceMeta>("PATCH", `/spaces/${encodeURIComponent(spaceId)}`, { body: opts });
517
+ }
518
+
519
+ /** Delete a space and its root document (Owner or admin required). */
520
+ async deleteSpace(spaceId: string): Promise<void> {
521
+ await this.request("DELETE", `/spaces/${encodeURIComponent(spaceId)}`);
522
+ }
523
+
475
524
  // ── System ───────────────────────────────────────────────────────────────
476
525
 
477
526
  /** Health check — no auth required. */
@@ -487,6 +536,24 @@ export class AbracadabraClient {
487
536
  return this.request<ServerInfo>("GET", "/info", { auth: false });
488
537
  }
489
538
 
539
+ /**
540
+ * Fetch ICE server configuration for WebRTC peer connections.
541
+ * Falls back to default Google STUN server if the endpoint is unavailable.
542
+ * No auth required.
543
+ */
544
+ async getIceServers(): Promise<RTCIceServer[]> {
545
+ try {
546
+ const res = await this.request<{ iceServers: RTCIceServer[] }>(
547
+ "GET",
548
+ "/ice-servers",
549
+ { auth: false },
550
+ );
551
+ return res.iceServers;
552
+ } catch {
553
+ return [{ urls: "stun:stun.l.google.com:19302" }];
554
+ }
555
+ }
556
+
490
557
  // ── Internals ────────────────────────────────────────────────────────────
491
558
 
492
559
  private async request<T = void>(
@@ -511,7 +511,9 @@ export class AbracadabraWS extends EventEmitter {
511
511
  this.emit("status", { status: WebSocketStatus.Disconnected });
512
512
 
513
513
  // Detect server-side rate-limit close (code 4429).
514
- const isRateLimited = (event as any)?.code === 4429;
514
+ // `event` may be a CloseEvent (browser) with `.code`, or a raw number (ws/Node.js).
515
+ console.log('[DEBUG] onClose event:', typeof event, JSON.stringify(event), 'code:', (event as any)?.code);
516
+ const isRateLimited = (event as any)?.code === 4429 || event === 4429;
515
517
  this.emit("disconnect", { event });
516
518
  if (isRateLimited) {
517
519
  this.emit("rateLimited");
package/src/index.ts CHANGED
@@ -23,3 +23,4 @@ export { BackgroundSyncManager } from "./BackgroundSyncManager.ts";
23
23
  export type { BackgroundSyncManagerOptions } from "./BackgroundSyncManager.ts";
24
24
  export { BackgroundSyncPersistence } from "./BackgroundSyncPersistence.ts";
25
25
  export type { DocSyncState } from "./BackgroundSyncPersistence.ts";
26
+ export * from "./webrtc/index.ts";
package/src/types.ts CHANGED
@@ -223,6 +223,20 @@ export interface SearchResult {
223
223
  score: number;
224
224
  }
225
225
 
226
+ // ── Spaces ───────────────────────────────────────────────────────────────────
227
+
228
+ export interface SpaceMeta {
229
+ id: string;
230
+ doc_id: string;
231
+ name: string;
232
+ description: string | null;
233
+ visibility: "public" | "private" | "invite";
234
+ is_hub: boolean;
235
+ owner_id: string | null;
236
+ created_at: number;
237
+ updated_at: number;
238
+ }
239
+
226
240
  // ── Invites ──────────────────────────────────────────────────────────────────
227
241
 
228
242
  export interface InviteRow {