@openlv/session 0.0.3 → 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.
package/dist/index.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { type SessionHandshakeParameters, type SessionLinkParameters } from "@openlv/core";
2
- import { type SignalingLayer, type SignalingProtocol, type SignalState } from "@openlv/signaling";
2
+ import { type PeerInfo, type SignalingLayer, type SignalingProtocol, type SignalState } from "@openlv/signaling";
3
3
  import { type TransportLayer, type TransportLayerFn } 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";
6
7
  export declare const SESSION_STATE: {
7
8
  readonly CREATED: "created";
8
9
  readonly SIGNALING: "signaling";
@@ -17,6 +18,14 @@ export type SessionStateObject = {
17
18
  signaling?: {
18
19
  state: SignalState;
19
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
+ };
26
+ export type SessionOptions = {
27
+ /** Shared with the peer during the handshake and shown in its UI. */
28
+ info?: PeerInfo;
20
29
  };
21
30
  /**
22
31
  * an OpenLV Session
@@ -43,7 +52,8 @@ export type Session = {
43
52
  emitter: EventEmitter<SessionEvents>;
44
53
  _internal: {
45
54
  signal: SignalingLayer;
46
- transport: TransportLayer;
55
+ /** Instantiated when signaling reaches ENCRYPTED and a transport is selected. */
56
+ transport: TransportLayer | undefined;
47
57
  };
48
58
  };
49
59
  /**
@@ -51,8 +61,8 @@ export type Session = {
51
61
  *
52
62
  * https://openlv.sh/api/session
53
63
  */
54
- export declare const createSession: (initParameters: SessionLinkParameters, signalLayer: SignalingProtocol, transportLayer: TransportLayerFn[], onMessage: (message: object) => Promise<object | string>) => Promise<Session>;
64
+ export declare const createSession: (initParameters: SessionLinkParameters, signalLayer: SignalingProtocol, transportLayers: TransportLayerFn[], onMessage: (message: object) => Promise<object | string>, options?: SessionOptions) => Promise<Session>;
55
65
  /**
56
66
  * Connect to a session from its openlv:// URL
57
67
  */
58
- export declare const connectSession: (connectionUrl: string, onMessage: (message: object) => Promise<object | string>, transport: TransportLayerFn[]) => Promise<Session>;
68
+ export declare const connectSession: (connectionUrl: string, onMessage: (message: object) => Promise<object | string>, transports: TransportLayerFn[], options?: SessionOptions) => Promise<Session>;
package/dist/index.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import { decodeConnectionURL, OPENLV_PROTOCOL_VERSION, } from "@openlv/core";
2
2
  import { deriveSymmetricKey, generateHandshakeKey, generateSessionId, initEncryptionKeys, initHash, parseEncryptionKey, } from "@openlv/core/encryption";
3
- import { SIGNAL_STATE, } from "@openlv/signaling";
3
+ import { SIGNAL_STATE, validatePeerInfo, } from "@openlv/signaling";
4
4
  import { dynamicSignalingLayer } from "@openlv/signaling/dynamic";
5
5
  import { TRANSPORT_STATE, } from "@openlv/transport";
6
6
  import { EventEmitter } from "eventemitter3";
7
+ import { awaitCorrelatedResponse } from "./messages/correlate.js";
8
+ import { selectTransportId } from "./transports.js";
7
9
  import { log } from "./utils/log.js";
8
10
  export const SESSION_STATE = {
9
11
  CREATED: "created",
@@ -18,7 +20,17 @@ export const SESSION_STATE = {
18
20
  *
19
21
  * https://openlv.sh/api/session
20
22
  */
21
- export const createSession = async (initParameters, signalLayer, transportLayer, onMessage) => {
23
+ export const createSession = async (initParameters, signalLayer, transportLayers, onMessage, options) => {
24
+ if (transportLayers.length === 0) {
25
+ throw new Error("At least one transport is required");
26
+ }
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
+ const problem = validatePeerInfo(options.info);
31
+ if (problem)
32
+ throw new Error(`Invalid session info: ${problem}`);
33
+ }
22
34
  const emitter = new EventEmitter();
23
35
  const messages = new EventEmitter();
24
36
  const sessionId = "sessionId" in initParameters
@@ -31,12 +43,23 @@ export const createSession = async (initParameters, signalLayer, transportLayer,
31
43
  : await generateHandshakeKey();
32
44
  const { hash, isHost } = await initHash("h" in initParameters ? initParameters.h : undefined, encryptionKey);
33
45
  let status = SESSION_STATE.CREATED;
46
+ let lastError;
34
47
  const protocol = initParameters.p;
35
48
  const server = initParameters.s;
49
+ const capabilities = {
50
+ transports: transportLayers.map(layer => layer.transportId),
51
+ ...(options?.info ? { info: options.info } : {}),
52
+ };
53
+ let peerCaps;
36
54
  const updateStatus = (newStatus) => {
37
55
  log("updateStatus", newStatus);
38
56
  status = newStatus;
39
- emitter.emit("state_change", { status, signaling: signal.getState() });
57
+ emitter.emit("state_change", {
58
+ status,
59
+ signaling: signal.getState(),
60
+ peerInfo: peerCaps?.info,
61
+ error: lastError,
62
+ });
40
63
  };
41
64
  const signaling = await signalLayer({
42
65
  topic: sessionId,
@@ -51,7 +74,6 @@ export const createSession = async (initParameters, signalLayer, transportLayer,
51
74
  if (!relyingPublicKey) {
52
75
  throw new Error("Relying party public key not found");
53
76
  }
54
- log(`encrypting to ${relyingPublicKey.toString()}`, message);
55
77
  return await relyingPublicKey.encrypt(message);
56
78
  },
57
79
  decrypt,
@@ -62,9 +84,18 @@ export const createSession = async (initParameters, signalLayer, transportLayer,
62
84
  log(`rpKey discovered by ${role}`, rpKey);
63
85
  relyingPublicKey = await parseEncryptionKey(rpKey);
64
86
  },
87
+ 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
+ },
65
95
  isHost,
66
96
  });
67
- const transport = transportLayer[0]({
97
+ let transport;
98
+ const createTransport = (layer) => layer.create({
68
99
  encrypt(message) {
69
100
  if (!relyingPublicKey) {
70
101
  throw new Error("Relying party public key not found");
@@ -77,19 +108,26 @@ export const createSession = async (initParameters, signalLayer, transportLayer,
77
108
  log("Session: received message from transport", message);
78
109
  if (message["type"] === "request") {
79
110
  const messageId = message["messageId"];
80
- // Immediately acknowledge receipt so the sender's ack-timeout does
81
- // not fire while the handler (which may await user interaction) runs.
82
- await transport.send({ type: "ack", messageId });
83
- // Notify observers before processing (e.g. wallet UI can show a
84
- // pending indicator before the handler resolves).
85
- emitter.emit("request", message["payload"]);
86
- const data = await onMessage(message["payload"]);
87
- const response = {
88
- type: "response",
89
- messageId,
90
- payload: data,
91
- };
92
- await transport.send(response);
111
+ try {
112
+ // Immediately acknowledge receipt so the sender's ack-timeout does
113
+ // not fire while the handler (which may await user interaction) runs.
114
+ await transport?.send({ type: "ack", messageId });
115
+ // Notify observers before processing (e.g. wallet UI can show a
116
+ // pending indicator before the handler resolves).
117
+ emitter.emit("request", message["payload"]);
118
+ const data = await onMessage(message["payload"])
119
+ // A throwing handler must still answer, otherwise the peer waits
120
+ // out its full response timeout.
121
+ .catch(() => ({ error: { code: -32603, message: "Internal error" } }));
122
+ await transport?.send({
123
+ type: "response",
124
+ messageId,
125
+ payload: data,
126
+ });
127
+ }
128
+ catch (error) {
129
+ log("failed to respond to peer request", error);
130
+ }
93
131
  }
94
132
  if (message["type"] === "response" || message["type"] === "ack") {
95
133
  // Both acks and responses are forwarded to the send() correlator.
@@ -106,26 +144,109 @@ export const createSession = async (initParameters, signalLayer, transportLayer,
106
144
  await signal.send(sessionMessage);
107
145
  },
108
146
  });
109
- transport.emitter.on("state_change", (state) => {
147
+ /**
148
+ * Sessions resumed with a known peer key never exchange capabilities and
149
+ * keep the first configured transport.
150
+ */
151
+ const selectTransportLayer = () => {
152
+ if (!peerCaps)
153
+ return transportLayers[0];
154
+ const selected = selectTransportId(isHost, capabilities.transports, peerCaps.transports);
155
+ return transportLayers.find(layer => layer.transportId === selected);
156
+ };
157
+ // Once both peers are present (signaling encrypted) the transport should
158
+ // connect promptly; if it cannot (e.g. no ICE candidates on a restricted
159
+ // network) fail loudly instead of sitting in "linking" forever.
160
+ const TRANSPORT_LINK_TIMEOUT_MS = 45000;
161
+ let linkDeadline;
162
+ const clearLinkDeadline = () => {
163
+ clearTimeout(linkDeadline);
164
+ linkDeadline = undefined;
165
+ };
166
+ const onTransportStateChange = (state) => {
110
167
  log("transport state change", state);
111
168
  if (state === TRANSPORT_STATE.CONNECTED) {
169
+ clearLinkDeadline();
170
+ lastError = undefined;
112
171
  updateStatus(SESSION_STATE.CONNECTED);
113
172
  }
114
- });
115
- const startTransport = async () => {
116
- await transport.setup();
173
+ if (state === TRANSPORT_STATE.ERROR) {
174
+ clearLinkDeadline();
175
+ lastError ?? (lastError = "Peer-to-peer transport failed");
176
+ updateStatus(SESSION_STATE.DISCONNECTED);
177
+ }
178
+ };
179
+ const onTransportError = (reason) => {
180
+ if (reason)
181
+ lastError = reason;
182
+ };
183
+ const startTransport = () => {
184
+ if (!transport) {
185
+ const layer = selectTransportLayer();
186
+ 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);
190
+ return;
191
+ }
192
+ log("selected transport", layer.transportId);
193
+ transport = createTransport(layer);
194
+ transport.emitter.on("state_change", onTransportStateChange);
195
+ transport.emitter.on("error", onTransportError);
196
+ }
197
+ linkDeadline ?? (linkDeadline = setTimeout(() => {
198
+ if (status === SESSION_STATE.CONNECTED)
199
+ return;
200
+ log("transport failed to connect in time");
201
+ lastError ?? (lastError = "Timed out establishing the peer-to-peer connection");
202
+ updateStatus(SESSION_STATE.DISCONNECTED);
203
+ }, TRANSPORT_LINK_TIMEOUT_MS));
204
+ Promise.resolve(transport.setup()).catch((error) => {
205
+ log("transport setup failed", error);
206
+ clearLinkDeadline();
207
+ lastError ?? (lastError = error instanceof Error ? error.message : "Transport setup failed");
208
+ updateStatus(SESSION_STATE.DISCONNECTED);
209
+ });
117
210
  };
118
- // let transport: TransportLayer | undefined;
119
211
  const onSignalMessage = async (message) => {
120
212
  log("Session: received message from signaling", message);
121
213
  const sessionMsg = message;
122
214
  if (sessionMsg.type === "response") {
123
- log("Session: received data message", sessionMsg.payload);
124
215
  messages.emit("message", sessionMsg);
125
216
  }
126
217
  if (sessionMsg.type === "request") {
127
- log("Session: received request message", sessionMsg.payload);
128
- await transport.handle(sessionMsg.payload);
218
+ if (!transport) {
219
+ log("dropping negotiation message: transport not selected yet");
220
+ return;
221
+ }
222
+ try {
223
+ await transport.handle(sessionMsg.payload);
224
+ }
225
+ catch (error) {
226
+ // Negotiation payloads come from the (untrusted) relay; a malformed
227
+ // one must not become an unhandled rejection.
228
+ log("failed to handle negotiation message", error);
229
+ }
230
+ }
231
+ };
232
+ const onSignalStateChange = (state) => {
233
+ log("signal state change", state);
234
+ if (state === SIGNAL_STATE.READY) {
235
+ updateStatus(SESSION_STATE.READY);
236
+ }
237
+ if ([
238
+ SIGNAL_STATE.HANDSHAKE,
239
+ SIGNAL_STATE.HANDSHAKE_PARTIAL,
240
+ ].includes(state)) {
241
+ updateStatus(SESSION_STATE.LINKING);
242
+ }
243
+ if (state === SIGNAL_STATE.ENCRYPTED) {
244
+ startTransport();
245
+ }
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);
129
250
  }
130
251
  };
131
252
  return {
@@ -133,32 +254,18 @@ export const createSession = async (initParameters, signalLayer, transportLayer,
133
254
  updateStatus(SESSION_STATE.SIGNALING);
134
255
  log("connecting to session, isHost:", isHost);
135
256
  signal.on("message", onSignalMessage);
136
- signal.on("state_change", (state) => {
137
- log("signal state change", state);
138
- if (state === SIGNAL_STATE.READY) {
139
- updateStatus(SESSION_STATE.READY);
140
- }
141
- if ([
142
- SIGNAL_STATE.HANDSHAKE,
143
- SIGNAL_STATE.HANDSHAKE_PARTIAL,
144
- ].includes(state)) {
145
- updateStatus(SESSION_STATE.LINKING);
146
- }
147
- if (state === SIGNAL_STATE.ENCRYPTED) {
148
- startTransport();
149
- }
150
- if (state === SIGNAL_STATE.ERROR) {
151
- log("signaling error — marking session disconnected");
152
- updateStatus(SESSION_STATE.DISCONNECTED);
153
- }
154
- });
257
+ signal.on("state_change", onSignalStateChange);
155
258
  await signal.setup();
156
259
  },
157
260
  async close() {
158
261
  log("session teardown");
262
+ clearLinkDeadline();
159
263
  signal.off("message", onSignalMessage);
264
+ signal.off("state_change", onSignalStateChange);
265
+ transport?.emitter.off("state_change", onTransportStateChange);
266
+ transport?.emitter.off("error", onTransportError);
160
267
  await Promise.all([
161
- transport.teardown(),
268
+ transport?.teardown(),
162
269
  signal.teardown(),
163
270
  ]);
164
271
  updateStatus(SESSION_STATE.DISCONNECTED);
@@ -167,7 +274,8 @@ export const createSession = async (initParameters, signalLayer, transportLayer,
167
274
  return {
168
275
  status,
169
276
  signaling: signal.getState(),
170
- // transport: transport.getState(),
277
+ peerInfo: peerCaps?.info,
278
+ error: lastError,
171
279
  };
172
280
  },
173
281
  getHandshakeParameters() {
@@ -180,28 +288,24 @@ export const createSession = async (initParameters, signalLayer, transportLayer,
180
288
  s: server,
181
289
  };
182
290
  },
183
- waitForLink: async () => {
184
- if (status === SESSION_STATE.CONNECTED)
185
- return;
186
- if (status === SESSION_STATE.DISCONNECTED) {
187
- throw new Error("Session failed to connect");
188
- }
189
- return new Promise((resolve, reject) => {
190
- const handler = (state) => {
191
- if (state?.status === SESSION_STATE.CONNECTED) {
192
- emitter.off("state_change", handler);
193
- resolve();
194
- }
195
- else if (state?.status === SESSION_STATE.DISCONNECTED) {
196
- emitter.off("state_change", handler);
197
- reject(new Error("Session failed to connect"));
198
- }
199
- };
200
- emitter.on("state_change", handler);
201
- });
202
- },
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
+ }),
203
307
  async send(message, ackTimeout = 10000, responseTimeout = 60 * 60000) {
204
- if (signal.getState().state !== SIGNAL_STATE.ENCRYPTED) {
308
+ if (!transport || signal.getState().state !== SIGNAL_STATE.ENCRYPTED) {
205
309
  throw new Error("Session not ready");
206
310
  }
207
311
  const messageId = crypto.randomUUID();
@@ -211,47 +315,13 @@ export const createSession = async (initParameters, signalLayer, transportLayer,
211
315
  payload: message,
212
316
  };
213
317
  await transport.send(sessionMessage);
214
- return new Promise((resolve, reject) => {
215
- let ackReceived = false;
216
- // eslint-disable-next-line prefer-const
217
- let ackTimer;
218
- let responseTimer;
219
- const cleanup = () => {
220
- clearTimeout(ackTimer);
221
- clearTimeout(responseTimer);
222
- messages.off("message", handler);
223
- };
224
- const handler = (msg) => {
225
- if (msg.messageId !== messageId)
226
- return;
227
- if (msg.type === "ack" && !ackReceived) {
228
- ackReceived = true;
229
- clearTimeout(ackTimer);
230
- // The other side confirmed receipt; wait for the full response.
231
- responseTimer = setTimeout(() => {
232
- cleanup();
233
- reject(new Error("Request timed out: no response after acknowledgement"));
234
- }, responseTimeout);
235
- return;
236
- }
237
- if (msg.type === "response") {
238
- cleanup();
239
- resolve(msg.payload);
240
- }
241
- };
242
- messages.on("message", handler);
243
- // Short window for the ack — tells us the peer is alive and processing.
244
- ackTimer = setTimeout(() => {
245
- if (!ackReceived) {
246
- cleanup();
247
- reject(new Error("Request timed out: remote peer did not acknowledge"));
248
- }
249
- }, ackTimeout);
250
- });
318
+ return awaitCorrelatedResponse(messages, messageId, ackTimeout, responseTimeout);
251
319
  },
252
320
  _internal: {
253
321
  signal,
254
- transport,
322
+ get transport() {
323
+ return transport;
324
+ },
255
325
  },
256
326
  emitter,
257
327
  };
@@ -259,12 +329,12 @@ export const createSession = async (initParameters, signalLayer, transportLayer,
259
329
  /**
260
330
  * Connect to a session from its openlv:// URL
261
331
  */
262
- export const connectSession = async (connectionUrl, onMessage, transport) => {
332
+ export const connectSession = async (connectionUrl, onMessage, transports, options) => {
263
333
  const initParameters = decodeConnectionURL(connectionUrl);
264
334
  const signaling = await dynamicSignalingLayer(initParameters.p);
265
335
  if (!signaling) {
266
336
  throw new Error(`Invalid signaling protocol: ${initParameters.p}`);
267
337
  }
268
- return createSession(initParameters, signaling, transport, onMessage);
338
+ return createSession(initParameters, signaling, transports, onMessage, options);
269
339
  };
270
340
  //# 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 SIGNAL_STATE,\n type SignalingLayer,\n type SignalingProtocol,\n type SignalState,\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 type { SessionMessage } from \"./messages/index.js\";\nimport { log } from \"./utils/log.js\";\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};\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 transport: TransportLayer;\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 transportLayer: TransportLayerFn[],\n onMessage: (message: object) => Promise<object | string>,\n): Promise<Session> => {\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 const protocol = initParameters.p;\n const server = initParameters.s;\n\n const updateStatus = (newStatus: SessionState) => {\n log(\"updateStatus\", newStatus);\n status = newStatus;\n emitter.emit(\"state_change\", { status, signaling: signal.getState() });\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 log(`encrypting to ${relyingPublicKey.toString()}`, message);\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 isHost,\n });\n\n const transport = transportLayer[0]({\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 // 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 const response: SessionMessage = {\n type: \"response\",\n messageId,\n payload: data,\n };\n\n await transport.send(response);\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 transport.emitter.on(\"state_change\", (state) => {\n log(\"transport state change\", state);\n\n if (state === TRANSPORT_STATE.CONNECTED) {\n updateStatus(SESSION_STATE.CONNECTED);\n }\n });\n\n const startTransport = async () => {\n await transport.setup();\n };\n\n // let transport: TransportLayer | undefined;\n const onSignalMessage = async (message: object) => {\n log(\"Session: received message from signaling\", message);\n\n const sessionMsg = message as SessionMessage;\n\n if (sessionMsg.type === \"response\") {\n log(\"Session: received data message\", sessionMsg.payload);\n messages.emit(\"message\", sessionMsg);\n }\n\n if (sessionMsg.type === \"request\") {\n log(\"Session: received request message\", sessionMsg.payload);\n\n await transport.handle(sessionMsg.payload as TransportMessage);\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\n signal.on(\"state_change\", (state) => {\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 updateStatus(SESSION_STATE.DISCONNECTED);\n }\n });\n\n await signal.setup();\n },\n async close() {\n log(\"session teardown\");\n signal.off(\"message\", onSignalMessage);\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 // transport: transport.getState(),\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 () => {\n if (status === SESSION_STATE.CONNECTED) return;\n\n if (status === SESSION_STATE.DISCONNECTED) {\n throw new Error(\"Session failed to connect\");\n }\n\n return 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(\"Session failed to connect\"));\n }\n };\n\n emitter.on(\"state_change\", handler);\n });\n },\n async send(\n message: object,\n ackTimeout: number = 10_000,\n responseTimeout: number = 60 * 60_000,\n ) {\n if (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 new Promise<unknown>((resolve, reject) => {\n let ackReceived = 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 = (msg: SessionMessage) => {\n if (msg.messageId !== messageId) return;\n\n if (msg.type === \"ack\" && !ackReceived) {\n ackReceived = 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 }, responseTimeout);\n\n return;\n }\n\n if (msg.type === \"response\") {\n cleanup();\n resolve(msg.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 (!ackReceived) {\n cleanup();\n reject(new Error(\"Request timed out: remote peer did not acknowledge\"));\n }\n }, ackTimeout);\n });\n },\n _internal: {\n signal,\n transport,\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 transport: TransportLayerFn[],\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, transport, onMessage);\n};\n"]}
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 sessionMsg = message as SessionMessage;\n\n if (sessionMsg.type === \"response\") {\n messages.emit(\"message\", sessionMsg);\n }\n\n if (sessionMsg.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(sessionMsg.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"]}
@@ -0,0 +1,12 @@
1
+ import type { EventEmitter } from "eventemitter3";
2
+ import type { SessionMessage } from "./index.js";
3
+ /**
4
+ * Await the ack and response correlated to a sent request.
5
+ *
6
+ * Two-phase timeout: the peer must ack within `ackTimeoutMs`, confirming it
7
+ * received the message; after the ack the wait extends to `responseTimeoutMs`
8
+ * — enough for user-interactive flows such as `eth_sendTransaction`.
9
+ */
10
+ export declare const awaitCorrelatedResponse: (messages: EventEmitter<{
11
+ message: SessionMessage;
12
+ }>, messageId: string, ackTimeoutMs: number, responseTimeoutMs: number) => Promise<unknown>;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Await the ack and response correlated to a sent request.
3
+ *
4
+ * Two-phase timeout: the peer must ack within `ackTimeoutMs`, confirming it
5
+ * received the message; after the ack the wait extends to `responseTimeoutMs`
6
+ * — enough for user-interactive flows such as `eth_sendTransaction`.
7
+ */
8
+ export const awaitCorrelatedResponse = (messages, messageId, ackTimeoutMs, responseTimeoutMs) => new Promise((resolve, reject) => {
9
+ let ackReceived = false;
10
+ // eslint-disable-next-line prefer-const
11
+ let ackTimer;
12
+ let responseTimer;
13
+ const cleanup = () => {
14
+ clearTimeout(ackTimer);
15
+ clearTimeout(responseTimer);
16
+ messages.off("message", handler);
17
+ };
18
+ const handler = (msg) => {
19
+ if (msg.messageId !== messageId)
20
+ return;
21
+ if (msg.type === "ack" && !ackReceived) {
22
+ ackReceived = true;
23
+ clearTimeout(ackTimer);
24
+ // The other side confirmed receipt; wait for the full response.
25
+ responseTimer = setTimeout(() => {
26
+ cleanup();
27
+ reject(new Error("Request timed out: no response after acknowledgement"));
28
+ }, responseTimeoutMs);
29
+ return;
30
+ }
31
+ if (msg.type === "response") {
32
+ cleanup();
33
+ resolve(msg.payload);
34
+ }
35
+ };
36
+ messages.on("message", handler);
37
+ // Short window for the ack — tells us the peer is alive and processing.
38
+ ackTimer = setTimeout(() => {
39
+ if (!ackReceived) {
40
+ cleanup();
41
+ reject(new Error("Request timed out: remote peer did not acknowledge"));
42
+ }
43
+ }, ackTimeoutMs);
44
+ });
45
+ //# sourceMappingURL=correlate.js.map
@@ -0,0 +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,WAAW,GAAG,KAAK,CAAC;IACxB,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,GAAmB,EAAE,EAAE;QACtC,IAAI,GAAG,CAAC,SAAS,KAAK,SAAS;YAAE,OAAO;QAExC,IAAI,GAAG,CAAC,IAAI,KAAK,KAAK,IAAI,CAAC,WAAW,EAAE,CAAC;YACvC,WAAW,GAAG,IAAI,CAAC;YACnB,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,GAAG,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;YAC5B,OAAO,EAAE,CAAC;YACV,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACvB,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,CAAC,WAAW,EAAE,CAAC;YACjB,OAAO,EAAE,CAAC;YACV,MAAM,CAAC,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAC,CAAC;QAC1E,CAAC;IACH,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 ackReceived = 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 = (msg: SessionMessage) => {\n if (msg.messageId !== messageId) return;\n\n if (msg.type === \"ack\" && !ackReceived) {\n ackReceived = 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 (msg.type === \"response\") {\n cleanup();\n resolve(msg.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 (!ackReceived) {\n cleanup();\n reject(new Error(\"Request timed out: remote peer did not acknowledge\"));\n }\n }, ackTimeoutMs);\n});\n"]}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ import { webrtc } from "@openlv/transport/webrtc";
2
+ import { describe, expect, it } from "vitest";
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
+ const linkParameters = { p: "ntfy", s: "https://ntfy.sh/" };
9
+ const onMessage = async () => ({});
10
+ describe("createSession input validation", () => {
11
+ it("rejects an empty transport list", async () => {
12
+ await expect(createSession(linkParameters, unreachableSignaling, [], onMessage)).rejects.toThrow("At least one transport is required");
13
+ });
14
+ it("rejects info a receiver would silently drop", async () => {
15
+ await expect(createSession(linkParameters, unreachableSignaling, [webrtc()], onMessage, {
16
+ info: {
17
+ identity: "com.example.dapp",
18
+ name: "Example dApp",
19
+ icon: "x".repeat(10000),
20
+ },
21
+ })).rejects.toThrow(/icon/);
22
+ await expect(createSession(linkParameters, unreachableSignaling, [webrtc()], onMessage, {
23
+ info: { identity: "", name: "Example dApp" },
24
+ })).rejects.toThrow(/identity/);
25
+ });
26
+ });
27
+ //# sourceMappingURL=options.test.js.map
@@ -0,0 +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"]}
@@ -0,0 +1,7 @@
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;
@@ -0,0 +1,12 @@
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
@@ -0,0 +1 @@
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"]}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,22 @@
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
@@ -0,0 +1 @@
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"]}
@@ -1 +1 @@
1
- export declare const log: (...x: Parameters<typeof console.log>) => void;
1
+ export declare const log: (...args: Parameters<typeof console.log>) => void;
package/dist/utils/log.js CHANGED
@@ -1,5 +1,3 @@
1
- export const log = (...x) => console.log("[session]",
2
- // "color: orange; font-weight: bold",
3
- // "color: inherit; font-weight: normal",
4
- ...x);
1
+ import { createLogger } from "@openlv/core";
2
+ export const log = createLogger("session", "mediumpurple");
5
3
  //# sourceMappingURL=log.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"log.js","sourceRoot":"","sources":["../../src/utils/log.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,GAAG,GAAG,CAAC,GAAG,CAAiC,EAAE,EAAE,CAC1D,OAAO,CAAC,GAAG,CACT,WAAW;AACX,sCAAsC;AACtC,yCAAyC;AACzC,GAAG,CAAC,CACL,CAAC","sourcesContent":["export const log = (...x: Parameters<typeof console.log>) =>\n console.log(\n \"[session]\",\n // \"color: orange; font-weight: bold\",\n // \"color: inherit; font-weight: normal\",\n ...x,\n );\n"]}
1
+ {"version":3,"file":"log.js","sourceRoot":"","sources":["../../src/utils/log.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE5C,MAAM,CAAC,MAAM,GAAG,GAAG,YAAY,CAAC,SAAS,EAAE,cAAc,CAAC,CAAC","sourcesContent":["import { createLogger } from \"@openlv/core\";\n\nexport const log = createLogger(\"session\", \"mediumpurple\");\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openlv/session",
3
- "version": "0.0.3",
3
+ "version": "0.1.0",
4
4
  "keywords": [
5
5
  "open-lavatory"
6
6
  ],
@@ -37,9 +37,9 @@
37
37
  "eventemitter3": "^5.0.1",
38
38
  "globals": "^16.2.0",
39
39
  "ts-pattern": "^5",
40
- "@openlv/core": "0.0.2",
41
- "@openlv/signaling": "0.0.3",
42
- "@openlv/transport": "0.0.3"
40
+ "@openlv/core": "0.1.0",
41
+ "@openlv/signaling": "0.1.0",
42
+ "@openlv/transport": "0.1.0"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@vitest/browser-playwright": "^4.0.8",