@openlv/session 0.1.3 → 0.2.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.
@@ -0,0 +1,2 @@
1
+ export declare const loadSignaling: (protocol: string) => Promise<import("@openlv/signaling").SignalingProtocol>;
2
+ export declare const loadTransport: (protocol: string) => Promise<import("@openlv/transport").Transport>;
@@ -0,0 +1,26 @@
1
+ import { match } from "ts-pattern";
2
+ export const loadSignaling = async (protocol) => match(protocol)
3
+ .with("mqtt", async () => {
4
+ const module_ = await import("@openlv/signaling/mqtt");
5
+ return module_.mqtt;
6
+ })
7
+ .with("ntfy", async () => {
8
+ const module_ = await import("@openlv/signaling/ntfy");
9
+ return module_.ntfy;
10
+ })
11
+ .with("gun", async () => {
12
+ const module_ = await import("@openlv/signaling/gundb");
13
+ return module_.gundb;
14
+ })
15
+ .otherwise(() => {
16
+ throw new Error(`Unknown signaling protocol: ${protocol}`);
17
+ });
18
+ export const loadTransport = async (protocol) => match(protocol)
19
+ .with("webrtc", async () => {
20
+ const module_ = await import("@openlv/transport/webrtc");
21
+ return module_.webrtc;
22
+ })
23
+ .otherwise(() => {
24
+ throw new Error(`Unknown transport protocol: ${protocol}`);
25
+ });
26
+ //# sourceMappingURL=dynamic.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dynamic.js","sourceRoot":"","sources":["../src/dynamic.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC,MAAM,CAAC,MAAM,aAAa,GAAG,KAAK,EAAE,QAAgB,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC;KACrE,IAAI,CAAC,MAAM,EAAE,KAAK,IAAI,EAAE;IACvB,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,wBAAwB,CAAC,CAAC;IAEvD,OAAO,OAAO,CAAC,IAAI,CAAC;AACtB,CAAC,CAAC;KACD,IAAI,CAAC,MAAM,EAAE,KAAK,IAAI,EAAE;IACvB,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,wBAAwB,CAAC,CAAC;IAEvD,OAAO,OAAO,CAAC,IAAI,CAAC;AACtB,CAAC,CAAC;KACD,IAAI,CAAC,KAAK,EAAE,KAAK,IAAI,EAAE;IACtB,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,yBAAyB,CAAC,CAAC;IAExD,OAAO,OAAO,CAAC,KAAK,CAAC;AACvB,CAAC,CAAC;KACD,SAAS,CAAC,GAAG,EAAE;IACd,MAAM,IAAI,KAAK,CAAC,+BAA+B,QAAQ,EAAE,CAAC,CAAC;AAC7D,CAAC,CAAC,CAAC;AAEL,MAAM,CAAC,MAAM,aAAa,GAAG,KAAK,EAAE,QAAgB,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC;KACrE,IAAI,CAAC,QAAQ,EAAE,KAAK,IAAI,EAAE;IACzB,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,0BAA0B,CAAC,CAAC;IAEzD,OAAO,OAAO,CAAC,MAAM,CAAC;AACxB,CAAC,CAAC;KACD,SAAS,CAAC,GAAG,EAAE;IACd,MAAM,IAAI,KAAK,CAAC,+BAA+B,QAAQ,EAAE,CAAC,CAAC;AAC7D,CAAC,CAAC,CAAC","sourcesContent":["import { match } from \"ts-pattern\";\n\nexport const loadSignaling = async (protocol: string) => match(protocol)\n .with(\"mqtt\", async () => {\n const module_ = await import(\"@openlv/signaling/mqtt\");\n\n return module_.mqtt;\n })\n .with(\"ntfy\", async () => {\n const module_ = await import(\"@openlv/signaling/ntfy\");\n\n return module_.ntfy;\n })\n .with(\"gun\", async () => {\n const module_ = await import(\"@openlv/signaling/gundb\");\n\n return module_.gundb;\n })\n .otherwise(() => {\n throw new Error(`Unknown signaling protocol: ${protocol}`);\n });\n\nexport const loadTransport = async (protocol: string) => match(protocol)\n .with(\"webrtc\", async () => {\n const module_ = await import(\"@openlv/transport/webrtc\");\n\n return module_.webrtc;\n })\n .otherwise(() => {\n throw new Error(`Unknown transport protocol: ${protocol}`);\n });\n"]}
package/dist/events.d.ts CHANGED
@@ -1,10 +1,8 @@
1
- import type { SessionStateObject } from "./index.js";
2
1
  export type SessionEvents = {
3
- state_change: (state?: SessionStateObject) => void;
4
2
  /**
5
- * Emitted when a request arrives from the remote peer — before the reply
6
- * is sent. Useful for observability, logging, or UI integration without
7
- * needing to intercept the `onMessage` callback.
8
- */
3
+ * Emitted when a request arrives from the remote peer -- before the reply
4
+ * is sent. Useful for observability, logging, or UI integration without
5
+ * needing to intercept the `onMessage` callback.
6
+ */
9
7
  request: (payload: object | string) => void;
10
8
  };
@@ -1 +1 @@
1
- {"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"","sourcesContent":["import type { SessionStateObject } from \"./index.js\";\n\nexport type SessionEvents = {\n state_change: (state?: SessionStateObject) => void;\n /**\n * Emitted when a request arrives from the remote peer — before the reply\n * is sent. Useful for observability, logging, or UI integration without\n * needing to intercept the `onMessage` callback.\n */\n request: (payload: object | string) => void;\n};\n"]}
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"","sourcesContent":["export type SessionEvents = {\n /**\n * Emitted when a request arrives from the remote peer -- before the reply\n * is sent. Useful for observability, logging, or UI integration without\n * needing to intercept the `onMessage` callback.\n */\n request: (payload: object | string) => void;\n};\n"]}
package/dist/index.d.ts CHANGED
@@ -1,10 +1,11 @@
1
- import { type SessionHandshakeParameters, type SessionLinkParameters } from "@openlv/core";
2
- import { type PeerInfo, type SignalingLayer, type SignalingProtocol, type SignalState } from "@openlv/signaling";
3
- import { type TransportLayer, type TransportLayerFn } from "@openlv/transport";
1
+ import { type Observable, type SessionHandshakeParameters, type SessionLinkParameters } from "@openlv/core";
2
+ import { type PeerInfo, type SignalingLayer, Status as SignalStatus } from "@openlv/signaling";
3
+ import { type TransportLayer, type TransportLayerFunction } from "@openlv/transport";
4
4
  import { EventEmitter } from "eventemitter3";
5
5
  import type { SessionEvents } from "./events.js";
6
- export type { PeerCapabilities, PeerInfo } from "@openlv/signaling";
7
- export declare const SESSION_STATE: {
6
+ export { loadSignaling, loadTransport } from "./dynamic.js";
7
+ export type { PeerInfo } from "@openlv/signaling";
8
+ export declare const SessionStatus: {
8
9
  readonly CREATED: "created";
9
10
  readonly SIGNALING: "signaling";
10
11
  readonly READY: "ready";
@@ -12,19 +13,8 @@ export declare const SESSION_STATE: {
12
13
  readonly CONNECTED: "connected";
13
14
  readonly DISCONNECTED: "disconnected";
14
15
  };
15
- export type SessionState = (typeof SESSION_STATE)[keyof typeof SESSION_STATE];
16
- export type SessionStateObject = {
17
- status: SessionState;
18
- signaling?: {
19
- state: SignalState;
20
- };
21
- /** The peer's self-description, set once capabilities are exchanged. */
22
- peerInfo?: PeerInfo;
23
- /** Human-readable reason for the last disconnect/failure, if any. */
24
- error?: string;
25
- };
16
+ export type SessionStatus = (typeof SessionStatus)[keyof typeof SessionStatus];
26
17
  export type SessionOptions = {
27
- /** Shared with the peer during the handshake and shown in its UI. */
28
18
  info?: PeerInfo;
29
19
  };
30
20
  /**
@@ -33,21 +23,13 @@ export type SessionOptions = {
33
23
  * https://openlv.sh/api/session
34
24
  */
35
25
  export type Session = {
36
- getState(): SessionStateObject;
26
+ status: Observable<SessionStatus>;
27
+ signalStatus: Observable<SignalStatus>;
28
+ error: Observable<string | undefined>;
29
+ peerInfo: Observable<PeerInfo | undefined>;
37
30
  getHandshakeParameters(): SessionHandshakeParameters;
38
31
  connect(): Promise<void>;
39
- waitForLink(): Promise<void>;
40
32
  close(): Promise<void>;
41
- /**
42
- * Send a request to the remote peer and await a correlated response.
43
- *
44
- * Two-phase timeout:
45
- * - `ackTimeout` (default 10 s): the remote peer must send back an ack
46
- * within this window, confirming it received the message.
47
- * - After an ack arrives the wait is extended to `responseTimeout`
48
- * (default 1 hour) — enough for user-interactive flows such as
49
- * `eth_sendTransaction` or `personal_sign`.
50
- */
51
33
  send(message: object, ackTimeout?: number, responseTimeout?: number): Promise<unknown>;
52
34
  emitter: EventEmitter<SessionEvents>;
53
35
  _internal: {
@@ -61,8 +43,8 @@ export type Session = {
61
43
  *
62
44
  * https://openlv.sh/api/session
63
45
  */
64
- export declare const createSession: (initParameters: SessionLinkParameters, signalLayer: SignalingProtocol, transportLayers: TransportLayerFn[], onMessage: (message: object) => Promise<object | string>, options?: SessionOptions) => Promise<Session>;
46
+ export declare const createSession: (initParameters: SessionLinkParameters, transportLayers: TransportLayerFunction[], onMessage: (message: object) => Promise<object | string>, options?: SessionOptions) => Promise<Session>;
65
47
  /**
66
48
  * Connect to a session from its openlv:// URL
67
49
  */
68
- export declare const connectSession: (connectionUrl: string, onMessage: (message: object) => Promise<object | string>, transports: TransportLayerFn[], options?: SessionOptions) => Promise<Session>;
50
+ export declare const connectSession: (connectionUrl: string, onMessage: (message: object) => Promise<object | string>, transports: TransportLayerFunction[], options?: SessionOptions) => Promise<Session>;
package/dist/index.js CHANGED
@@ -1,13 +1,13 @@
1
- import { decodeConnectionURL, OPENLV_PROTOCOL_VERSION, } from "@openlv/core";
2
- import { deriveSymmetricKey, generateHandshakeKey, generateSessionId, initEncryptionKeys, initHash, parseEncryptionKey, } from "@openlv/core/encryption";
3
- import { SIGNAL_STATE, validatePeerInfo, } from "@openlv/signaling";
4
- import { dynamicSignalingLayer } from "@openlv/signaling/dynamic";
5
- import { TRANSPORT_STATE, } from "@openlv/transport";
1
+ import { decodeConnectionURL, observable, OPENLV_PROTOCOL_VERSION, } from "@openlv/core";
2
+ import { deriveSymmetricKey, generateHandshakeKey, generateSessionId, initEncryptionKeys, initHash, } from "@openlv/core/encryption";
3
+ import { Status as SignalStatus, validatePeerInfo, } from "@openlv/signaling";
4
+ import { Status as TransportStatus, } from "@openlv/transport";
6
5
  import { EventEmitter } from "eventemitter3";
6
+ import { loadSignaling } from "./dynamic.js";
7
7
  import { awaitCorrelatedResponse } from "./messages/correlate.js";
8
- import { selectTransportId } from "./transports.js";
9
8
  import { log } from "./utils/log.js";
10
- export const SESSION_STATE = {
9
+ export { loadSignaling, loadTransport } from "./dynamic.js";
10
+ export const SessionStatus = {
11
11
  CREATED: "created",
12
12
  SIGNALING: "signaling",
13
13
  READY: "ready",
@@ -20,13 +20,11 @@ export const SESSION_STATE = {
20
20
  *
21
21
  * https://openlv.sh/api/session
22
22
  */
23
- export const createSession = async (initParameters, signalLayer, transportLayers, onMessage, options) => {
23
+ export const createSession = async (initParameters, transportLayers, onMessage, options) => {
24
24
  if (transportLayers.length === 0) {
25
25
  throw new Error("At least one transport is required");
26
26
  }
27
27
  if (options?.info) {
28
- // A receiver silently drops out-of-bounds info with the whole packet,
29
- // which would surface only as a handshake timeout — fail loudly here.
30
28
  const problem = validatePeerInfo(options.info);
31
29
  if (problem)
32
30
  throw new Error(`Invalid session info: ${problem}`);
@@ -37,70 +35,65 @@ export const createSession = async (initParameters, signalLayer, transportLayers
37
35
  ? initParameters.sessionId
38
36
  : generateSessionId();
39
37
  const { encryptionKey, decryptionKey: { decrypt }, } = await initEncryptionKeys(initParameters);
40
- let relyingPublicKey;
41
38
  const handshakeKey = "k" in initParameters
42
39
  ? await deriveSymmetricKey(initParameters.k)
43
40
  : await generateHandshakeKey();
44
41
  const { hash, isHost } = await initHash("h" in initParameters ? initParameters.h : undefined, encryptionKey);
45
- let status = SESSION_STATE.CREATED;
46
- let lastError;
47
42
  const protocol = initParameters.p;
48
43
  const server = initParameters.s;
49
44
  const capabilities = {
50
45
  transports: transportLayers.map(layer => layer.transportId),
51
46
  ...(options?.info && { info: options.info }),
52
47
  };
53
- let peerCaps;
48
+ const [status, setStatus] = observable(SessionStatus.CREATED);
49
+ const [lastError, setLastError] = observable(undefined);
50
+ const [peerInfo, setPeerInfo] = observable(undefined);
51
+ const setLastErrorIfUnset = (reason) => {
52
+ if (lastError.get() === undefined)
53
+ setLastError(reason);
54
+ };
54
55
  const updateStatus = (newStatus) => {
55
56
  log("updateStatus", newStatus);
56
- status = newStatus;
57
- emitter.emit("state_change", {
58
- status,
59
- signaling: signal.getState(),
60
- peerInfo: peerCaps?.info,
61
- error: lastError,
62
- });
57
+ setStatus(newStatus);
63
58
  };
59
+ const signalLayer = await loadSignaling(protocol);
64
60
  const signaling = await signalLayer({
65
61
  topic: sessionId,
66
62
  url: server,
67
63
  });
64
+ const canEncrypt = () => signal.peerKey.get() !== undefined;
68
65
  const signal = await signaling({
69
66
  h: hash,
70
- canEncrypt() {
71
- return relyingPublicKey !== undefined;
72
- },
67
+ canEncrypt,
73
68
  async encrypt(message) {
74
- if (!relyingPublicKey) {
69
+ const key = signal.peerKey.get();
70
+ if (!key) {
75
71
  throw new Error("Relying party public key not found");
76
72
  }
77
- return await relyingPublicKey.encrypt(message);
73
+ return await key.encrypt(message);
78
74
  },
79
75
  decrypt,
80
76
  publicKey: encryptionKey,
81
77
  k: handshakeKey,
82
- async rpDiscovered(rpKey) {
83
- const role = isHost ? "host" : "client";
84
- log(`rpKey discovered by ${role}`, rpKey);
85
- relyingPublicKey = await parseEncryptionKey(rpKey);
86
- },
87
78
  capabilities,
88
- peerCapabilities(received) {
89
- log("peer capabilities received", received);
90
- peerCaps = received;
91
- // Re-emit the current status so observers see peerInfo the moment the
92
- // capabilities exchange completes, not at the next status transition.
93
- updateStatus(status);
94
- },
95
79
  isHost,
96
80
  });
81
+ signal.peerKey.subscribe((key) => {
82
+ const role = isHost ? "host" : "client";
83
+ log(`rpKey discovered by ${role}`, key);
84
+ });
85
+ signal.peerCapabilities.subscribe((received) => {
86
+ log("peer capabilities received", received);
87
+ setPeerInfo(received?.info);
88
+ });
97
89
  let transport;
98
90
  const createTransport = (layer) => layer.create({
99
- encrypt(message) {
100
- if (!relyingPublicKey) {
91
+ async encrypt(message) {
92
+ const key = signal.peerKey.get();
93
+ if (!key) {
101
94
  throw new Error("Relying party public key not found");
102
95
  }
103
- return relyingPublicKey?.encrypt(message);
96
+ return await key.encrypt(message);
104
97
  },
105
98
  decrypt,
106
99
  isHost,
@@ -149,9 +142,14 @@ export const createSession = async (initParameters, signalLayer, transportLayers
149
142
  * keep the first configured transport.
150
143
  */
151
144
  const selectTransportLayer = () => {
145
+ const peerCaps = signal.peerCapabilities.get();
152
146
  if (!peerCaps)
153
147
  return transportLayers[0];
154
- const selected = selectTransportId(isHost, capabilities.transports, peerCaps.transports);
148
+ // The host's preferred transport that the client also supports; both
149
+ // peers compute the same result independently -- no confirmation round trip.
150
+ const hostPreference = isHost ? capabilities.transports : peerCaps.transports;
151
+ const clientSupported = new Set(isHost ? peerCaps.transports : capabilities.transports);
152
+ const selected = hostPreference.find(transportId => clientSupported.has(transportId));
155
153
  return transportLayers.find(layer => layer.transportId === selected);
156
154
  };
157
155
  // Once both peers are present (signaling encrypted) the transport should
@@ -159,53 +157,54 @@ export const createSession = async (initParameters, signalLayer, transportLayers
159
157
  // network) fail loudly instead of sitting in "linking" forever.
160
158
  const TRANSPORT_LINK_TIMEOUT_MS = 45000;
161
159
  let linkDeadline;
160
+ let unsubscribeTransportStatus;
162
161
  const clearLinkDeadline = () => {
163
162
  clearTimeout(linkDeadline);
164
163
  linkDeadline = undefined;
165
164
  };
166
- const onTransportStateChange = (state) => {
167
- log("transport state change", state);
168
- if (state === TRANSPORT_STATE.CONNECTED) {
165
+ const onTransportStateChange = (transportStatus) => {
166
+ log("transport state change", transportStatus);
167
+ if (transportStatus === TransportStatus.CONNECTED) {
169
168
  clearLinkDeadline();
170
- lastError = undefined;
171
- updateStatus(SESSION_STATE.CONNECTED);
169
+ setLastError(undefined);
170
+ updateStatus(SessionStatus.CONNECTED);
172
171
  }
173
- if (state === TRANSPORT_STATE.ERROR) {
172
+ if (transportStatus === TransportStatus.ERROR) {
174
173
  clearLinkDeadline();
175
- lastError ?? (lastError = "Peer-to-peer transport failed");
176
- updateStatus(SESSION_STATE.DISCONNECTED);
174
+ setLastErrorIfUnset("Peer-to-peer transport failed");
175
+ updateStatus(SessionStatus.DISCONNECTED);
177
176
  }
178
177
  };
179
178
  const onTransportError = (reason) => {
180
179
  if (reason)
181
- lastError = reason;
180
+ setLastError(reason);
182
181
  };
183
182
  const startTransport = () => {
184
183
  if (!transport) {
185
184
  const layer = selectTransportLayer();
186
185
  if (!layer) {
187
- log("no common transport with peer", capabilities.transports, peerCaps?.transports);
188
- lastError ?? (lastError = "No common transport with peer");
189
- updateStatus(SESSION_STATE.DISCONNECTED);
186
+ log("no common transport with peer", capabilities.transports, signal.peerCapabilities.get()?.transports);
187
+ setLastErrorIfUnset("No common transport with peer");
188
+ updateStatus(SessionStatus.DISCONNECTED);
190
189
  return;
191
190
  }
192
191
  log("selected transport", layer.transportId);
193
192
  transport = createTransport(layer);
194
- transport.emitter.on("state_change", onTransportStateChange);
193
+ unsubscribeTransportStatus = transport.status.subscribe(onTransportStateChange);
195
194
  transport.emitter.on("error", onTransportError);
196
195
  }
197
196
  linkDeadline ?? (linkDeadline = setTimeout(() => {
198
- if (status === SESSION_STATE.CONNECTED)
197
+ if (status.get() === SessionStatus.CONNECTED)
199
198
  return;
200
199
  log("transport failed to connect in time");
201
- lastError ?? (lastError = "Timed out establishing the peer-to-peer connection");
202
- updateStatus(SESSION_STATE.DISCONNECTED);
200
+ setLastErrorIfUnset("Timed out establishing the peer-to-peer connection");
201
+ updateStatus(SessionStatus.DISCONNECTED);
203
202
  }, TRANSPORT_LINK_TIMEOUT_MS));
204
203
  Promise.resolve(transport.setup()).catch((error) => {
205
204
  log("transport setup failed", error);
206
205
  clearLinkDeadline();
207
- lastError ?? (lastError = error instanceof Error ? error.message : "Transport setup failed");
208
- updateStatus(SESSION_STATE.DISCONNECTED);
206
+ setLastErrorIfUnset(error instanceof Error ? error.message : "Transport setup failed");
207
+ updateStatus(SessionStatus.DISCONNECTED);
209
208
  });
210
209
  };
211
210
  const onSignalMessage = async (message) => {
@@ -214,7 +213,7 @@ export const createSession = async (initParameters, signalLayer, transportLayers
214
213
  if (sessionMessage.type === "response") {
215
214
  messages.emit("message", sessionMessage);
216
215
  }
217
- if (sessionMessage.type === "request") {
216
+ else if (sessionMessage.type === "request") {
218
217
  if (!transport) {
219
218
  log("dropping negotiation message: transport not selected yet");
220
219
  return;
@@ -229,55 +228,52 @@ export const createSession = async (initParameters, signalLayer, transportLayers
229
228
  }
230
229
  }
231
230
  };
232
- const onSignalStateChange = (state) => {
233
- log("signal state change", state);
234
- if (state === SIGNAL_STATE.READY) {
235
- updateStatus(SESSION_STATE.READY);
231
+ const onSignalStateChange = (signalStatus) => {
232
+ log("signal state change", signalStatus);
233
+ if (signalStatus === SignalStatus.READY) {
234
+ updateStatus(SessionStatus.READY);
236
235
  }
237
236
  if ([
238
- SIGNAL_STATE.HANDSHAKE,
239
- SIGNAL_STATE.HANDSHAKE_PARTIAL,
240
- ].includes(state)) {
241
- updateStatus(SESSION_STATE.LINKING);
237
+ SignalStatus.HANDSHAKE,
238
+ SignalStatus.HANDSHAKE_PARTIAL,
239
+ ].includes(signalStatus)) {
240
+ updateStatus(SessionStatus.LINKING);
242
241
  }
243
- if (state === SIGNAL_STATE.ENCRYPTED) {
242
+ if (signalStatus === SignalStatus.ENCRYPTED) {
244
243
  startTransport();
245
244
  }
246
- if (state === SIGNAL_STATE.ERROR) {
247
- log("signaling error — marking session disconnected");
248
- lastError ?? (lastError = "Signaling failed or timed out");
249
- updateStatus(SESSION_STATE.DISCONNECTED);
245
+ if (signalStatus === SignalStatus.ERROR) {
246
+ log("signaling error -- marking session disconnected");
247
+ setLastErrorIfUnset("Signaling failed or timed out");
248
+ updateStatus(SessionStatus.DISCONNECTED);
250
249
  }
251
250
  };
251
+ let unsubscribeSignalStatus;
252
252
  return {
253
253
  connect: async () => {
254
- updateStatus(SESSION_STATE.SIGNALING);
254
+ updateStatus(SessionStatus.SIGNALING);
255
255
  log("connecting to session, isHost:", isHost);
256
256
  signal.on("message", onSignalMessage);
257
- signal.on("state_change", onSignalStateChange);
257
+ unsubscribeSignalStatus = signal.status.subscribe(onSignalStateChange);
258
258
  await signal.setup();
259
259
  },
260
260
  async close() {
261
261
  log("session teardown");
262
262
  clearLinkDeadline();
263
263
  signal.off("message", onSignalMessage);
264
- signal.off("state_change", onSignalStateChange);
265
- transport?.emitter.off("state_change", onTransportStateChange);
264
+ unsubscribeSignalStatus?.();
265
+ unsubscribeTransportStatus?.();
266
266
  transport?.emitter.off("error", onTransportError);
267
267
  await Promise.all([
268
268
  transport?.teardown(),
269
269
  signal.teardown(),
270
270
  ]);
271
- updateStatus(SESSION_STATE.DISCONNECTED);
272
- },
273
- getState() {
274
- return {
275
- status,
276
- signaling: signal.getState(),
277
- peerInfo: peerCaps?.info,
278
- error: lastError,
279
- };
271
+ updateStatus(SessionStatus.DISCONNECTED);
280
272
  },
273
+ status,
274
+ signalStatus: signal.status,
275
+ error: lastError,
276
+ peerInfo,
281
277
  getHandshakeParameters() {
282
278
  return {
283
279
  version: OPENLV_PROTOCOL_VERSION,
@@ -288,24 +284,8 @@ export const createSession = async (initParameters, signalLayer, transportLayers
288
284
  s: server,
289
285
  };
290
286
  },
291
- waitForLink: async () => new Promise((resolve, reject) => {
292
- const handler = (state) => {
293
- if (state?.status === SESSION_STATE.CONNECTED) {
294
- emitter.off("state_change", handler);
295
- resolve();
296
- }
297
- else if (state?.status === SESSION_STATE.DISCONNECTED) {
298
- emitter.off("state_change", handler);
299
- reject(new Error(lastError ?? "Session failed to connect"));
300
- }
301
- };
302
- // Subscribe before inspecting the current status so a transition
303
- // between the check and the subscription cannot be missed.
304
- emitter.on("state_change", handler);
305
- handler({ status });
306
- }),
307
287
  async send(message, ackTimeout = 10000, responseTimeout = 60 * 60000) {
308
- if (!transport || signal.getState().state !== SIGNAL_STATE.ENCRYPTED) {
288
+ if (!transport || signal.status.get() !== SignalStatus.ENCRYPTED) {
309
289
  throw new Error("Session not ready");
310
290
  }
311
291
  const messageId = crypto.randomUUID();
@@ -331,10 +311,6 @@ export const createSession = async (initParameters, signalLayer, transportLayers
331
311
  */
332
312
  export const connectSession = async (connectionUrl, onMessage, transports, options) => {
333
313
  const initParameters = decodeConnectionURL(connectionUrl);
334
- const signaling = await dynamicSignalingLayer(initParameters.p);
335
- if (!signaling) {
336
- throw new Error(`Invalid signaling protocol: ${initParameters.p}`);
337
- }
338
- return createSession(initParameters, signaling, transports, onMessage, options);
314
+ return createSession(initParameters, transports, onMessage, options);
339
315
  };
340
316
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n decodeConnectionURL,\n OPENLV_PROTOCOL_VERSION,\n type SessionHandshakeParameters,\n type SessionLinkParameters,\n} from \"@openlv/core\";\nimport {\n deriveSymmetricKey,\n type EncryptionKey,\n generateHandshakeKey,\n generateSessionId,\n initEncryptionKeys,\n initHash,\n parseEncryptionKey,\n} from \"@openlv/core/encryption\";\nimport {\n type PeerCapabilities,\n type PeerInfo,\n SIGNAL_STATE,\n type SignalingLayer,\n type SignalingProtocol,\n type SignalState,\n validatePeerInfo,\n} from \"@openlv/signaling\";\nimport { dynamicSignalingLayer } from \"@openlv/signaling/dynamic\";\nimport {\n TRANSPORT_STATE,\n type TransportLayer,\n type TransportLayerFn,\n type TransportMessage,\n} from \"@openlv/transport\";\nimport { EventEmitter } from \"eventemitter3\";\n\nimport type { SessionEvents } from \"./events.js\";\nimport { awaitCorrelatedResponse } from \"./messages/correlate.js\";\nimport type { SessionMessage } from \"./messages/index.js\";\nimport { selectTransportId } from \"./transports.js\";\nimport { log } from \"./utils/log.js\";\n\nexport type { PeerCapabilities, PeerInfo } from \"@openlv/signaling\";\n\nexport const SESSION_STATE = {\n CREATED: \"created\",\n SIGNALING: \"signaling\",\n READY: \"ready\",\n LINKING: \"linking\",\n CONNECTED: \"connected\",\n DISCONNECTED: \"disconnected\",\n} as const;\nexport type SessionState = (typeof SESSION_STATE)[keyof typeof SESSION_STATE];\n\nexport type SessionStateObject = {\n status: SessionState;\n signaling?: {\n state: SignalState;\n };\n /** The peer's self-description, set once capabilities are exchanged. */\n peerInfo?: PeerInfo;\n /** Human-readable reason for the last disconnect/failure, if any. */\n error?: string;\n};\n\nexport type SessionOptions = {\n /** Shared with the peer during the handshake and shown in its UI. */\n info?: PeerInfo;\n};\n\n/**\n * an OpenLV Session\n *\n * https://openlv.sh/api/session\n */\nexport type Session = {\n getState(): SessionStateObject;\n getHandshakeParameters(): SessionHandshakeParameters;\n connect(): Promise<void>;\n waitForLink(): Promise<void>;\n close(): Promise<void>;\n /**\n * Send a request to the remote peer and await a correlated response.\n *\n * Two-phase timeout:\n * - `ackTimeout` (default 10 s): the remote peer must send back an ack\n * within this window, confirming it received the message.\n * - After an ack arrives the wait is extended to `responseTimeout`\n * (default 1 hour) — enough for user-interactive flows such as\n * `eth_sendTransaction` or `personal_sign`.\n */\n send(message: object, ackTimeout?: number, responseTimeout?: number): Promise<unknown>;\n emitter: EventEmitter<SessionEvents>;\n _internal: {\n signal: SignalingLayer;\n /** Instantiated when signaling reaches ENCRYPTED and a transport is selected. */\n transport: TransportLayer | undefined;\n };\n};\n\n/**\n * OpenLV Session\n *\n * https://openlv.sh/api/session\n */\nexport const createSession = async (\n initParameters: SessionLinkParameters,\n signalLayer: SignalingProtocol,\n transportLayers: TransportLayerFn[],\n onMessage: (message: object) => Promise<object | string>,\n options?: SessionOptions,\n): Promise<Session> => {\n if (transportLayers.length === 0) {\n throw new Error(\"At least one transport is required\");\n }\n\n if (options?.info) {\n // A receiver silently drops out-of-bounds info with the whole packet,\n // which would surface only as a handshake timeout — fail loudly here.\n const problem = validatePeerInfo(options.info);\n\n if (problem) throw new Error(`Invalid session info: ${problem}`);\n }\n\n const emitter = new EventEmitter<SessionEvents>();\n const messages = new EventEmitter<{ message: SessionMessage; }>();\n const sessionId\n = \"sessionId\" in initParameters\n ? initParameters.sessionId\n : generateSessionId();\n const {\n encryptionKey,\n decryptionKey: { decrypt },\n } = await initEncryptionKeys(initParameters);\n let relyingPublicKey: EncryptionKey | undefined;\n const handshakeKey\n = \"k\" in initParameters\n ? await deriveSymmetricKey(initParameters.k)\n : await generateHandshakeKey();\n const { hash, isHost } = await initHash(\n \"h\" in initParameters ? initParameters.h : undefined,\n encryptionKey,\n );\n let status: SessionState = SESSION_STATE.CREATED;\n let lastError: string | undefined;\n const protocol = initParameters.p;\n const server = initParameters.s;\n const capabilities: PeerCapabilities = {\n transports: transportLayers.map(layer => layer.transportId),\n ...(options?.info && { info: options.info }),\n };\n let peerCaps: PeerCapabilities | undefined;\n\n const updateStatus = (newStatus: SessionState) => {\n log(\"updateStatus\", newStatus);\n status = newStatus;\n emitter.emit(\"state_change\", {\n status,\n signaling: signal.getState(),\n peerInfo: peerCaps?.info,\n error: lastError,\n });\n };\n\n const signaling = await signalLayer({\n topic: sessionId,\n url: server,\n });\n const signal = await signaling({\n h: hash,\n canEncrypt() {\n return relyingPublicKey !== undefined;\n },\n async encrypt(message) {\n if (!relyingPublicKey) {\n throw new Error(\"Relying party public key not found\");\n }\n\n return await relyingPublicKey.encrypt(message);\n },\n decrypt,\n publicKey: encryptionKey,\n k: handshakeKey,\n async rpDiscovered(rpKey) {\n const role = isHost ? \"host\" : \"client\";\n\n log(`rpKey discovered by ${role}`, rpKey);\n\n relyingPublicKey = await parseEncryptionKey(rpKey);\n },\n capabilities,\n peerCapabilities(received) {\n log(\"peer capabilities received\", received);\n peerCaps = received;\n // Re-emit the current status so observers see peerInfo the moment the\n // capabilities exchange completes, not at the next status transition.\n updateStatus(status);\n },\n isHost,\n });\n\n let transport: TransportLayer | undefined;\n\n const createTransport = (layer: TransportLayerFn): TransportLayer => layer.create({\n encrypt(message) {\n if (!relyingPublicKey) {\n throw new Error(\"Relying party public key not found\");\n }\n\n return relyingPublicKey?.encrypt(message);\n },\n decrypt,\n isHost,\n onmessage: async (message: { type: string; payload: object; messageId: string; }) => {\n log(\"Session: received message from transport\", message);\n\n if (message[\"type\"] === \"request\") {\n const messageId = message[\"messageId\"] as string;\n\n try {\n // Immediately acknowledge receipt so the sender's ack-timeout does\n // not fire while the handler (which may await user interaction) runs.\n await transport?.send({ type: \"ack\", messageId } satisfies SessionMessage);\n\n // Notify observers before processing (e.g. wallet UI can show a\n // pending indicator before the handler resolves).\n emitter.emit(\"request\", message[\"payload\"] as object);\n\n const data = await onMessage(message[\"payload\"] as object)\n // A throwing handler must still answer, otherwise the peer waits\n // out its full response timeout.\n .catch(() => ({ error: { code: -32_603, message: \"Internal error\" } }));\n\n await transport?.send({\n type: \"response\",\n messageId,\n payload: data,\n } satisfies SessionMessage);\n }\n catch (error) {\n log(\"failed to respond to peer request\", error);\n }\n }\n\n if (message[\"type\"] === \"response\" || message[\"type\"] === \"ack\") {\n // Both acks and responses are forwarded to the send() correlator.\n messages.emit(\"message\", message as SessionMessage);\n }\n },\n async subsend(message) {\n log(\"Session: sending trans msg to signal\", message);\n const sessionMessage: SessionMessage = {\n type: \"request\",\n messageId: crypto.randomUUID(),\n payload: message,\n };\n\n await signal.send(sessionMessage);\n },\n });\n\n /**\n * Sessions resumed with a known peer key never exchange capabilities and\n * keep the first configured transport.\n */\n const selectTransportLayer = (): TransportLayerFn | undefined => {\n if (!peerCaps) return transportLayers[0];\n\n const selected = selectTransportId(isHost, capabilities.transports, peerCaps.transports);\n\n return transportLayers.find(layer => layer.transportId === selected);\n };\n\n // Once both peers are present (signaling encrypted) the transport should\n // connect promptly; if it cannot (e.g. no ICE candidates on a restricted\n // network) fail loudly instead of sitting in \"linking\" forever.\n const TRANSPORT_LINK_TIMEOUT_MS = 45_000;\n let linkDeadline: ReturnType<typeof setTimeout> | undefined;\n\n const clearLinkDeadline = () => {\n clearTimeout(linkDeadline);\n linkDeadline = undefined;\n };\n\n const onTransportStateChange = (state: string) => {\n log(\"transport state change\", state);\n\n if (state === TRANSPORT_STATE.CONNECTED) {\n clearLinkDeadline();\n lastError = undefined;\n updateStatus(SESSION_STATE.CONNECTED);\n }\n\n if (state === TRANSPORT_STATE.ERROR) {\n clearLinkDeadline();\n lastError ??= \"Peer-to-peer transport failed\";\n updateStatus(SESSION_STATE.DISCONNECTED);\n }\n };\n\n const onTransportError = (reason?: string) => {\n if (reason) lastError = reason;\n };\n\n const startTransport = () => {\n if (!transport) {\n const layer = selectTransportLayer();\n\n if (!layer) {\n log(\"no common transport with peer\", capabilities.transports, peerCaps?.transports);\n lastError ??= \"No common transport with peer\";\n updateStatus(SESSION_STATE.DISCONNECTED);\n\n return;\n }\n\n log(\"selected transport\", layer.transportId);\n transport = createTransport(layer);\n transport.emitter.on(\"state_change\", onTransportStateChange);\n transport.emitter.on(\"error\", onTransportError);\n }\n\n linkDeadline ??= setTimeout(() => {\n if (status === SESSION_STATE.CONNECTED) return;\n\n log(\"transport failed to connect in time\");\n lastError ??= \"Timed out establishing the peer-to-peer connection\";\n updateStatus(SESSION_STATE.DISCONNECTED);\n }, TRANSPORT_LINK_TIMEOUT_MS);\n\n Promise.resolve(transport.setup()).catch((error) => {\n log(\"transport setup failed\", error);\n clearLinkDeadline();\n lastError ??= error instanceof Error ? error.message : \"Transport setup failed\";\n updateStatus(SESSION_STATE.DISCONNECTED);\n });\n };\n\n const onSignalMessage = async (message: object) => {\n log(\"Session: received message from signaling\", message);\n\n const sessionMessage = message as SessionMessage;\n\n if (sessionMessage.type === \"response\") {\n messages.emit(\"message\", sessionMessage);\n }\n\n if (sessionMessage.type === \"request\") {\n if (!transport) {\n log(\"dropping negotiation message: transport not selected yet\");\n\n return;\n }\n\n try {\n await transport.handle(sessionMessage.payload as TransportMessage);\n }\n catch (error) {\n // Negotiation payloads come from the (untrusted) relay; a malformed\n // one must not become an unhandled rejection.\n log(\"failed to handle negotiation message\", error);\n }\n }\n };\n\n const onSignalStateChange = (state: SignalState) => {\n log(\"signal state change\", state);\n\n if (state === SIGNAL_STATE.READY) {\n updateStatus(SESSION_STATE.READY);\n }\n\n if (\n (\n [\n SIGNAL_STATE.HANDSHAKE,\n SIGNAL_STATE.HANDSHAKE_PARTIAL,\n ] as SignalState[]\n ).includes(state)\n ) {\n updateStatus(SESSION_STATE.LINKING);\n }\n\n if (state === SIGNAL_STATE.ENCRYPTED) {\n startTransport();\n }\n\n if (state === SIGNAL_STATE.ERROR) {\n log(\"signaling error — marking session disconnected\");\n lastError ??= \"Signaling failed or timed out\";\n updateStatus(SESSION_STATE.DISCONNECTED);\n }\n };\n\n return {\n connect: async () => {\n updateStatus(SESSION_STATE.SIGNALING);\n log(\"connecting to session, isHost:\", isHost);\n\n signal.on(\"message\", onSignalMessage);\n signal.on(\"state_change\", onSignalStateChange);\n\n await signal.setup();\n },\n async close() {\n log(\"session teardown\");\n clearLinkDeadline();\n signal.off(\"message\", onSignalMessage);\n signal.off(\"state_change\", onSignalStateChange);\n transport?.emitter.off(\"state_change\", onTransportStateChange);\n transport?.emitter.off(\"error\", onTransportError);\n await Promise.all([\n transport?.teardown(),\n signal.teardown(),\n ]);\n updateStatus(SESSION_STATE.DISCONNECTED);\n },\n getState() {\n return {\n status,\n signaling: signal.getState(),\n peerInfo: peerCaps?.info,\n error: lastError,\n };\n },\n getHandshakeParameters() {\n return {\n version: OPENLV_PROTOCOL_VERSION,\n sessionId,\n h: hash,\n k: handshakeKey.toString(),\n p: protocol,\n s: server,\n };\n },\n waitForLink: async () => new Promise<void>((resolve, reject) => {\n const handler = (state?: SessionStateObject) => {\n if (state?.status === SESSION_STATE.CONNECTED) {\n emitter.off(\"state_change\", handler);\n resolve();\n }\n else if (state?.status === SESSION_STATE.DISCONNECTED) {\n emitter.off(\"state_change\", handler);\n reject(new Error(lastError ?? \"Session failed to connect\"));\n }\n };\n\n // Subscribe before inspecting the current status so a transition\n // between the check and the subscription cannot be missed.\n emitter.on(\"state_change\", handler);\n handler({ status });\n }),\n async send(\n message: object,\n ackTimeout: number = 10_000,\n responseTimeout: number = 60 * 60_000,\n ) {\n if (!transport || signal.getState().state !== SIGNAL_STATE.ENCRYPTED) {\n throw new Error(\"Session not ready\");\n }\n\n const messageId = crypto.randomUUID();\n const sessionMessage: SessionMessage = {\n type: \"request\",\n messageId,\n payload: message,\n };\n\n await transport.send(sessionMessage);\n\n return awaitCorrelatedResponse(messages, messageId, ackTimeout, responseTimeout);\n },\n _internal: {\n signal,\n get transport() {\n return transport;\n },\n },\n emitter,\n };\n};\n\n/**\n * Connect to a session from its openlv:// URL\n */\nexport const connectSession = async (\n connectionUrl: string,\n onMessage: (message: object) => Promise<object | string>,\n transports: TransportLayerFn[],\n options?: SessionOptions,\n): Promise<Session> => {\n const initParameters = decodeConnectionURL(connectionUrl);\n\n const signaling = await dynamicSignalingLayer(initParameters.p);\n\n if (!signaling) {\n throw new Error(`Invalid signaling protocol: ${initParameters.p}`);\n }\n\n return createSession(initParameters, signaling, transports, onMessage, options);\n};\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n decodeConnectionURL,\n type Observable,\n observable,\n OPENLV_PROTOCOL_VERSION,\n type SessionHandshakeParameters,\n type SessionLinkParameters,\n} from \"@openlv/core\";\nimport {\n deriveSymmetricKey,\n generateHandshakeKey,\n generateSessionId,\n initEncryptionKeys,\n initHash,\n} from \"@openlv/core/encryption\";\nimport {\n type PeerCapabilities,\n type PeerInfo,\n type SignalingLayer,\n Status as SignalStatus,\n validatePeerInfo,\n} from \"@openlv/signaling\";\nimport {\n Status as TransportStatus,\n type TransportLayer,\n type TransportLayerFunction,\n type TransportMessage,\n} from \"@openlv/transport\";\nimport { EventEmitter } from \"eventemitter3\";\n\nimport { loadSignaling } from \"./dynamic.js\";\nimport type { SessionEvents } from \"./events.js\";\nimport { awaitCorrelatedResponse } from \"./messages/correlate.js\";\nimport type { SessionMessage } from \"./messages/index.js\";\nimport { log } from \"./utils/log.js\";\n\nexport { loadSignaling, loadTransport } from \"./dynamic.js\";\n\n// `peerInfo` is part of the session's surface, so consumers must be able to\n// name its type without reaching into @openlv/signaling.\nexport type { PeerInfo } from \"@openlv/signaling\";\n\nexport const SessionStatus = {\n CREATED: \"created\",\n SIGNALING: \"signaling\",\n READY: \"ready\",\n LINKING: \"linking\",\n CONNECTED: \"connected\",\n DISCONNECTED: \"disconnected\",\n} as const;\nexport type SessionStatus = (typeof SessionStatus)[keyof typeof SessionStatus];\n\nexport type SessionOptions = {\n info?: PeerInfo;\n};\n\n/**\n * an OpenLV Session\n *\n * https://openlv.sh/api/session\n */\nexport type Session = {\n status: Observable<SessionStatus>;\n signalStatus: Observable<SignalStatus>;\n error: Observable<string | undefined>;\n peerInfo: Observable<PeerInfo | undefined>;\n getHandshakeParameters(): SessionHandshakeParameters;\n connect(): Promise<void>;\n close(): Promise<void>;\n send(message: object, ackTimeout?: number, responseTimeout?: number): Promise<unknown>;\n emitter: EventEmitter<SessionEvents>;\n _internal: {\n signal: SignalingLayer;\n /** Instantiated when signaling reaches ENCRYPTED and a transport is selected. */\n transport: TransportLayer | undefined;\n };\n};\n\n/**\n * OpenLV Session\n *\n * https://openlv.sh/api/session\n */\nexport const createSession = async (\n initParameters: SessionLinkParameters,\n transportLayers: TransportLayerFunction[],\n onMessage: (message: object) => Promise<object | string>,\n options?: SessionOptions,\n): Promise<Session> => {\n if (transportLayers.length === 0) {\n throw new Error(\"At least one transport is required\");\n }\n\n if (options?.info) {\n const problem = validatePeerInfo(options.info);\n\n if (problem) throw new Error(`Invalid session info: ${problem}`);\n }\n\n const emitter = new EventEmitter<SessionEvents>();\n const messages = new EventEmitter<{ message: SessionMessage; }>();\n const sessionId\n = \"sessionId\" in initParameters\n ? initParameters.sessionId\n : generateSessionId();\n const {\n encryptionKey,\n decryptionKey: { decrypt },\n } = await initEncryptionKeys(initParameters);\n const handshakeKey\n = \"k\" in initParameters\n ? await deriveSymmetricKey(initParameters.k)\n : await generateHandshakeKey();\n const { hash, isHost } = await initHash(\n \"h\" in initParameters ? initParameters.h : undefined,\n encryptionKey,\n );\n const protocol = initParameters.p;\n const server = initParameters.s;\n const capabilities: PeerCapabilities = {\n transports: transportLayers.map(layer => layer.transportId),\n ...(options?.info && { info: options.info }),\n };\n\n const [status, setStatus] = observable<SessionStatus>(SessionStatus.CREATED);\n const [lastError, setLastError] = observable<string | undefined>(undefined);\n const [peerInfo, setPeerInfo] = observable<PeerInfo | undefined>(undefined);\n\n const setLastErrorIfUnset = (reason: string) => {\n if (lastError.get() === undefined) setLastError(reason);\n };\n\n const updateStatus = (newStatus: SessionStatus) => {\n log(\"updateStatus\", newStatus);\n setStatus(newStatus);\n };\n\n const signalLayer = await loadSignaling(protocol);\n const signaling = await signalLayer({\n topic: sessionId,\n url: server,\n });\n const canEncrypt = () => signal.peerKey.get() !== undefined;\n const signal = await signaling({\n h: hash,\n canEncrypt,\n async encrypt(message) {\n const key = signal.peerKey.get();\n\n if (!key) {\n throw new Error(\"Relying party public key not found\");\n }\n\n return await key.encrypt(message);\n },\n decrypt,\n publicKey: encryptionKey,\n k: handshakeKey,\n capabilities,\n isHost,\n });\n\n signal.peerKey.subscribe((key) => {\n const role = isHost ? \"host\" : \"client\";\n\n log(`rpKey discovered by ${role}`, key);\n });\n\n signal.peerCapabilities.subscribe((received) => {\n log(\"peer capabilities received\", received);\n setPeerInfo(received?.info);\n });\n\n let transport: TransportLayer | undefined;\n\n const createTransport = (layer: TransportLayerFunction): TransportLayer => layer.create({\n async encrypt(message) {\n const key = signal.peerKey.get();\n\n if (!key) {\n throw new Error(\"Relying party public key not found\");\n }\n\n return await key.encrypt(message);\n },\n decrypt,\n isHost,\n onmessage: async (message: { type: string; payload: object; messageId: string; }) => {\n log(\"Session: received message from transport\", message);\n\n if (message[\"type\"] === \"request\") {\n const messageId = message[\"messageId\"] as string;\n\n try {\n // Immediately acknowledge receipt so the sender's ack-timeout does\n // not fire while the handler (which may await user interaction) runs.\n await transport?.send({ type: \"ack\", messageId } satisfies SessionMessage);\n\n // Notify observers before processing (e.g. wallet UI can show a\n // pending indicator before the handler resolves).\n emitter.emit(\"request\", message[\"payload\"] as object);\n\n const data = await onMessage(message[\"payload\"] as object)\n // A throwing handler must still answer, otherwise the peer waits\n // out its full response timeout.\n .catch(() => ({ error: { code: -32_603, message: \"Internal error\" } }));\n\n await transport?.send({\n type: \"response\",\n messageId,\n payload: data,\n } satisfies SessionMessage);\n }\n catch (error) {\n log(\"failed to respond to peer request\", error);\n }\n }\n\n if (message[\"type\"] === \"response\" || message[\"type\"] === \"ack\") {\n // Both acks and responses are forwarded to the send() correlator.\n messages.emit(\"message\", message as SessionMessage);\n }\n },\n async subsend(message) {\n log(\"Session: sending trans msg to signal\", message);\n const sessionMessage: SessionMessage = {\n type: \"request\",\n messageId: crypto.randomUUID(),\n payload: message,\n };\n\n await signal.send(sessionMessage);\n },\n });\n\n /**\n * Sessions resumed with a known peer key never exchange capabilities and\n * keep the first configured transport.\n */\n const selectTransportLayer = (): TransportLayerFunction | undefined => {\n const peerCaps = signal.peerCapabilities.get();\n\n if (!peerCaps) return transportLayers[0];\n\n // The host's preferred transport that the client also supports; both\n // peers compute the same result independently -- no confirmation round trip.\n const hostPreference = isHost ? capabilities.transports : peerCaps.transports;\n const clientSupported = new Set(isHost ? peerCaps.transports : capabilities.transports);\n const selected = hostPreference.find(transportId => clientSupported.has(transportId));\n\n return transportLayers.find(layer => layer.transportId === selected);\n };\n\n // Once both peers are present (signaling encrypted) the transport should\n // connect promptly; if it cannot (e.g. no ICE candidates on a restricted\n // network) fail loudly instead of sitting in \"linking\" forever.\n const TRANSPORT_LINK_TIMEOUT_MS = 45_000;\n let linkDeadline: ReturnType<typeof setTimeout> | undefined;\n let unsubscribeTransportStatus: (() => void) | undefined;\n\n const clearLinkDeadline = () => {\n clearTimeout(linkDeadline);\n linkDeadline = undefined;\n };\n\n const onTransportStateChange = (transportStatus: TransportStatus) => {\n log(\"transport state change\", transportStatus);\n\n if (transportStatus === TransportStatus.CONNECTED) {\n clearLinkDeadline();\n setLastError(undefined);\n updateStatus(SessionStatus.CONNECTED);\n }\n\n if (transportStatus === TransportStatus.ERROR) {\n clearLinkDeadline();\n setLastErrorIfUnset(\"Peer-to-peer transport failed\");\n updateStatus(SessionStatus.DISCONNECTED);\n }\n };\n\n const onTransportError = (reason?: string) => {\n if (reason) setLastError(reason);\n };\n\n const startTransport = () => {\n if (!transport) {\n const layer = selectTransportLayer();\n\n if (!layer) {\n log(\"no common transport with peer\", capabilities.transports, signal.peerCapabilities.get()?.transports);\n setLastErrorIfUnset(\"No common transport with peer\");\n updateStatus(SessionStatus.DISCONNECTED);\n\n return;\n }\n\n log(\"selected transport\", layer.transportId);\n transport = createTransport(layer);\n unsubscribeTransportStatus = transport.status.subscribe(onTransportStateChange);\n transport.emitter.on(\"error\", onTransportError);\n }\n\n linkDeadline ??= setTimeout(() => {\n if (status.get() === SessionStatus.CONNECTED) return;\n\n log(\"transport failed to connect in time\");\n setLastErrorIfUnset(\"Timed out establishing the peer-to-peer connection\");\n updateStatus(SessionStatus.DISCONNECTED);\n }, TRANSPORT_LINK_TIMEOUT_MS);\n\n Promise.resolve(transport.setup()).catch((error) => {\n log(\"transport setup failed\", error);\n clearLinkDeadline();\n setLastErrorIfUnset(error instanceof Error ? error.message : \"Transport setup failed\");\n updateStatus(SessionStatus.DISCONNECTED);\n });\n };\n\n const onSignalMessage = async (message: object) => {\n log(\"Session: received message from signaling\", message);\n\n const sessionMessage = message as SessionMessage;\n\n if (sessionMessage.type === \"response\") {\n messages.emit(\"message\", sessionMessage);\n }\n else if (sessionMessage.type === \"request\") {\n if (!transport) {\n log(\"dropping negotiation message: transport not selected yet\");\n\n return;\n }\n\n try {\n await transport.handle(sessionMessage.payload as TransportMessage);\n }\n catch (error) {\n // Negotiation payloads come from the (untrusted) relay; a malformed\n // one must not become an unhandled rejection.\n log(\"failed to handle negotiation message\", error);\n }\n }\n };\n\n const onSignalStateChange = (signalStatus: SignalStatus) => {\n log(\"signal state change\", signalStatus);\n\n if (signalStatus === SignalStatus.READY) {\n updateStatus(SessionStatus.READY);\n }\n\n if (\n (\n [\n SignalStatus.HANDSHAKE,\n SignalStatus.HANDSHAKE_PARTIAL,\n ] as SignalStatus[]\n ).includes(signalStatus)\n ) {\n updateStatus(SessionStatus.LINKING);\n }\n\n if (signalStatus === SignalStatus.ENCRYPTED) {\n startTransport();\n }\n\n if (signalStatus === SignalStatus.ERROR) {\n log(\"signaling error -- marking session disconnected\");\n setLastErrorIfUnset(\"Signaling failed or timed out\");\n updateStatus(SessionStatus.DISCONNECTED);\n }\n };\n\n let unsubscribeSignalStatus: (() => void) | undefined;\n\n return {\n connect: async () => {\n updateStatus(SessionStatus.SIGNALING);\n log(\"connecting to session, isHost:\", isHost);\n\n signal.on(\"message\", onSignalMessage);\n unsubscribeSignalStatus = signal.status.subscribe(onSignalStateChange);\n\n await signal.setup();\n },\n async close() {\n log(\"session teardown\");\n clearLinkDeadline();\n signal.off(\"message\", onSignalMessage);\n unsubscribeSignalStatus?.();\n unsubscribeTransportStatus?.();\n transport?.emitter.off(\"error\", onTransportError);\n await Promise.all([\n transport?.teardown(),\n signal.teardown(),\n ]);\n updateStatus(SessionStatus.DISCONNECTED);\n },\n status,\n signalStatus: signal.status,\n error: lastError,\n peerInfo,\n getHandshakeParameters() {\n return {\n version: OPENLV_PROTOCOL_VERSION,\n sessionId,\n h: hash,\n k: handshakeKey.toString(),\n p: protocol,\n s: server,\n };\n },\n async send(\n message: object,\n ackTimeout: number = 10_000,\n responseTimeout: number = 60 * 60_000,\n ) {\n if (!transport || signal.status.get() !== SignalStatus.ENCRYPTED) {\n throw new Error(\"Session not ready\");\n }\n\n const messageId = crypto.randomUUID();\n const sessionMessage: SessionMessage = {\n type: \"request\",\n messageId,\n payload: message,\n };\n\n await transport.send(sessionMessage);\n\n return awaitCorrelatedResponse(messages, messageId, ackTimeout, responseTimeout);\n },\n _internal: {\n signal,\n get transport() {\n return transport;\n },\n },\n emitter,\n };\n};\n\n/**\n * Connect to a session from its openlv:// URL\n */\nexport const connectSession = async (\n connectionUrl: string,\n onMessage: (message: object) => Promise<object | string>,\n transports: TransportLayerFunction[],\n options?: SessionOptions,\n): Promise<Session> => {\n const initParameters = decodeConnectionURL(connectionUrl);\n\n return createSession(initParameters, transports, onMessage, options);\n};\n"]}
@@ -1,20 +1,19 @@
1
1
  import { decodeConnectionURL, encodeConnectionURL } from "@openlv/core";
2
- import { ntfy } from "@openlv/signaling/ntfy";
3
2
  import { webrtc } from "@openlv/transport/webrtc";
4
3
  import { describe, expect, test } from "vitest";
5
- import { connectSession, createSession } from "./index.js";
4
+ import { connectSession, createSession, SessionStatus } from "./index.js";
6
5
  describe("Session", () => {
7
6
  test("Should be able to create a session", async () => {
8
7
  const sessionA = await createSession({
9
8
  sessionId: "mytestsession111",
10
9
  p: "ntfy",
11
10
  s: "https://ntfy.sh/",
12
- }, ntfy, [webrtc()], async (message) => {
11
+ }, [webrtc()], async (message) => {
13
12
  console.log("sessionA received message", message);
14
13
  return { result: "success" };
15
14
  });
16
15
  expect(sessionA).toBeDefined();
17
- console.log(sessionA.getState());
16
+ console.log(sessionA.status.get());
18
17
  await sessionA.connect();
19
18
  const handshakeParametersA = sessionA.getHandshakeParameters();
20
19
  const encodedUrl = encodeConnectionURL(handshakeParametersA);
@@ -28,12 +27,12 @@ describe("Session", () => {
28
27
  console.log("sessionB received message", message);
29
28
  return { result: "success" };
30
29
  }, [webrtc()]);
31
- console.log(sessionB.getState());
30
+ console.log(sessionB.status.get());
32
31
  await sessionB.connect();
33
- await Promise.all([sessionA.waitForLink(), sessionB.waitForLink()]);
32
+ await Promise.all([sessionA, sessionB].map(session => session.status.until(current => current === SessionStatus.CONNECTED)));
34
33
  //
35
- console.log("A", sessionA.getState());
36
- console.log("B", sessionB.getState());
34
+ console.log("A", sessionA.status.get());
35
+ console.log("B", sessionB.status.get());
37
36
  const response = await sessionA.send({ data: "test" });
38
37
  console.log("response", response);
39
38
  expect(response).toEqual({ result: "success" });
@@ -1 +1 @@
1
- {"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACxE,OAAO,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAClD,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAEhD,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3D,QAAQ,CAAC,SAAS,EAAE,GAAG,EAAE;IACvB,IAAI,CAAC,oCAAoC,EAAE,KAAK,IAAI,EAAE;QACpD,MAAM,QAAQ,GAAG,MAAM,aAAa,CAClC;YACE,SAAS,EAAE,kBAAkB;YAC7B,CAAC,EAAE,MAAM;YACT,CAAC,EAAE,kBAAkB;SACtB,EACD,IAAI,EACJ,CAAC,MAAM,EAAE,CAAC,EACV,KAAK,EAAE,OAAO,EAAE,EAAE;YAChB,OAAO,CAAC,GAAG,CAAC,2BAA2B,EAAE,OAAO,CAAC,CAAC;YAElD,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;QAC/B,CAAC,CACF,CAAC;QAEF,MAAM,CAAC,QAAQ,CAAC,CAAC,WAAW,EAAE,CAAC;QAC/B,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC;QAEjC,MAAM,QAAQ,CAAC,OAAO,EAAE,CAAC;QAEzB,MAAM,oBAAoB,GAAG,QAAQ,CAAC,sBAAsB,EAAE,CAAC;QAC/D,MAAM,UAAU,GAAG,mBAAmB,CAAC,oBAAoB,CAAC,CAAC;QAE7D,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QAExB,MAAM,UAAU,GAAG,mBAAmB,CAAC,UAAU,CAAC,CAAC;QAEnD,MAAM,CAAC,UAAU,CAAC,CAAC,OAAO,CAAC,oBAAoB,CAAC,CAAC;QAEjD,EAAE;QACF,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC;QAExD,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC;QAEvC,MAAM,QAAQ,GAAG,MAAM,cAAc,CACnC,UAAU,EACV,KAAK,EAAE,OAAO,EAAE,EAAE;YAChB,OAAO,CAAC,GAAG,CAAC,2BAA2B,EAAE,OAAO,CAAC,CAAC;YAElD,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;QAC/B,CAAC,EACD,CAAC,MAAM,EAAE,CAAC,CACX,CAAC;QAEF,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC;QACjC,MAAM,QAAQ,CAAC,OAAO,EAAE,CAAC;QAEzB,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,WAAW,EAAE,EAAE,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;QAEpE,EAAE;QACF,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC;QACtC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC;QAEtC,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;QAEvD,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;QAElC,MAAM,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import { decodeConnectionURL, encodeConnectionURL } from \"@openlv/core\";\nimport { ntfy } from \"@openlv/signaling/ntfy\";\nimport { webrtc } from \"@openlv/transport/webrtc\";\nimport { describe, expect, test } from \"vitest\";\n\nimport { connectSession, createSession } from \"./index.js\";\n\ndescribe(\"Session\", () => {\n test(\"Should be able to create a session\", async () => {\n const sessionA = await createSession(\n {\n sessionId: \"mytestsession111\",\n p: \"ntfy\",\n s: \"https://ntfy.sh/\",\n },\n ntfy,\n [webrtc()],\n async (message) => {\n console.log(\"sessionA received message\", message);\n\n return { result: \"success\" };\n },\n );\n\n expect(sessionA).toBeDefined();\n console.log(sessionA.getState());\n\n await sessionA.connect();\n\n const handshakeParametersA = sessionA.getHandshakeParameters();\n const encodedUrl = encodeConnectionURL(handshakeParametersA);\n\n console.log(encodedUrl);\n\n const decodedUrl = decodeConnectionURL(encodedUrl);\n\n expect(decodedUrl).toEqual(handshakeParametersA);\n\n //\n await new Promise(resolve => setTimeout(resolve, 1000));\n\n console.log(\"Connecting to session B\");\n\n const sessionB = await connectSession(\n encodedUrl,\n async (message) => {\n console.log(\"sessionB received message\", message);\n\n return { result: \"success\" };\n },\n [webrtc()],\n );\n\n console.log(sessionB.getState());\n await sessionB.connect();\n\n await Promise.all([sessionA.waitForLink(), sessionB.waitForLink()]);\n\n //\n console.log(\"A\", sessionA.getState());\n console.log(\"B\", sessionB.getState());\n\n const response = await sessionA.send({ data: \"test\" });\n\n console.log(\"response\", response);\n\n expect(response).toEqual({ result: \"success\" });\n });\n});\n"]}
1
+ {"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { decodeConnectionURL, encodeConnectionURL } from \"@openlv/core\";\nimport { webrtc } from \"@openlv/transport/webrtc\";\nimport { describe, expect, test } from \"vitest\";\n\nimport { connectSession, createSession, SessionStatus } from \"./index.js\";\n\ndescribe(\"Session\", () => {\n test(\"Should be able to create a session\", async () => {\n const sessionA = await createSession(\n {\n sessionId: \"mytestsession111\",\n p: \"ntfy\",\n s: \"https://ntfy.sh/\",\n },\n [webrtc()],\n async (message) => {\n console.log(\"sessionA received message\", message);\n\n return { result: \"success\" };\n },\n );\n\n expect(sessionA).toBeDefined();\n console.log(sessionA.status.get());\n\n await sessionA.connect();\n\n const handshakeParametersA = sessionA.getHandshakeParameters();\n const encodedUrl = encodeConnectionURL(handshakeParametersA);\n\n console.log(encodedUrl);\n\n const decodedUrl = decodeConnectionURL(encodedUrl);\n\n expect(decodedUrl).toEqual(handshakeParametersA);\n\n //\n await new Promise(resolve => setTimeout(resolve, 1000));\n\n console.log(\"Connecting to session B\");\n\n const sessionB = await connectSession(\n encodedUrl,\n async (message) => {\n console.log(\"sessionB received message\", message);\n\n return { result: \"success\" };\n },\n [webrtc()],\n );\n\n console.log(sessionB.status.get());\n await sessionB.connect();\n\n await Promise.all([sessionA, sessionB].map(\n session => session.status.until(current => current === SessionStatus.CONNECTED),\n ));\n\n //\n console.log(\"A\", sessionA.status.get());\n console.log(\"B\", sessionB.status.get());\n\n const response = await sessionA.send({ data: \"test\" });\n\n console.log(\"response\", response);\n\n expect(response).toEqual({ result: \"success\" });\n });\n});\n"]}
@@ -5,7 +5,7 @@ import type { SessionMessage } from "./index.js";
5
5
  *
6
6
  * Two-phase timeout: the peer must ack within `ackTimeoutMs`, confirming it
7
7
  * received the message; after the ack the wait extends to `responseTimeoutMs`
8
- * — enough for user-interactive flows such as `eth_sendTransaction`.
8
+ * -- enough for user-interactive flows such as `eth_sendTransaction`.
9
9
  */
10
10
  export declare const awaitCorrelatedResponse: (messages: EventEmitter<{
11
11
  message: SessionMessage;
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Two-phase timeout: the peer must ack within `ackTimeoutMs`, confirming it
5
5
  * received the message; after the ack the wait extends to `responseTimeoutMs`
6
- * — enough for user-interactive flows such as `eth_sendTransaction`.
6
+ * -- enough for user-interactive flows such as `eth_sendTransaction`.
7
7
  */
8
8
  export const awaitCorrelatedResponse = (messages, messageId, ackTimeoutMs, responseTimeoutMs) => new Promise((resolve, reject) => {
9
9
  let isAckReceived = false;
@@ -34,7 +34,7 @@ export const awaitCorrelatedResponse = (messages, messageId, ackTimeoutMs, respo
34
34
  }
35
35
  };
36
36
  messages.on("message", handler);
37
- // Short window for the ack — tells us the peer is alive and processing.
37
+ // Short window for the ack -- tells us the peer is alive and processing.
38
38
  ackTimer = setTimeout(() => {
39
39
  if (isAckReceived) {
40
40
  return;
@@ -1 +1 @@
1
- {"version":3,"file":"correlate.js","sourceRoot":"","sources":["../../src/messages/correlate.ts"],"names":[],"mappings":"AAIA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CACrC,QAAoD,EACpD,SAAiB,EACjB,YAAoB,EACpB,iBAAyB,EACP,EAAE,CAAC,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;IACrD,IAAI,aAAa,GAAG,KAAK,CAAC;IAC1B,wCAAwC;IACxC,IAAI,QAAmD,CAAC;IACxD,IAAI,aAAwD,CAAC;IAE7D,MAAM,OAAO,GAAG,GAAG,EAAE;QACnB,YAAY,CAAC,QAAQ,CAAC,CAAC;QACvB,YAAY,CAAC,aAAa,CAAC,CAAC;QAC5B,QAAQ,CAAC,GAAG,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IACnC,CAAC,CAAC;IAEF,MAAM,OAAO,GAAG,CAAC,OAAuB,EAAE,EAAE;QAC1C,IAAI,OAAO,CAAC,SAAS,KAAK,SAAS;YAAE,OAAO;QAE5C,IAAI,OAAO,CAAC,IAAI,KAAK,KAAK,IAAI,CAAC,aAAa,EAAE,CAAC;YAC7C,aAAa,GAAG,IAAI,CAAC;YACrB,YAAY,CAAC,QAAQ,CAAC,CAAC;YACvB,gEAAgE;YAChE,aAAa,GAAG,UAAU,CAAC,GAAG,EAAE;gBAC9B,OAAO,EAAE,CAAC;gBACV,MAAM,CAAC,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAC,CAAC;YAC5E,CAAC,EAAE,iBAAiB,CAAC,CAAC;YAEtB,OAAO;QACT,CAAC;QAED,IAAI,OAAO,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;YAChC,OAAO,EAAE,CAAC;YACV,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAC3B,CAAC;IACH,CAAC,CAAC;IAEF,QAAQ,CAAC,EAAE,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IAEhC,wEAAwE;IACxE,QAAQ,GAAG,UAAU,CAAC,GAAG,EAAE;QACzB,IAAI,aAAa,EAAE,CAAC;YAClB,OAAO;QACT,CAAC;QAED,OAAO,EAAE,CAAC;QACV,MAAM,CAAC,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAC,CAAC;IAC1E,CAAC,EAAE,YAAY,CAAC,CAAC;AACnB,CAAC,CAAC,CAAC","sourcesContent":["import type { EventEmitter } from \"eventemitter3\";\n\nimport type { SessionMessage } from \"./index.js\";\n\n/**\n * Await the ack and response correlated to a sent request.\n *\n * Two-phase timeout: the peer must ack within `ackTimeoutMs`, confirming it\n * received the message; after the ack the wait extends to `responseTimeoutMs`\n * — enough for user-interactive flows such as `eth_sendTransaction`.\n */\nexport const awaitCorrelatedResponse = (\n messages: EventEmitter<{ message: SessionMessage; }>,\n messageId: string,\n ackTimeoutMs: number,\n responseTimeoutMs: number,\n): Promise<unknown> => new Promise((resolve, reject) => {\n let isAckReceived = false;\n // eslint-disable-next-line prefer-const\n let ackTimer: ReturnType<typeof setTimeout> | undefined;\n let responseTimer: ReturnType<typeof setTimeout> | undefined;\n\n const cleanup = () => {\n clearTimeout(ackTimer);\n clearTimeout(responseTimer);\n messages.off(\"message\", handler);\n };\n\n const handler = (message: SessionMessage) => {\n if (message.messageId !== messageId) return;\n\n if (message.type === \"ack\" && !isAckReceived) {\n isAckReceived = true;\n clearTimeout(ackTimer);\n // The other side confirmed receipt; wait for the full response.\n responseTimer = setTimeout(() => {\n cleanup();\n reject(new Error(\"Request timed out: no response after acknowledgement\"));\n }, responseTimeoutMs);\n\n return;\n }\n\n if (message.type === \"response\") {\n cleanup();\n resolve(message.payload);\n }\n };\n\n messages.on(\"message\", handler);\n\n // Short window for the ack — tells us the peer is alive and processing.\n ackTimer = setTimeout(() => {\n if (isAckReceived) {\n return;\n }\n\n cleanup();\n reject(new Error(\"Request timed out: remote peer did not acknowledge\"));\n }, ackTimeoutMs);\n});\n"]}
1
+ {"version":3,"file":"correlate.js","sourceRoot":"","sources":["../../src/messages/correlate.ts"],"names":[],"mappings":"AAIA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CACrC,QAAoD,EACpD,SAAiB,EACjB,YAAoB,EACpB,iBAAyB,EACP,EAAE,CAAC,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;IACrD,IAAI,aAAa,GAAG,KAAK,CAAC;IAC1B,wCAAwC;IACxC,IAAI,QAAmD,CAAC;IACxD,IAAI,aAAwD,CAAC;IAE7D,MAAM,OAAO,GAAG,GAAG,EAAE;QACnB,YAAY,CAAC,QAAQ,CAAC,CAAC;QACvB,YAAY,CAAC,aAAa,CAAC,CAAC;QAC5B,QAAQ,CAAC,GAAG,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IACnC,CAAC,CAAC;IAEF,MAAM,OAAO,GAAG,CAAC,OAAuB,EAAE,EAAE;QAC1C,IAAI,OAAO,CAAC,SAAS,KAAK,SAAS;YAAE,OAAO;QAE5C,IAAI,OAAO,CAAC,IAAI,KAAK,KAAK,IAAI,CAAC,aAAa,EAAE,CAAC;YAC7C,aAAa,GAAG,IAAI,CAAC;YACrB,YAAY,CAAC,QAAQ,CAAC,CAAC;YACvB,gEAAgE;YAChE,aAAa,GAAG,UAAU,CAAC,GAAG,EAAE;gBAC9B,OAAO,EAAE,CAAC;gBACV,MAAM,CAAC,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAC,CAAC;YAC5E,CAAC,EAAE,iBAAiB,CAAC,CAAC;YAEtB,OAAO;QACT,CAAC;QAED,IAAI,OAAO,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;YAChC,OAAO,EAAE,CAAC;YACV,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAC3B,CAAC;IACH,CAAC,CAAC;IAEF,QAAQ,CAAC,EAAE,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IAEhC,yEAAyE;IACzE,QAAQ,GAAG,UAAU,CAAC,GAAG,EAAE;QACzB,IAAI,aAAa,EAAE,CAAC;YAClB,OAAO;QACT,CAAC;QAED,OAAO,EAAE,CAAC;QACV,MAAM,CAAC,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAC,CAAC;IAC1E,CAAC,EAAE,YAAY,CAAC,CAAC;AACnB,CAAC,CAAC,CAAC","sourcesContent":["import type { EventEmitter } from \"eventemitter3\";\n\nimport type { SessionMessage } from \"./index.js\";\n\n/**\n * Await the ack and response correlated to a sent request.\n *\n * Two-phase timeout: the peer must ack within `ackTimeoutMs`, confirming it\n * received the message; after the ack the wait extends to `responseTimeoutMs`\n * -- enough for user-interactive flows such as `eth_sendTransaction`.\n */\nexport const awaitCorrelatedResponse = (\n messages: EventEmitter<{ message: SessionMessage; }>,\n messageId: string,\n ackTimeoutMs: number,\n responseTimeoutMs: number,\n): Promise<unknown> => new Promise((resolve, reject) => {\n let isAckReceived = false;\n // eslint-disable-next-line prefer-const\n let ackTimer: ReturnType<typeof setTimeout> | undefined;\n let responseTimer: ReturnType<typeof setTimeout> | undefined;\n\n const cleanup = () => {\n clearTimeout(ackTimer);\n clearTimeout(responseTimer);\n messages.off(\"message\", handler);\n };\n\n const handler = (message: SessionMessage) => {\n if (message.messageId !== messageId) return;\n\n if (message.type === \"ack\" && !isAckReceived) {\n isAckReceived = true;\n clearTimeout(ackTimer);\n // The other side confirmed receipt; wait for the full response.\n responseTimer = setTimeout(() => {\n cleanup();\n reject(new Error(\"Request timed out: no response after acknowledgement\"));\n }, responseTimeoutMs);\n\n return;\n }\n\n if (message.type === \"response\") {\n cleanup();\n resolve(message.payload);\n }\n };\n\n messages.on(\"message\", handler);\n\n // Short window for the ack -- tells us the peer is alive and processing.\n ackTimer = setTimeout(() => {\n if (isAckReceived) {\n return;\n }\n\n cleanup();\n reject(new Error(\"Request timed out: remote peer did not acknowledge\"));\n }, ackTimeoutMs);\n});\n"]}
@@ -1,25 +1,21 @@
1
1
  import { webrtc } from "@openlv/transport/webrtc";
2
2
  import { describe, expect, it } from "vitest";
3
3
  import { createSession } from "./index.js";
4
- // The guards under test throw before any signaling connection is attempted.
5
- const unreachableSignaling = () => {
6
- throw new Error("signaling should not be reached");
7
- };
8
4
  const linkParameters = { p: "ntfy", s: "https://ntfy.sh/" };
9
5
  const onMessage = async () => ({});
10
6
  describe("createSession input validation", () => {
11
7
  it("rejects an empty transport list", async () => {
12
- await expect(createSession(linkParameters, unreachableSignaling, [], onMessage)).rejects.toThrow("At least one transport is required");
8
+ await expect(createSession(linkParameters, [], onMessage)).rejects.toThrow("At least one transport is required");
13
9
  });
14
10
  it("rejects info a receiver would silently drop", async () => {
15
- await expect(createSession(linkParameters, unreachableSignaling, [webrtc()], onMessage, {
11
+ await expect(createSession(linkParameters, [webrtc()], onMessage, {
16
12
  info: {
17
13
  identity: "com.example.dapp",
18
14
  name: "Example dApp",
19
15
  icon: "x".repeat(10000),
20
16
  },
21
17
  })).rejects.toThrow(/icon/);
22
- await expect(createSession(linkParameters, unreachableSignaling, [webrtc()], onMessage, {
18
+ await expect(createSession(linkParameters, [webrtc()], onMessage, {
23
19
  info: { identity: "", name: "Example dApp" },
24
20
  })).rejects.toThrow(/identity/);
25
21
  });
@@ -1 +1 @@
1
- {"version":3,"file":"options.test.js","sourceRoot":"","sources":["../src/options.test.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAClD,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3C,4EAA4E;AAC5E,MAAM,oBAAoB,GAAsB,GAAG,EAAE;IACnD,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAC;AACrD,CAAC,CAAC;AAEF,MAAM,cAAc,GAAG,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,kBAAkB,EAAE,CAAC;AAC5D,MAAM,SAAS,GAAG,KAAK,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;AAEnC,QAAQ,CAAC,gCAAgC,EAAE,GAAG,EAAE;IAC9C,EAAE,CAAC,iCAAiC,EAAE,KAAK,IAAI,EAAE;QAC/C,MAAM,MAAM,CACV,aAAa,CAAC,cAAc,EAAE,oBAAoB,EAAE,EAAE,EAAE,SAAS,CAAC,CACnE,CAAC,OAAO,CAAC,OAAO,CAAC,oCAAoC,CAAC,CAAC;IAC1D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6CAA6C,EAAE,KAAK,IAAI,EAAE;QAC3D,MAAM,MAAM,CACV,aAAa,CAAC,cAAc,EAAE,oBAAoB,EAAE,CAAC,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE;YACzE,IAAI,EAAE;gBACJ,QAAQ,EAAE,kBAAkB;gBAC5B,IAAI,EAAE,cAAc;gBACpB,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,KAAM,CAAC;aACzB;SACF,CAAC,CACH,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAE1B,MAAM,MAAM,CACV,aAAa,CAAC,cAAc,EAAE,oBAAoB,EAAE,CAAC,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE;YACzE,IAAI,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE;SAC7C,CAAC,CACH,CAAC,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAChC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import type { SignalingProtocol } from \"@openlv/signaling\";\nimport { webrtc } from \"@openlv/transport/webrtc\";\nimport { describe, expect, it } from \"vitest\";\n\nimport { createSession } from \"./index.js\";\n\n// The guards under test throw before any signaling connection is attempted.\nconst unreachableSignaling: SignalingProtocol = () => {\n throw new Error(\"signaling should not be reached\");\n};\n\nconst linkParameters = { p: \"ntfy\", s: \"https://ntfy.sh/\" };\nconst onMessage = async () => ({});\n\ndescribe(\"createSession input validation\", () => {\n it(\"rejects an empty transport list\", async () => {\n await expect(\n createSession(linkParameters, unreachableSignaling, [], onMessage),\n ).rejects.toThrow(\"At least one transport is required\");\n });\n\n it(\"rejects info a receiver would silently drop\", async () => {\n await expect(\n createSession(linkParameters, unreachableSignaling, [webrtc()], onMessage, {\n info: {\n identity: \"com.example.dapp\",\n name: \"Example dApp\",\n icon: \"x\".repeat(10_000),\n },\n }),\n ).rejects.toThrow(/icon/);\n\n await expect(\n createSession(linkParameters, unreachableSignaling, [webrtc()], onMessage, {\n info: { identity: \"\", name: \"Example dApp\" },\n }),\n ).rejects.toThrow(/identity/);\n });\n});\n"]}
1
+ {"version":3,"file":"options.test.js","sourceRoot":"","sources":["../src/options.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAClD,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3C,MAAM,cAAc,GAAG,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,kBAAkB,EAAE,CAAC;AAC5D,MAAM,SAAS,GAAG,KAAK,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;AAEnC,QAAQ,CAAC,gCAAgC,EAAE,GAAG,EAAE;IAC9C,EAAE,CAAC,iCAAiC,EAAE,KAAK,IAAI,EAAE;QAC/C,MAAM,MAAM,CACV,aAAa,CAAC,cAAc,EAAE,EAAE,EAAE,SAAS,CAAC,CAC7C,CAAC,OAAO,CAAC,OAAO,CAAC,oCAAoC,CAAC,CAAC;IAC1D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6CAA6C,EAAE,KAAK,IAAI,EAAE;QAC3D,MAAM,MAAM,CACV,aAAa,CAAC,cAAc,EAAE,CAAC,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE;YACnD,IAAI,EAAE;gBACJ,QAAQ,EAAE,kBAAkB;gBAC5B,IAAI,EAAE,cAAc;gBACpB,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,KAAM,CAAC;aACzB;SACF,CAAC,CACH,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAE1B,MAAM,MAAM,CACV,aAAa,CAAC,cAAc,EAAE,CAAC,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE;YACnD,IAAI,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE;SAC7C,CAAC,CACH,CAAC,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAChC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import { webrtc } from \"@openlv/transport/webrtc\";\nimport { describe, expect, it } from \"vitest\";\n\nimport { createSession } from \"./index.js\";\n\nconst linkParameters = { p: \"ntfy\", s: \"https://ntfy.sh/\" };\nconst onMessage = async () => ({});\n\ndescribe(\"createSession input validation\", () => {\n it(\"rejects an empty transport list\", async () => {\n await expect(\n createSession(linkParameters, [], onMessage),\n ).rejects.toThrow(\"At least one transport is required\");\n });\n\n it(\"rejects info a receiver would silently drop\", async () => {\n await expect(\n createSession(linkParameters, [webrtc()], onMessage, {\n info: {\n identity: \"com.example.dapp\",\n name: \"Example dApp\",\n icon: \"x\".repeat(10_000),\n },\n }),\n ).rejects.toThrow(/icon/);\n\n await expect(\n createSession(linkParameters, [webrtc()], onMessage, {\n info: { identity: \"\", name: \"Example dApp\" },\n }),\n ).rejects.toThrow(/identity/);\n });\n});\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openlv/session",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "keywords": [
5
5
  "open-lavatory"
6
6
  ],
@@ -37,13 +37,13 @@
37
37
  "eventemitter3": "^5.0.1",
38
38
  "globals": "^16.2.0",
39
39
  "ts-pattern": "^5",
40
- "@openlv/core": "0.1.2",
41
- "@openlv/signaling": "0.1.3",
42
- "@openlv/transport": "0.1.2"
40
+ "@openlv/core": "0.2.0",
41
+ "@openlv/signaling": "0.2.0",
42
+ "@openlv/transport": "0.2.0"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@vitest/browser-playwright": "^4.0.8",
46
- "playwright": "1.58.1",
46
+ "playwright": "1.60.0",
47
47
  "typescript": "^5",
48
48
  "vitest": "^4"
49
49
  },
@@ -1,7 +0,0 @@
1
- /**
2
- * Pick the transport once both capability lists are known: the first entry
3
- * in the host's preference list that the client also supports. Both peers
4
- * compute the same result independently — no confirmation round trip.
5
- * Unknown identifiers are skipped, not fatal.
6
- */
7
- export declare const selectTransportId: (isHost: boolean, ownTransports: string[], peerTransports: string[]) => string | undefined;
@@ -1,12 +0,0 @@
1
- /**
2
- * Pick the transport once both capability lists are known: the first entry
3
- * in the host's preference list that the client also supports. Both peers
4
- * compute the same result independently — no confirmation round trip.
5
- * Unknown identifiers are skipped, not fatal.
6
- */
7
- export const selectTransportId = (isHost, ownTransports, peerTransports) => {
8
- const hostPreference = isHost ? ownTransports : peerTransports;
9
- const clientSupported = new Set(isHost ? peerTransports : ownTransports);
10
- return hostPreference.find(transportId => clientSupported.has(transportId));
11
- };
12
- //# sourceMappingURL=transports.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"transports.js","sourceRoot":"","sources":["../src/transports.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAC/B,MAAe,EACf,aAAuB,EACvB,cAAwB,EACJ,EAAE;IACtB,MAAM,cAAc,GAAG,MAAM,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,cAAc,CAAC;IAC/D,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC;IAEzE,OAAO,cAAc,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC,eAAe,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC;AAC9E,CAAC,CAAC","sourcesContent":["/**\n * Pick the transport once both capability lists are known: the first entry\n * in the host's preference list that the client also supports. Both peers\n * compute the same result independently — no confirmation round trip.\n * Unknown identifiers are skipped, not fatal.\n */\nexport const selectTransportId = (\n isHost: boolean,\n ownTransports: string[],\n peerTransports: string[],\n): string | undefined => {\n const hostPreference = isHost ? ownTransports : peerTransports;\n const clientSupported = new Set(isHost ? peerTransports : ownTransports);\n\n return hostPreference.find(transportId => clientSupported.has(transportId));\n};\n"]}
@@ -1 +0,0 @@
1
- export {};
@@ -1,22 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { selectTransportId } from "./transports.js";
3
- describe("selectTransportId", () => {
4
- it("picks the first host preference the client supports", () => {
5
- expect(selectTransportId(true, ["ws", "wrtc"], ["wrtc", "ws"])).toBe("ws");
6
- expect(selectTransportId(true, ["wrtc", "ws"], ["ws"])).toBe("ws");
7
- });
8
- it("computes the same result from both perspectives", () => {
9
- const hostTransports = ["ws", "wrtc"];
10
- const clientTransports = ["wrtc", "ws"];
11
- expect(selectTransportId(true, hostTransports, clientTransports))
12
- .toBe(selectTransportId(false, clientTransports, hostTransports));
13
- });
14
- it("skips identifiers the client has never heard of", () => {
15
- expect(selectTransportId(true, ["quic", "wrtc"], ["wrtc"])).toBe("wrtc");
16
- });
17
- it("returns undefined when there is no common transport", () => {
18
- expect(selectTransportId(true, ["ws"], ["wrtc"])).toBeUndefined();
19
- expect(selectTransportId(true, ["wrtc"], [])).toBeUndefined();
20
- });
21
- });
22
- //# sourceMappingURL=transports.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"transports.test.js","sourceRoot":"","sources":["../src/transports.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEpD,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;IACjC,EAAE,CAAC,qDAAqD,EAAE,GAAG,EAAE;QAC7D,MAAM,CAAC,iBAAiB,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC3E,MAAM,CAAC,iBAAiB,CAAC,IAAI,EAAE,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACrE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iDAAiD,EAAE,GAAG,EAAE;QACzD,MAAM,cAAc,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACtC,MAAM,gBAAgB,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QAExC,MAAM,CAAC,iBAAiB,CAAC,IAAI,EAAE,cAAc,EAAE,gBAAgB,CAAC,CAAC;aAC9D,IAAI,CAAC,iBAAiB,CAAC,KAAK,EAAE,gBAAgB,EAAE,cAAc,CAAC,CAAC,CAAC;IACtE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iDAAiD,EAAE,GAAG,EAAE;QACzD,MAAM,CAAC,iBAAiB,CAAC,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC3E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qDAAqD,EAAE,GAAG,EAAE;QAC7D,MAAM,CAAC,iBAAiB,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,aAAa,EAAE,CAAC;QAClE,MAAM,CAAC,iBAAiB,CAAC,IAAI,EAAE,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,EAAE,CAAC;IAChE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import { describe, expect, it } from \"vitest\";\n\nimport { selectTransportId } from \"./transports.js\";\n\ndescribe(\"selectTransportId\", () => {\n it(\"picks the first host preference the client supports\", () => {\n expect(selectTransportId(true, [\"ws\", \"wrtc\"], [\"wrtc\", \"ws\"])).toBe(\"ws\");\n expect(selectTransportId(true, [\"wrtc\", \"ws\"], [\"ws\"])).toBe(\"ws\");\n });\n\n it(\"computes the same result from both perspectives\", () => {\n const hostTransports = [\"ws\", \"wrtc\"];\n const clientTransports = [\"wrtc\", \"ws\"];\n\n expect(selectTransportId(true, hostTransports, clientTransports))\n .toBe(selectTransportId(false, clientTransports, hostTransports));\n });\n\n it(\"skips identifiers the client has never heard of\", () => {\n expect(selectTransportId(true, [\"quic\", \"wrtc\"], [\"wrtc\"])).toBe(\"wrtc\");\n });\n\n it(\"returns undefined when there is no common transport\", () => {\n expect(selectTransportId(true, [\"ws\"], [\"wrtc\"])).toBeUndefined();\n expect(selectTransportId(true, [\"wrtc\"], [])).toBeUndefined();\n });\n});\n"]}