openrtc 0.1.0

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.
Files changed (122) hide show
  1. package/README.md +106 -0
  2. package/dist/OpenRTC.d.ts +71 -0
  3. package/dist/OpenRTC.js +130 -0
  4. package/dist/api/MediaTransport.d.ts +7 -0
  5. package/dist/api/MediaTransport.js +262 -0
  6. package/dist/api/PlutoPeerConnection.d.ts +50 -0
  7. package/dist/api/PlutoPeerConnection.js +434 -0
  8. package/dist/api/PlutoWebSocket.d.ts +24 -0
  9. package/dist/api/PlutoWebSocket.js +113 -0
  10. package/dist/api/PlutoWebTransport.d.ts +29 -0
  11. package/dist/api/PlutoWebTransport.js +105 -0
  12. package/dist/auth/host.d.ts +73 -0
  13. package/dist/auth/host.js +513 -0
  14. package/dist/auth/index.d.ts +1 -0
  15. package/dist/auth/index.js +1 -0
  16. package/dist/auth/internal.d.ts +1 -0
  17. package/dist/auth/internal.js +1 -0
  18. package/dist/compat/index.d.ts +20 -0
  19. package/dist/compat/index.js +20 -0
  20. package/dist/core/Client.d.ts +352 -0
  21. package/dist/core/Client.js +3689 -0
  22. package/dist/core/Connection.d.ts +67 -0
  23. package/dist/core/Connection.js +404 -0
  24. package/dist/core/IEngineBridge.d.ts +18 -0
  25. package/dist/core/IEngineBridge.js +1 -0
  26. package/dist/core/ISignalingTransport.d.ts +13 -0
  27. package/dist/core/ISignalingTransport.js +1 -0
  28. package/dist/core/PeerLifecycleManager.d.ts +43 -0
  29. package/dist/core/PeerLifecycleManager.js +330 -0
  30. package/dist/core/PeerProjectionStore.d.ts +43 -0
  31. package/dist/core/PeerProjectionStore.js +327 -0
  32. package/dist/core/PlutoRTC.d.ts +13 -0
  33. package/dist/core/PlutoRTC.js +26 -0
  34. package/dist/core/Room.d.ts +47 -0
  35. package/dist/core/Room.js +376 -0
  36. package/dist/core/Signaling.d.ts +39 -0
  37. package/dist/core/Signaling.js +147 -0
  38. package/dist/core/bridges/BridgeCodecs.d.ts +19 -0
  39. package/dist/core/bridges/BridgeCodecs.js +317 -0
  40. package/dist/core/bridges/BrowserFirestoreRooms.d.ts +36 -0
  41. package/dist/core/bridges/BrowserFirestoreRooms.js +169 -0
  42. package/dist/core/bridges/BrowserFirestoreSignaling.d.ts +111 -0
  43. package/dist/core/bridges/BrowserFirestoreSignaling.js +1010 -0
  44. package/dist/core/bridges/NativeIpcBridge.d.ts +156 -0
  45. package/dist/core/bridges/NativeIpcBridge.js +1472 -0
  46. package/dist/core/bridges/RtdbPresence.d.ts +122 -0
  47. package/dist/core/bridges/RtdbPresence.js +201 -0
  48. package/dist/core/bridges/TauriBridge.d.ts +8 -0
  49. package/dist/core/bridges/TauriBridge.js +7 -0
  50. package/dist/core/bridges/WasmBridge.d.ts +233 -0
  51. package/dist/core/bridges/WasmBridge.js +1811 -0
  52. package/dist/core/config.d.ts +31 -0
  53. package/dist/core/config.js +138 -0
  54. package/dist/core/transports/ITransport.d.ts +12 -0
  55. package/dist/core/transports/ITransport.js +1 -0
  56. package/dist/core/transports/LocalDaemonBridge.d.ts +41 -0
  57. package/dist/core/transports/LocalDaemonBridge.js +227 -0
  58. package/dist/core/transports/TauriSignaling.d.ts +28 -0
  59. package/dist/core/transports/TauriSignaling.js +373 -0
  60. package/dist/core/transports/TransportFactory.d.ts +9 -0
  61. package/dist/core/transports/TransportFactory.js +59 -0
  62. package/dist/core/transports/WasmSignaling.d.ts +78 -0
  63. package/dist/core/transports/WasmSignaling.js +522 -0
  64. package/dist/core/transports/WebRTCTransport.d.ts +23 -0
  65. package/dist/core/transports/WebRTCTransport.js +132 -0
  66. package/dist/core/trust.d.ts +23 -0
  67. package/dist/core/trust.js +99 -0
  68. package/dist/core/types.d.ts +559 -0
  69. package/dist/core/types.js +19 -0
  70. package/dist/index.d.ts +3 -0
  71. package/dist/index.js +3 -0
  72. package/dist/moq/MoQMediaTransport.d.ts +6 -0
  73. package/dist/moq/MoQMediaTransport.js +106 -0
  74. package/dist/moq/MoQPublisher.d.ts +22 -0
  75. package/dist/moq/MoQPublisher.js +179 -0
  76. package/dist/moq/MoQSession.d.ts +35 -0
  77. package/dist/moq/MoQSession.js +166 -0
  78. package/dist/moq/MoQSubscriber.d.ts +19 -0
  79. package/dist/moq/MoQSubscriber.js +235 -0
  80. package/dist/moq/common.d.ts +47 -0
  81. package/dist/moq/common.js +304 -0
  82. package/dist/moq/messages.d.ts +105 -0
  83. package/dist/moq/messages.js +367 -0
  84. package/dist/presets/plutoHosted.d.ts +13 -0
  85. package/dist/presets/plutoHosted.js +18 -0
  86. package/dist/runtime/DelegatingRuntimeAdapter.d.ts +68 -0
  87. package/dist/runtime/DelegatingRuntimeAdapter.js +250 -0
  88. package/dist/runtime/IpcRuntimeAdapter.d.ts +9 -0
  89. package/dist/runtime/IpcRuntimeAdapter.js +13 -0
  90. package/dist/runtime/RuntimeClient.d.ts +257 -0
  91. package/dist/runtime/RuntimeClient.js +1484 -0
  92. package/dist/runtime/TauriRuntimeAdapter.d.ts +1 -0
  93. package/dist/runtime/TauriRuntimeAdapter.js +1 -0
  94. package/dist/runtime/WasmRuntimeAdapter.d.ts +6 -0
  95. package/dist/runtime/WasmRuntimeAdapter.js +11 -0
  96. package/dist/runtime/factories.d.ts +24 -0
  97. package/dist/runtime/factories.js +66 -0
  98. package/dist/runtime/index.d.ts +6 -0
  99. package/dist/runtime/index.js +5 -0
  100. package/dist/runtime/ipc.d.ts +8 -0
  101. package/dist/runtime/ipc.js +65 -0
  102. package/dist/runtime/ipcContract.d.ts +46 -0
  103. package/dist/runtime/ipcContract.js +1 -0
  104. package/dist/runtime/tauri.d.ts +16 -0
  105. package/dist/runtime/tauri.js +22 -0
  106. package/dist/runtime/types.d.ts +110 -0
  107. package/dist/runtime/types.js +1 -0
  108. package/dist/transports/TauriTransport.d.ts +17 -0
  109. package/dist/transports/TauriTransport.js +49 -0
  110. package/dist/transports/WebTransport.d.ts +23 -0
  111. package/dist/transports/WebTransport.js +71 -0
  112. package/dist/utils/debug.d.ts +4 -0
  113. package/dist/utils/debug.js +33 -0
  114. package/dist/utils/runtime.d.ts +1 -0
  115. package/dist/utils/runtime.js +10 -0
  116. package/package.json +97 -0
  117. package/wasm/README.md +64 -0
  118. package/wasm/openrtc.d.ts +284 -0
  119. package/wasm/openrtc.js +2132 -0
  120. package/wasm/openrtc_bg.wasm +0 -0
  121. package/wasm/openrtc_bg.wasm.d.ts +110 -0
  122. package/wasm/package.json +16 -0
@@ -0,0 +1,31 @@
1
+ import type { AuthMode, ClientOptions, DiscoveryMode, SignalingOptions } from './types';
2
+ /** Validate that a string looks like a pluto-rtc API key (pk_live_... or pk_test_...). */
3
+ export declare function isValidApiKeyFormat(value: string): boolean;
4
+ /**
5
+ * Derive a deterministic space namespace from apiKey + spaceKey.
6
+ * Uses a simple hash to create a Firestore-safe document ID.
7
+ */
8
+ export declare function deriveSpaceNamespace(apiKey: string, spaceKey: string): Promise<string>;
9
+ /**
10
+ * Resolve the Firestore namespace for device discovery.
11
+ *
12
+ * For personal mode with a spaceKey: `spaces/{sha256(apiKey+spaceKey)}`
13
+ * For managed mode: `apps/{appTag}/users/{userId}`
14
+ * For legacy/fallback: `apps/{appTag}`
15
+ */
16
+ export declare function resolveAppTag(options: Pick<ClientOptions, 'apiKey' | 'allowAnonymousHostedDefaults'> | Pick<SignalingOptions, 'apiKey' | 'allowAnonymousHostedDefaults'>): string;
17
+ /**
18
+ * Resolve the Firestore collection path for device discovery based on discovery mode.
19
+ *
20
+ * - personal + spaceKey: `spaces/{hash}/devices`
21
+ * - user-scoped: `apps/{appTag}/users/{userId}/devices`
22
+ * - fallback: `apps/{appTag}/users/{userId}/devices`
23
+ */
24
+ export declare function resolveDiscoveryPath(options: Pick<ClientOptions, 'apiKey' | 'discoveryMode' | 'spaceKey'>, context: {
25
+ userId?: string;
26
+ orgId?: string;
27
+ }): Promise<string>;
28
+ export declare function resolveDiscoveryMode(options: Pick<ClientOptions, 'discoveryMode'>): DiscoveryMode;
29
+ export declare function resolveAuthMode(options: Pick<ClientOptions, 'authMode' | 'apiKey' | 'spaceKey'> | Pick<SignalingOptions, 'authMode'>): AuthMode;
30
+ export declare function resolveProjectId(options: Pick<ClientOptions, 'projectId'> | Pick<SignalingOptions, 'projectId'>): string;
31
+ export declare function resolveStorageNamespace(options: Pick<ClientOptions, 'apiKey' | 'storagePrefix'> | Pick<SignalingOptions, 'apiKey'>): string;
@@ -0,0 +1,138 @@
1
+ import { PLUTO_HOSTED_PROJECT_ID } from '../presets/plutoHosted';
2
+ const warned = new Set();
3
+ function warnOnce(key, message) {
4
+ if (warned.has(key))
5
+ return;
6
+ warned.add(key);
7
+ console.warn(message);
8
+ }
9
+ function allowAnonymousHostedDefaults(options) {
10
+ return options.allowAnonymousHostedDefaults === true;
11
+ }
12
+ /** Validate that a string looks like a pluto-rtc API key (pk_live_... or pk_test_...). */
13
+ export function isValidApiKeyFormat(value) {
14
+ return /^pk_(live|test)_[0-9a-f]{40}$/.test(value);
15
+ }
16
+ /**
17
+ * Derive a deterministic space namespace from apiKey + spaceKey.
18
+ * Uses a simple hash to create a Firestore-safe document ID.
19
+ */
20
+ export async function deriveSpaceNamespace(apiKey, spaceKey) {
21
+ const input = `${apiKey}:${spaceKey}`;
22
+ const encoder = new TextEncoder();
23
+ const data = encoder.encode(input);
24
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data);
25
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
26
+ const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
27
+ return hashHex;
28
+ }
29
+ /**
30
+ * Resolve the Firestore namespace for device discovery.
31
+ *
32
+ * For personal mode with a spaceKey: `spaces/{sha256(apiKey+spaceKey)}`
33
+ * For managed mode: `apps/{appTag}/users/{userId}`
34
+ * For legacy/fallback: `apps/{appTag}`
35
+ */
36
+ export function resolveAppTag(options) {
37
+ const apiKey = typeof options.apiKey === 'string' ? options.apiKey.trim() : '';
38
+ if (apiKey) {
39
+ if (!isValidApiKeyFormat(apiKey)) {
40
+ warnOnce(`invalid-api-key:${apiKey.slice(0, 10)}`, "[pluto-rtc] The provided `apiKey` does not match the expected format (pk_live_... or pk_test_...). " +
41
+ "Create an app at https://api.openrtc.app/developer.");
42
+ }
43
+ return `app_${apiKey.slice(-16)}`;
44
+ }
45
+ if (!allowAnonymousHostedDefaults(options)) {
46
+ throw new Error('[openrtc] `apiKey` is required. ' +
47
+ 'If you intentionally need the old shared hosted fallback for a controlled demo or test, ' +
48
+ 'set `allowAnonymousHostedDefaults: true` explicitly.');
49
+ }
50
+ warnOnce('no-api-key', "[pluto-rtc] No `apiKey` provided. Operating on the legacy shared hosted namespace because " +
51
+ "`allowAnonymousHostedDefaults` is enabled explicitly. Create an app at https://api.openrtc.app/developer.");
52
+ return 'app_anonymous';
53
+ }
54
+ /**
55
+ * Resolve the Firestore collection path for device discovery based on discovery mode.
56
+ *
57
+ * - personal + spaceKey: `spaces/{hash}/devices`
58
+ * - user-scoped: `apps/{appTag}/users/{userId}/devices`
59
+ * - fallback: `apps/{appTag}/users/{userId}/devices`
60
+ */
61
+ export async function resolveDiscoveryPath(options, context) {
62
+ const rawMode = options.discoveryMode;
63
+ if (rawMode === 'org-scoped') {
64
+ throw new Error('[openrtc] discoveryMode "org-scoped" is not supported yet. Use "personal" or "user-scoped".');
65
+ }
66
+ if (rawMode && rawMode !== 'personal' && rawMode !== 'user-scoped') {
67
+ throw new Error(`[openrtc] Unsupported discoveryMode "${rawMode}". Use "personal" or "user-scoped".`);
68
+ }
69
+ const mode = rawMode === 'user-scoped' ? 'user-scoped' : 'personal';
70
+ const appTag = resolveAppTag(options);
71
+ if (mode === 'personal') {
72
+ const apiKey = typeof options.apiKey === 'string' ? options.apiKey.trim() : '';
73
+ const spaceKey = typeof options.spaceKey === 'string' ? options.spaceKey.trim() : '';
74
+ if (apiKey && spaceKey) {
75
+ const namespace = await deriveSpaceNamespace(apiKey, spaceKey);
76
+ return `spaces/${namespace}/devices`;
77
+ }
78
+ // No spaceKey: fall back to user-scoped path if userId available
79
+ if (context.userId) {
80
+ return `apps/${appTag}/users/${context.userId}/devices`;
81
+ }
82
+ warnOnce('personal-no-space-key', '[pluto-rtc] Personal mode without a spaceKey requires authentication. ' +
83
+ 'Provide a spaceKey for passwordless device discovery, or set discoveryMode to "user-scoped".');
84
+ return `apps/${appTag}/users/anonymous/devices`;
85
+ }
86
+ // user-scoped (default for managed)
87
+ return `apps/${appTag}/users/${context.userId ?? 'anonymous'}/devices`;
88
+ }
89
+ export function resolveDiscoveryMode(options) {
90
+ const rawMode = options.discoveryMode;
91
+ if (rawMode === 'org-scoped') {
92
+ throw new Error('[openrtc] discoveryMode "org-scoped" is not supported yet. Use "personal" or "user-scoped".');
93
+ }
94
+ if (rawMode && rawMode !== 'personal' && rawMode !== 'user-scoped') {
95
+ throw new Error(`[openrtc] Unsupported discoveryMode "${rawMode}". Use "personal" or "user-scoped".`);
96
+ }
97
+ return rawMode === 'user-scoped' ? 'user-scoped' : 'personal';
98
+ }
99
+ export function resolveAuthMode(options) {
100
+ if (options.authMode) {
101
+ return options.authMode;
102
+ }
103
+ // Personal Mode with spaceKey doesn't use auth at all — suppress the irrelevant warning.
104
+ const isPersonalMode = 'apiKey' in options &&
105
+ 'spaceKey' in options &&
106
+ typeof options.apiKey === 'string' &&
107
+ options.apiKey.trim().length > 0 &&
108
+ typeof options.spaceKey === 'string' &&
109
+ options.spaceKey.trim().length > 0;
110
+ if (!isPersonalMode) {
111
+ warnOnce('default-auth-mode', "[pluto-rtc] `authMode` is not set; defaulting to `anonymous` for compatibility.");
112
+ }
113
+ return 'anonymous';
114
+ }
115
+ export function resolveProjectId(options) {
116
+ const projectId = typeof options.projectId === 'string' ? options.projectId.trim() : '';
117
+ if (projectId)
118
+ return projectId;
119
+ warnOnce('default-project-id', "[pluto-rtc] `projectId` is not set; defaulting to the built-in PlutoRTC Firebase project.");
120
+ return PLUTO_HOSTED_PROJECT_ID;
121
+ }
122
+ function sanitizeStorageSegment(value) {
123
+ return value
124
+ .trim()
125
+ .replace(/[^a-zA-Z0-9._-]+/g, '-')
126
+ .replace(/-+/g, '-')
127
+ .replace(/^-|-$/g, '');
128
+ }
129
+ export function resolveStorageNamespace(options) {
130
+ const explicitPrefix = 'storagePrefix' in options && typeof options.storagePrefix === 'string'
131
+ ? sanitizeStorageSegment(options.storagePrefix)
132
+ : '';
133
+ if (explicitPrefix) {
134
+ return explicitPrefix;
135
+ }
136
+ const appTag = sanitizeStorageSegment(resolveAppTag(options));
137
+ return appTag || 'default';
138
+ }
@@ -0,0 +1,12 @@
1
+ export interface ITransport {
2
+ readonly name: string;
3
+ readonly isReady: boolean;
4
+ init(localNodeId: string, remoteNodeId: string, options: any): Promise<void>;
5
+ send(data: Uint8Array): Promise<void>;
6
+ close(): void;
7
+ onMessage(callback: (data: Uint8Array) => void): void;
8
+ onStateChange(callback: (state: 'connecting' | 'connected' | 'failed' | 'disconnected') => void): void;
9
+ handleSignalingMessage?(msg: any): void;
10
+ getSignalingHandler?(): (msg: any) => Promise<void>;
11
+ getNativeTransport?(): any;
12
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,41 @@
1
+ import { LocalDaemonEventName, LocalDaemonEventPayloadMap, LocalDaemonInfo } from '../types';
2
+ type EventHandler<T extends LocalDaemonEventName> = (payload: LocalDaemonEventPayloadMap[T]) => void;
3
+ type BridgeStatus = {
4
+ enabled: boolean;
5
+ connected: boolean;
6
+ endpoint?: string;
7
+ daemonDeviceId?: string;
8
+ lastError?: string;
9
+ };
10
+ type LocalDaemonBridgeOptions = {
11
+ enabled?: boolean;
12
+ autoDiscover?: boolean;
13
+ connectTimeoutMs?: number;
14
+ eventAllowlist?: LocalDaemonEventName[];
15
+ };
16
+ type DiscoveryFn = () => Promise<LocalDaemonInfo[]>;
17
+ export declare class LocalDaemonBridge {
18
+ private options;
19
+ private discoverDaemons;
20
+ private socket;
21
+ private connectedInfo;
22
+ private handlers;
23
+ private pendingSubscriptions;
24
+ private enabled;
25
+ private lastError;
26
+ constructor(discoverDaemons: DiscoveryFn, options?: LocalDaemonBridgeOptions);
27
+ configure(options?: LocalDaemonBridgeOptions): void;
28
+ getStatus(): BridgeStatus;
29
+ enable(options?: LocalDaemonBridgeOptions): Promise<boolean>;
30
+ disable(): Promise<void>;
31
+ maybeAutoDiscover(): Promise<void>;
32
+ subscribe<T extends LocalDaemonEventName>(eventName: T, handler: EventHandler<T>): () => void;
33
+ private connect;
34
+ private openSocket;
35
+ private installSocketHandlers;
36
+ private handleRawMessage;
37
+ private dispatch;
38
+ private sendSubscriptions;
39
+ private sendUnsubscribe;
40
+ }
41
+ export {};
@@ -0,0 +1,227 @@
1
+ function nowMs() {
2
+ return Date.now();
3
+ }
4
+ function toProtocol(token) {
5
+ return `pluto-local.${token}`;
6
+ }
7
+ export class LocalDaemonBridge {
8
+ constructor(discoverDaemons, options) {
9
+ this.socket = null;
10
+ this.connectedInfo = null;
11
+ this.handlers = new Map();
12
+ this.pendingSubscriptions = new Set();
13
+ this.enabled = false;
14
+ this.discoverDaemons = discoverDaemons;
15
+ this.options = {
16
+ enabled: options?.enabled ?? false,
17
+ autoDiscover: options?.autoDiscover ?? false,
18
+ connectTimeoutMs: options?.connectTimeoutMs ?? 1500,
19
+ eventAllowlist: options?.eventAllowlist ?? [],
20
+ };
21
+ this.enabled = this.options.enabled;
22
+ }
23
+ configure(options) {
24
+ if (!options)
25
+ return;
26
+ this.options = {
27
+ enabled: options.enabled ?? this.options.enabled,
28
+ autoDiscover: options.autoDiscover ?? this.options.autoDiscover,
29
+ connectTimeoutMs: options.connectTimeoutMs ?? this.options.connectTimeoutMs,
30
+ eventAllowlist: options.eventAllowlist ?? this.options.eventAllowlist,
31
+ };
32
+ this.enabled = this.options.enabled;
33
+ }
34
+ getStatus() {
35
+ return {
36
+ enabled: this.enabled,
37
+ connected: !!this.socket && this.socket.readyState === WebSocket.OPEN,
38
+ endpoint: this.connectedInfo
39
+ ? `ws://${this.connectedInfo.host}:${this.connectedInfo.localPort}`
40
+ : undefined,
41
+ daemonDeviceId: this.connectedInfo?.deviceId,
42
+ lastError: this.lastError,
43
+ };
44
+ }
45
+ async enable(options) {
46
+ this.configure({ ...options, enabled: true });
47
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
48
+ return true;
49
+ }
50
+ return this.connect();
51
+ }
52
+ async disable() {
53
+ this.enabled = false;
54
+ if (this.socket) {
55
+ try {
56
+ this.socket.close();
57
+ }
58
+ catch {
59
+ // noop
60
+ }
61
+ }
62
+ this.socket = null;
63
+ this.connectedInfo = null;
64
+ }
65
+ async maybeAutoDiscover() {
66
+ if (!this.enabled || !this.options.autoDiscover)
67
+ return;
68
+ if (this.socket && this.socket.readyState === WebSocket.OPEN)
69
+ return;
70
+ await this.connect();
71
+ }
72
+ subscribe(eventName, handler) {
73
+ const allowed = this.options.eventAllowlist;
74
+ if (allowed.length > 0 && !allowed.includes(eventName)) {
75
+ throw new Error(`Event '${eventName}' is not in daemon bridge allowlist`);
76
+ }
77
+ let set = this.handlers.get(eventName);
78
+ if (!set) {
79
+ set = new Set();
80
+ this.handlers.set(eventName, set);
81
+ }
82
+ const rawHandler = handler;
83
+ set.add(rawHandler);
84
+ this.pendingSubscriptions.add(eventName);
85
+ this.sendSubscriptions();
86
+ return () => {
87
+ const current = this.handlers.get(eventName);
88
+ if (!current)
89
+ return;
90
+ current.delete(rawHandler);
91
+ if (current.size === 0) {
92
+ this.handlers.delete(eventName);
93
+ this.pendingSubscriptions.delete(eventName);
94
+ this.sendUnsubscribe(eventName);
95
+ }
96
+ };
97
+ }
98
+ async connect() {
99
+ if (!this.enabled)
100
+ return false;
101
+ if (typeof window === 'undefined' || typeof WebSocket === 'undefined') {
102
+ this.lastError = 'WebSocket is unavailable in this runtime';
103
+ return false;
104
+ }
105
+ const daemons = await this.discoverDaemons().catch((error) => {
106
+ this.lastError = error instanceof Error ? error.message : String(error);
107
+ return [];
108
+ });
109
+ const now = nowMs();
110
+ const candidates = daemons
111
+ .filter((entry) => entry.expiresAt > now)
112
+ .sort((a, b) => (b.updatedAt || b.expiresAt) - (a.updatedAt || a.expiresAt));
113
+ for (const candidate of candidates) {
114
+ try {
115
+ const socket = await this.openSocket(candidate, this.options.connectTimeoutMs);
116
+ this.lastError = undefined;
117
+ this.socket = socket;
118
+ this.connectedInfo = candidate;
119
+ this.installSocketHandlers(socket);
120
+ this.sendSubscriptions();
121
+ return true;
122
+ }
123
+ catch (error) {
124
+ this.lastError = error instanceof Error ? error.message : String(error);
125
+ }
126
+ }
127
+ return false;
128
+ }
129
+ openSocket(candidate, timeoutMs) {
130
+ const host = candidate.host || '127.0.0.1';
131
+ const url = `ws://${host}:${candidate.localPort}/pluto-rtc?token=${encodeURIComponent(candidate.localToken)}`;
132
+ const protocol = toProtocol(candidate.localToken);
133
+ return new Promise((resolve, reject) => {
134
+ const socket = new WebSocket(url, [protocol]);
135
+ const timeout = window.setTimeout(() => {
136
+ try {
137
+ socket.close();
138
+ }
139
+ catch {
140
+ // noop
141
+ }
142
+ reject(new Error('timeout opening daemon bridge socket'));
143
+ }, timeoutMs);
144
+ socket.onopen = () => {
145
+ window.clearTimeout(timeout);
146
+ resolve(socket);
147
+ };
148
+ socket.onerror = () => {
149
+ window.clearTimeout(timeout);
150
+ reject(new Error('daemon bridge socket open failed'));
151
+ };
152
+ });
153
+ }
154
+ installSocketHandlers(socket) {
155
+ socket.onmessage = (event) => {
156
+ if (typeof event.data !== 'string')
157
+ return;
158
+ this.handleRawMessage(event.data);
159
+ };
160
+ socket.onclose = () => {
161
+ this.socket = null;
162
+ this.connectedInfo = null;
163
+ };
164
+ socket.onerror = () => {
165
+ this.lastError = 'daemon bridge socket error';
166
+ };
167
+ }
168
+ handleRawMessage(raw) {
169
+ let parsed;
170
+ try {
171
+ parsed = JSON.parse(raw);
172
+ }
173
+ catch {
174
+ return;
175
+ }
176
+ if (parsed?.type === 'event' && typeof parsed.eventType === 'string') {
177
+ this.dispatch(parsed.eventType, parsed.payload ?? {});
178
+ return;
179
+ }
180
+ if (typeof parsed?.type === 'string' && parsed.payload && parsed.type.startsWith('local.')) {
181
+ this.dispatch(parsed.type, parsed.payload);
182
+ return;
183
+ }
184
+ if (parsed?.type === 'subscribed' && Array.isArray(parsed.events)) {
185
+ return;
186
+ }
187
+ if (parsed?.type === 'error') {
188
+ this.lastError = typeof parsed.message === 'string' ? parsed.message : 'daemon bridge error';
189
+ }
190
+ }
191
+ dispatch(eventName, payload) {
192
+ const listeners = this.handlers.get(eventName);
193
+ if (!listeners || listeners.size === 0)
194
+ return;
195
+ for (const handler of listeners) {
196
+ try {
197
+ handler(payload);
198
+ }
199
+ catch (error) {
200
+ console.error('[LocalDaemonBridge] handler failed:', error);
201
+ }
202
+ }
203
+ }
204
+ sendSubscriptions() {
205
+ if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
206
+ return;
207
+ }
208
+ const events = Array.from(this.pendingSubscriptions);
209
+ if (events.length === 0)
210
+ return;
211
+ const envelope = {
212
+ type: 'subscribeLocalEvents',
213
+ events,
214
+ };
215
+ this.socket.send(JSON.stringify(envelope));
216
+ }
217
+ sendUnsubscribe(eventName) {
218
+ if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
219
+ return;
220
+ }
221
+ const envelope = {
222
+ type: 'unsubscribeLocalEvents',
223
+ events: [eventName],
224
+ };
225
+ this.socket.send(JSON.stringify(envelope));
226
+ }
227
+ }
@@ -0,0 +1,28 @@
1
+ import { Device, ISignalingBackend } from '../types';
2
+ import { WasmSignalingBackend } from './WasmSignaling';
3
+ declare global {
4
+ interface Window {
5
+ __TAURI_INTERNALS__?: Record<string, any>;
6
+ }
7
+ }
8
+ export declare class TauriSignalingBackend extends WasmSignalingBackend implements ISignalingBackend {
9
+ private daemonSignalingEnabled;
10
+ private subscriptionNonce;
11
+ private lastSyncedAuthToken;
12
+ private cachedLocalDeviceId;
13
+ constructor(options: {
14
+ tag: string;
15
+ storagePrefix?: string;
16
+ nodeIdPersistence?: 'persistent' | 'ephemeral';
17
+ });
18
+ setDaemonSignalingEnabled(enabled: boolean): void;
19
+ private ensureDaemonAuth;
20
+ private resolveLocalDeviceId;
21
+ private withPresenceMetadata;
22
+ updatePresence(localNodeId: string, ticketStr: string, isOnline?: boolean, ttlMs?: number, metadata?: string): Promise<void>;
23
+ searchDevices(excludeNodeId?: string): Promise<Device[]>;
24
+ setOffline(localNodeId: string): Promise<void>;
25
+ onDevicesChange(callback: (devices: Device[]) => void, excludeNodeId?: string): () => void;
26
+ subscribeDevices(userId: string): Promise<ReadableStream<import('../types').DeviceEvent[]>>;
27
+ subscribeSessions(localDeviceId: string): Promise<ReadableStream<import('../types').SessionEvent[]>>;
28
+ }