@openlv/session 0.1.3 → 0.3.1
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/dynamic.d.ts +2 -0
- package/dist/dynamic.js +26 -0
- package/dist/dynamic.js.map +1 -0
- package/dist/events.d.ts +5 -7
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +16 -32
- package/dist/index.js +138 -163
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +12 -8
- package/dist/index.test.js.map +1 -1
- package/dist/messages/correlate.d.ts +13 -5
- package/dist/messages/correlate.js +47 -39
- package/dist/messages/correlate.js.map +1 -1
- package/dist/messages/correlate.test.js +18 -0
- package/dist/messages/correlate.test.js.map +1 -0
- package/dist/messages/index.d.ts +12 -3
- package/dist/messages/index.js +11 -1
- package/dist/messages/index.js.map +1 -1
- package/dist/messages/index.test.d.ts +1 -0
- package/dist/messages/index.test.js +17 -0
- package/dist/messages/index.test.js.map +1 -0
- package/dist/options.test.js +3 -7
- package/dist/options.test.js.map +1 -1
- package/package.json +5 -5
- package/dist/transports.d.ts +0 -7
- package/dist/transports.js +0 -12
- package/dist/transports.js.map +0 -1
- package/dist/transports.test.js +0 -22
- package/dist/transports.test.js.map +0 -1
- /package/dist/{transports.test.d.ts → messages/correlate.test.d.ts} +0 -0
package/dist/dynamic.js
ADDED
|
@@ -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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
request: (payload:
|
|
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
|
+
*/
|
|
7
|
+
request: (payload: unknown) => void;
|
|
10
8
|
};
|
package/dist/events.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"","sourcesContent":["
|
|
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: unknown) => void;\n};\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { type SessionHandshakeParameters, type SessionLinkParameters } from "@openlv/core";
|
|
2
|
-
import { type PeerInfo, type SignalingLayer,
|
|
3
|
-
import { type TransportLayer, type
|
|
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
|
|
7
|
-
export
|
|
6
|
+
export { loadSignaling, loadTransport } from "./dynamic.js";
|
|
7
|
+
export { RequestFailedError } from "./messages/correlate.js";
|
|
8
|
+
export type { SessionMessage } from "./messages/index.js";
|
|
9
|
+
export type { PeerInfo } from "@openlv/signaling";
|
|
10
|
+
export declare const SessionStatus: {
|
|
8
11
|
readonly CREATED: "created";
|
|
9
12
|
readonly SIGNALING: "signaling";
|
|
10
13
|
readonly READY: "ready";
|
|
@@ -12,19 +15,8 @@ export declare const SESSION_STATE: {
|
|
|
12
15
|
readonly CONNECTED: "connected";
|
|
13
16
|
readonly DISCONNECTED: "disconnected";
|
|
14
17
|
};
|
|
15
|
-
export type
|
|
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
|
-
};
|
|
18
|
+
export type SessionStatus = (typeof SessionStatus)[keyof typeof SessionStatus];
|
|
26
19
|
export type SessionOptions = {
|
|
27
|
-
/** Shared with the peer during the handshake and shown in its UI. */
|
|
28
20
|
info?: PeerInfo;
|
|
29
21
|
};
|
|
30
22
|
/**
|
|
@@ -33,22 +25,14 @@ export type SessionOptions = {
|
|
|
33
25
|
* https://openlv.sh/api/session
|
|
34
26
|
*/
|
|
35
27
|
export type Session = {
|
|
36
|
-
|
|
28
|
+
status: Observable<SessionStatus>;
|
|
29
|
+
signalStatus: Observable<SignalStatus>;
|
|
30
|
+
error: Observable<string | undefined>;
|
|
31
|
+
peerInfo: Observable<PeerInfo | undefined>;
|
|
37
32
|
getHandshakeParameters(): SessionHandshakeParameters;
|
|
38
33
|
connect(): Promise<void>;
|
|
39
|
-
waitForLink(): Promise<void>;
|
|
40
34
|
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
|
-
send(message: object, ackTimeout?: number, responseTimeout?: number): Promise<unknown>;
|
|
35
|
+
send(message: unknown, ackTimeout?: number, responseTimeout?: number): Promise<unknown>;
|
|
52
36
|
emitter: EventEmitter<SessionEvents>;
|
|
53
37
|
_internal: {
|
|
54
38
|
signal: SignalingLayer;
|
|
@@ -61,8 +45,8 @@ export type Session = {
|
|
|
61
45
|
*
|
|
62
46
|
* https://openlv.sh/api/session
|
|
63
47
|
*/
|
|
64
|
-
export declare const createSession: (initParameters: SessionLinkParameters,
|
|
48
|
+
export declare const createSession: (initParameters: SessionLinkParameters, transportLayers: TransportLayerFunction[], onMessage: (message: unknown) => Promise<unknown>, options?: SessionOptions) => Promise<Session>;
|
|
65
49
|
/**
|
|
66
50
|
* Connect to a session from its openlv:// URL
|
|
67
51
|
*/
|
|
68
|
-
export declare const connectSession: (connectionUrl: string, onMessage: (message:
|
|
52
|
+
export declare const connectSession: (connectionUrl: string, onMessage: (message: unknown) => Promise<unknown>, transports: TransportLayerFunction[], options?: SessionOptions) => Promise<Session>;
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import { decodeConnectionURL, OPENLV_PROTOCOL_VERSION, } from "@openlv/core";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { TRANSPORT_STATE, } from "@openlv/transport";
|
|
1
|
+
import { createScope, decodeConnectionURL, observable, OPENLV_PROTOCOL_VERSION, } from "@openlv/core";
|
|
2
|
+
import { createHandshakeCipher, generateHandshakeKey, generateKeyPair, generateSessionId, hashPublicKey, } 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 {
|
|
8
|
+
import { isSessionMessage } from "./messages/index.js";
|
|
9
9
|
import { log } from "./utils/log.js";
|
|
10
|
-
export
|
|
10
|
+
export { loadSignaling, loadTransport } from "./dynamic.js";
|
|
11
|
+
export { RequestFailedError } from "./messages/correlate.js";
|
|
12
|
+
export const SessionStatus = {
|
|
11
13
|
CREATED: "created",
|
|
12
14
|
SIGNALING: "signaling",
|
|
13
15
|
READY: "ready",
|
|
@@ -20,13 +22,11 @@ export const SESSION_STATE = {
|
|
|
20
22
|
*
|
|
21
23
|
* https://openlv.sh/api/session
|
|
22
24
|
*/
|
|
23
|
-
export const createSession = async (initParameters,
|
|
25
|
+
export const createSession = async (initParameters, transportLayers, onMessage, options) => {
|
|
24
26
|
if (transportLayers.length === 0) {
|
|
25
27
|
throw new Error("At least one transport is required");
|
|
26
28
|
}
|
|
27
29
|
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
30
|
const problem = validatePeerInfo(options.info);
|
|
31
31
|
if (problem)
|
|
32
32
|
throw new Error(`Invalid session info: ${problem}`);
|
|
@@ -36,101 +36,100 @@ export const createSession = async (initParameters, signalLayer, transportLayers
|
|
|
36
36
|
const sessionId = "sessionId" in initParameters
|
|
37
37
|
? initParameters.sessionId
|
|
38
38
|
: generateSessionId();
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const { hash, isHost } = await initHash("h" in initParameters ? initParameters.h : undefined, encryptionKey);
|
|
45
|
-
let status = SESSION_STATE.CREATED;
|
|
46
|
-
let lastError;
|
|
39
|
+
const keyPair = generateKeyPair();
|
|
40
|
+
const k = "k" in initParameters ? initParameters.k : generateHandshakeKey();
|
|
41
|
+
const ownHash = await hashPublicKey(keyPair.publicKey);
|
|
42
|
+
const hash = ("h" in initParameters && initParameters.h) || ownHash;
|
|
43
|
+
const isHost = hash === ownHash;
|
|
47
44
|
const protocol = initParameters.p;
|
|
48
45
|
const server = initParameters.s;
|
|
49
46
|
const capabilities = {
|
|
50
47
|
transports: transportLayers.map(layer => layer.transportId),
|
|
51
48
|
...(options?.info && { info: options.info }),
|
|
52
49
|
};
|
|
53
|
-
|
|
50
|
+
const [status, setStatus] = observable(SessionStatus.CREATED);
|
|
51
|
+
const [lastError, setLastError] = observable(undefined);
|
|
52
|
+
const [peerInfo, setPeerInfo] = observable(undefined);
|
|
53
|
+
const setLastErrorIfUnset = (reason) => {
|
|
54
|
+
if (lastError.get() === undefined)
|
|
55
|
+
setLastError(reason);
|
|
56
|
+
};
|
|
54
57
|
const updateStatus = (newStatus) => {
|
|
55
58
|
log("updateStatus", newStatus);
|
|
56
|
-
|
|
57
|
-
emitter.emit("state_change", {
|
|
58
|
-
status,
|
|
59
|
-
signaling: signal.getState(),
|
|
60
|
-
peerInfo: peerCaps?.info,
|
|
61
|
-
error: lastError,
|
|
62
|
-
});
|
|
59
|
+
setStatus(newStatus);
|
|
63
60
|
};
|
|
61
|
+
status.subscribe((current) => {
|
|
62
|
+
if (current === SessionStatus.DISCONNECTED) {
|
|
63
|
+
messages.emit("ended", lastError.get() ?? "Session closed");
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
const signalLayer = await loadSignaling(protocol);
|
|
64
67
|
const signaling = await signalLayer({
|
|
65
68
|
topic: sessionId,
|
|
66
69
|
url: server,
|
|
67
70
|
});
|
|
68
71
|
const signal = await signaling({
|
|
69
72
|
h: hash,
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
},
|
|
73
|
-
async encrypt(message) {
|
|
74
|
-
if (!relyingPublicKey) {
|
|
75
|
-
throw new Error("Relying party public key not found");
|
|
76
|
-
}
|
|
77
|
-
return await relyingPublicKey.encrypt(message);
|
|
78
|
-
},
|
|
79
|
-
decrypt,
|
|
80
|
-
publicKey: encryptionKey,
|
|
81
|
-
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
|
-
},
|
|
73
|
+
handshakeCipher: await createHandshakeCipher(k),
|
|
74
|
+
keyPair,
|
|
87
75
|
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
76
|
isHost,
|
|
96
77
|
});
|
|
78
|
+
const scope = createScope();
|
|
79
|
+
scope.add(signal.teardown);
|
|
80
|
+
signal.peer.subscribe((peer) => {
|
|
81
|
+
const role = isHost ? "host" : "client";
|
|
82
|
+
log(`rpKey discovered by ${role}`, peer?.peerKey);
|
|
83
|
+
});
|
|
84
|
+
signal.peerCapabilities.subscribe((received) => {
|
|
85
|
+
log("peer capabilities received", received);
|
|
86
|
+
setPeerInfo(received?.info);
|
|
87
|
+
});
|
|
97
88
|
let transport;
|
|
98
|
-
const createTransport = (layer) => layer.create({
|
|
99
|
-
|
|
100
|
-
if (!relyingPublicKey) {
|
|
101
|
-
throw new Error("Relying party public key not found");
|
|
102
|
-
}
|
|
103
|
-
return relyingPublicKey?.encrypt(message);
|
|
104
|
-
},
|
|
105
|
-
decrypt,
|
|
89
|
+
const createTransport = (layer, cipher) => layer.create({
|
|
90
|
+
cipher,
|
|
106
91
|
isHost,
|
|
107
92
|
onmessage: async (message) => {
|
|
108
93
|
log("Session: received message from transport", message);
|
|
94
|
+
if (!isSessionMessage(message)) {
|
|
95
|
+
log("dropping invalid session message", message);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (message["type"] === "close") {
|
|
99
|
+
await teardown();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
109
102
|
if (message["type"] === "request") {
|
|
110
103
|
const messageId = message["messageId"];
|
|
111
104
|
try {
|
|
112
105
|
// Immediately acknowledge receipt so the sender's ack-timeout does
|
|
113
106
|
// not fire while the handler (which may await user interaction) runs.
|
|
114
107
|
await transport?.send({ type: "ack", messageId });
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
108
|
+
let answer;
|
|
109
|
+
try {
|
|
110
|
+
// Notify observers before processing (e.g. wallet UI can show a
|
|
111
|
+
// pending indicator before the handler resolves).
|
|
112
|
+
emitter.emit("request", message["payload"]);
|
|
113
|
+
answer = {
|
|
114
|
+
type: "response",
|
|
115
|
+
messageId,
|
|
116
|
+
payload: await onMessage(message["payload"]),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
// Only the fact of failure goes to the peer; the thrown value
|
|
121
|
+
// may carry wallet-private details and stays in the local log.
|
|
122
|
+
log("request handler threw", error);
|
|
123
|
+
answer = { type: "failed", messageId };
|
|
124
|
+
}
|
|
125
|
+
await transport?.send(answer);
|
|
127
126
|
}
|
|
128
127
|
catch (error) {
|
|
129
128
|
log("failed to respond to peer request", error);
|
|
130
129
|
}
|
|
131
130
|
}
|
|
132
|
-
|
|
133
|
-
//
|
|
131
|
+
else {
|
|
132
|
+
// What is left is an ack, a response or a failure: the send() correlator takes all three.
|
|
134
133
|
messages.emit("message", message);
|
|
135
134
|
}
|
|
136
135
|
},
|
|
@@ -149,63 +148,66 @@ export const createSession = async (initParameters, signalLayer, transportLayers
|
|
|
149
148
|
* keep the first configured transport.
|
|
150
149
|
*/
|
|
151
150
|
const selectTransportLayer = () => {
|
|
151
|
+
const peerCaps = signal.peerCapabilities.get();
|
|
152
152
|
if (!peerCaps)
|
|
153
153
|
return transportLayers[0];
|
|
154
|
-
|
|
154
|
+
// The host's preferred transport that the client also supports; both
|
|
155
|
+
// peers compute the same result independently -- no confirmation round trip.
|
|
156
|
+
const hostPreference = isHost ? capabilities.transports : peerCaps.transports;
|
|
157
|
+
const clientSupported = new Set(isHost ? peerCaps.transports : capabilities.transports);
|
|
158
|
+
const selected = hostPreference.find(transportId => clientSupported.has(transportId));
|
|
155
159
|
return transportLayers.find(layer => layer.transportId === selected);
|
|
156
160
|
};
|
|
157
161
|
// Once both peers are present (signaling encrypted) the transport should
|
|
158
162
|
// connect promptly; if it cannot (e.g. no ICE candidates on a restricted
|
|
159
163
|
// network) fail loudly instead of sitting in "linking" forever.
|
|
160
164
|
const TRANSPORT_LINK_TIMEOUT_MS = 45000;
|
|
161
|
-
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
clearLinkDeadline();
|
|
170
|
-
lastError = undefined;
|
|
171
|
-
updateStatus(SESSION_STATE.CONNECTED);
|
|
165
|
+
// From transport start until the session connects or ends
|
|
166
|
+
const linking = scope.child();
|
|
167
|
+
linking.closeWhen(status, value => value === SessionStatus.CONNECTED || value === SessionStatus.DISCONNECTED);
|
|
168
|
+
const onTransportStateChange = (transportStatus) => {
|
|
169
|
+
log("transport state change", transportStatus);
|
|
170
|
+
if (transportStatus === TransportStatus.CONNECTED) {
|
|
171
|
+
setLastError(undefined);
|
|
172
|
+
updateStatus(SessionStatus.CONNECTED);
|
|
172
173
|
}
|
|
173
|
-
if (
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
updateStatus(SESSION_STATE.DISCONNECTED);
|
|
174
|
+
if (transportStatus === TransportStatus.ERROR) {
|
|
175
|
+
setLastErrorIfUnset("Peer-to-peer transport failed");
|
|
176
|
+
updateStatus(SessionStatus.DISCONNECTED);
|
|
177
177
|
}
|
|
178
178
|
};
|
|
179
179
|
const onTransportError = (reason) => {
|
|
180
180
|
if (reason)
|
|
181
|
-
|
|
181
|
+
setLastError(reason);
|
|
182
182
|
};
|
|
183
183
|
const startTransport = () => {
|
|
184
184
|
if (!transport) {
|
|
185
185
|
const layer = selectTransportLayer();
|
|
186
186
|
if (!layer) {
|
|
187
|
-
log("no common transport with peer", capabilities.transports,
|
|
188
|
-
|
|
189
|
-
updateStatus(
|
|
187
|
+
log("no common transport with peer", capabilities.transports, signal.peerCapabilities.get()?.transports);
|
|
188
|
+
setLastErrorIfUnset("No common transport with peer");
|
|
189
|
+
updateStatus(SessionStatus.DISCONNECTED);
|
|
190
190
|
return;
|
|
191
191
|
}
|
|
192
192
|
log("selected transport", layer.transportId);
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
193
|
+
const peer = signal.peer.get();
|
|
194
|
+
if (!peer) {
|
|
195
|
+
throw new Error("Relying party public key not found");
|
|
196
|
+
}
|
|
197
|
+
transport = createTransport(layer, peer.cipher);
|
|
198
|
+
scope.add(transport.teardown);
|
|
199
|
+
scope.add(transport.status.subscribe(onTransportStateChange));
|
|
200
|
+
scope.listen(transport.emitter, "error", onTransportError);
|
|
201
|
+
linking.timeout(TRANSPORT_LINK_TIMEOUT_MS, () => {
|
|
202
|
+
log("transport failed to connect in time");
|
|
203
|
+
setLastErrorIfUnset("Timed out establishing the peer-to-peer connection");
|
|
204
|
+
updateStatus(SessionStatus.DISCONNECTED);
|
|
205
|
+
});
|
|
196
206
|
}
|
|
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
207
|
Promise.resolve(transport.setup()).catch((error) => {
|
|
205
208
|
log("transport setup failed", error);
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
updateStatus(SESSION_STATE.DISCONNECTED);
|
|
209
|
+
setLastErrorIfUnset(error instanceof Error ? error.message : "Transport setup failed");
|
|
210
|
+
updateStatus(SessionStatus.DISCONNECTED);
|
|
209
211
|
});
|
|
210
212
|
};
|
|
211
213
|
const onSignalMessage = async (message) => {
|
|
@@ -214,7 +216,7 @@ export const createSession = async (initParameters, signalLayer, transportLayers
|
|
|
214
216
|
if (sessionMessage.type === "response") {
|
|
215
217
|
messages.emit("message", sessionMessage);
|
|
216
218
|
}
|
|
217
|
-
if (sessionMessage.type === "request") {
|
|
219
|
+
else if (sessionMessage.type === "request") {
|
|
218
220
|
if (!transport) {
|
|
219
221
|
log("dropping negotiation message: transport not selected yet");
|
|
220
222
|
return;
|
|
@@ -229,83 +231,60 @@ export const createSession = async (initParameters, signalLayer, transportLayers
|
|
|
229
231
|
}
|
|
230
232
|
}
|
|
231
233
|
};
|
|
232
|
-
const onSignalStateChange = (
|
|
233
|
-
log("signal state change",
|
|
234
|
-
if (
|
|
235
|
-
updateStatus(
|
|
234
|
+
const onSignalStateChange = (signalStatus) => {
|
|
235
|
+
log("signal state change", signalStatus);
|
|
236
|
+
if (signalStatus === SignalStatus.READY) {
|
|
237
|
+
updateStatus(SessionStatus.READY);
|
|
236
238
|
}
|
|
237
239
|
if ([
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
].includes(
|
|
241
|
-
updateStatus(
|
|
240
|
+
SignalStatus.HANDSHAKE,
|
|
241
|
+
SignalStatus.HANDSHAKE_PARTIAL,
|
|
242
|
+
].includes(signalStatus)) {
|
|
243
|
+
updateStatus(SessionStatus.LINKING);
|
|
242
244
|
}
|
|
243
|
-
if (
|
|
245
|
+
if (signalStatus === SignalStatus.ENCRYPTED) {
|
|
244
246
|
startTransport();
|
|
245
247
|
}
|
|
246
|
-
if (
|
|
247
|
-
log("signaling error
|
|
248
|
-
|
|
249
|
-
updateStatus(
|
|
248
|
+
if (signalStatus === SignalStatus.ERROR) {
|
|
249
|
+
log("signaling error -- marking session disconnected");
|
|
250
|
+
setLastErrorIfUnset("Signaling failed or timed out");
|
|
251
|
+
updateStatus(SessionStatus.DISCONNECTED);
|
|
250
252
|
}
|
|
251
253
|
};
|
|
254
|
+
const teardown = async () => {
|
|
255
|
+
await scope.close();
|
|
256
|
+
updateStatus(SessionStatus.DISCONNECTED);
|
|
257
|
+
};
|
|
252
258
|
return {
|
|
253
259
|
connect: async () => {
|
|
254
|
-
updateStatus(
|
|
260
|
+
updateStatus(SessionStatus.SIGNALING);
|
|
255
261
|
log("connecting to session, isHost:", isHost);
|
|
256
|
-
|
|
257
|
-
signal.
|
|
262
|
+
scope.listen(signal, "message", onSignalMessage);
|
|
263
|
+
scope.add(signal.status.subscribe(onSignalStateChange));
|
|
258
264
|
await signal.setup();
|
|
259
265
|
},
|
|
260
266
|
async close() {
|
|
261
267
|
log("session teardown");
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
transport?.emitter.off("state_change", onTransportStateChange);
|
|
266
|
-
transport?.emitter.off("error", onTransportError);
|
|
267
|
-
await Promise.all([
|
|
268
|
-
transport?.teardown(),
|
|
269
|
-
signal.teardown(),
|
|
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
|
-
};
|
|
268
|
+
await transport?.send({ type: "close" })
|
|
269
|
+
.catch(error => log("failed to send close to peer", error));
|
|
270
|
+
await teardown();
|
|
280
271
|
},
|
|
272
|
+
status,
|
|
273
|
+
signalStatus: signal.status,
|
|
274
|
+
error: lastError,
|
|
275
|
+
peerInfo,
|
|
281
276
|
getHandshakeParameters() {
|
|
282
277
|
return {
|
|
283
278
|
version: OPENLV_PROTOCOL_VERSION,
|
|
284
279
|
sessionId,
|
|
285
280
|
h: hash,
|
|
286
|
-
k
|
|
281
|
+
k,
|
|
287
282
|
p: protocol,
|
|
288
283
|
s: server,
|
|
289
284
|
};
|
|
290
285
|
},
|
|
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
286
|
async send(message, ackTimeout = 10000, responseTimeout = 60 * 60000) {
|
|
308
|
-
if (!transport || signal.
|
|
287
|
+
if (!transport || signal.status.get() !== SignalStatus.ENCRYPTED) {
|
|
309
288
|
throw new Error("Session not ready");
|
|
310
289
|
}
|
|
311
290
|
const messageId = crypto.randomUUID();
|
|
@@ -331,10 +310,6 @@ export const createSession = async (initParameters, signalLayer, transportLayers
|
|
|
331
310
|
*/
|
|
332
311
|
export const connectSession = async (connectionUrl, onMessage, transports, options) => {
|
|
333
312
|
const initParameters = decodeConnectionURL(connectionUrl);
|
|
334
|
-
|
|
335
|
-
if (!signaling) {
|
|
336
|
-
throw new Error(`Invalid signaling protocol: ${initParameters.p}`);
|
|
337
|
-
}
|
|
338
|
-
return createSession(initParameters, signaling, transports, onMessage, options);
|
|
313
|
+
return createSession(initParameters, transports, onMessage, options);
|
|
339
314
|
};
|
|
340
315
|
//# 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 createScope,\n decodeConnectionURL,\n type Observable,\n observable,\n OPENLV_PROTOCOL_VERSION,\n type SessionHandshakeParameters,\n type SessionLinkParameters,\n} from \"@openlv/core\";\nimport {\n type Cipher,\n createHandshakeCipher,\n generateHandshakeKey,\n generateKeyPair,\n generateSessionId,\n hashPublicKey,\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, type CorrelationEvents } from \"./messages/correlate.js\";\nimport { isSessionMessage, type SessionMessage } from \"./messages/index.js\";\nimport { log } from \"./utils/log.js\";\n\nexport { loadSignaling, loadTransport } from \"./dynamic.js\";\nexport { RequestFailedError } from \"./messages/correlate.js\";\nexport type { SessionMessage } from \"./messages/index.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(\n message: unknown,\n ackTimeout?: number,\n responseTimeout?: number,\n ): 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: unknown) => Promise<unknown>,\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<CorrelationEvents>();\n const sessionId\n = \"sessionId\" in initParameters\n ? initParameters.sessionId\n : generateSessionId();\n const keyPair = generateKeyPair();\n const k = \"k\" in initParameters ? initParameters.k : generateHandshakeKey();\n const ownHash = await hashPublicKey(keyPair.publicKey);\n const hash = (\"h\" in initParameters && initParameters.h) || ownHash;\n const isHost = hash === ownHash;\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 status.subscribe((current) => {\n if (current === SessionStatus.DISCONNECTED) {\n messages.emit(\"ended\", lastError.get() ?? \"Session closed\");\n }\n });\n\n const signalLayer = await loadSignaling(protocol);\n const signaling = await signalLayer({\n topic: sessionId,\n url: server,\n });\n const signal = await signaling({\n h: hash,\n handshakeCipher: await createHandshakeCipher(k),\n keyPair,\n capabilities,\n isHost,\n });\n\n const scope = createScope();\n\n scope.add(signal.teardown);\n\n signal.peer.subscribe((peer) => {\n const role = isHost ? \"host\" : \"client\";\n\n log(`rpKey discovered by ${role}`, peer?.peerKey);\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, cipher: Cipher): TransportLayer => layer.create({\n cipher,\n isHost,\n onmessage: async (message) => {\n log(\"Session: received message from transport\", message);\n\n if (!isSessionMessage(message)) {\n log(\"dropping invalid session message\", message);\n\n return;\n }\n\n if (message[\"type\"] === \"close\") {\n await teardown();\n\n return;\n }\n\n if (message[\"type\"] === \"request\") {\n const messageId = message[\"messageId\"];\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 let answer: SessionMessage;\n\n try {\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\"]);\n\n answer = {\n type: \"response\",\n messageId,\n payload: await onMessage(message[\"payload\"]),\n };\n }\n catch (error) {\n // Only the fact of failure goes to the peer; the thrown value\n // may carry wallet-private details and stays in the local log.\n log(\"request handler threw\", error);\n answer = { type: \"failed\", messageId };\n }\n\n await transport?.send(answer);\n }\n catch (error) {\n log(\"failed to respond to peer request\", error);\n }\n }\n\n else {\n // What is left is an ack, a response or a failure: the send() correlator takes all three.\n messages.emit(\"message\", message);\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 // From transport start until the session connects or ends\n const linking = scope.child();\n\n linking.closeWhen(status, value => value === SessionStatus.CONNECTED || value === SessionStatus.DISCONNECTED);\n\n const onTransportStateChange = (transportStatus: TransportStatus) => {\n log(\"transport state change\", transportStatus);\n\n if (transportStatus === TransportStatus.CONNECTED) {\n setLastError(undefined);\n updateStatus(SessionStatus.CONNECTED);\n }\n\n if (transportStatus === TransportStatus.ERROR) {\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 const peer = signal.peer.get();\n\n if (!peer) {\n throw new Error(\"Relying party public key not found\");\n }\n\n transport = createTransport(layer, peer.cipher);\n scope.add(transport.teardown);\n scope.add(transport.status.subscribe(onTransportStateChange));\n scope.listen(transport.emitter, \"error\", onTransportError);\n linking.timeout(TRANSPORT_LINK_TIMEOUT_MS, () => {\n log(\"transport failed to connect in time\");\n setLastErrorIfUnset(\"Timed out establishing the peer-to-peer connection\");\n updateStatus(SessionStatus.DISCONNECTED);\n });\n }\n\n Promise.resolve(transport.setup()).catch((error) => {\n log(\"transport setup failed\", error);\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 const teardown = async () => {\n await scope.close();\n updateStatus(SessionStatus.DISCONNECTED);\n };\n\n return {\n connect: async () => {\n updateStatus(SessionStatus.SIGNALING);\n log(\"connecting to session, isHost:\", isHost);\n\n scope.listen(signal, \"message\", onSignalMessage);\n scope.add(signal.status.subscribe(onSignalStateChange));\n\n await signal.setup();\n },\n async close() {\n log(\"session teardown\");\n await transport?.send({ type: \"close\" } satisfies SessionMessage)\n .catch(error => log(\"failed to send close to peer\", error));\n await teardown();\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,\n p: protocol,\n s: server,\n };\n },\n async send(\n message: unknown,\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: unknown) => Promise<unknown>,\n transports: TransportLayerFunction[],\n options?: SessionOptions,\n): Promise<Session> => {\n const initParameters = decodeConnectionURL(connectionUrl);\n\n return createSession(initParameters, transports, onMessage, options);\n};\n"]}
|
package/dist/index.test.js
CHANGED
|
@@ -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, RequestFailedError, 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
|
-
},
|
|
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.
|
|
16
|
+
console.log(sessionA.status.get());
|
|
18
17
|
await sessionA.connect();
|
|
19
18
|
const handshakeParametersA = sessionA.getHandshakeParameters();
|
|
20
19
|
const encodedUrl = encodeConnectionURL(handshakeParametersA);
|
|
@@ -26,17 +25,22 @@ describe("Session", () => {
|
|
|
26
25
|
console.log("Connecting to session B");
|
|
27
26
|
const sessionB = await connectSession(encodedUrl, async (message) => {
|
|
28
27
|
console.log("sessionB received message", message);
|
|
28
|
+
if (JSON.stringify(message).includes("boom")) {
|
|
29
|
+
throw new Error("failed to sign with privkey 12345");
|
|
30
|
+
}
|
|
29
31
|
return { result: "success" };
|
|
30
32
|
}, [webrtc()]);
|
|
31
|
-
console.log(sessionB.
|
|
33
|
+
console.log(sessionB.status.get());
|
|
32
34
|
await sessionB.connect();
|
|
33
|
-
await Promise.all([sessionA
|
|
35
|
+
await Promise.all([sessionA, sessionB].map(session => session.status.until(current => current === SessionStatus.CONNECTED)));
|
|
34
36
|
//
|
|
35
|
-
console.log("A", sessionA.
|
|
36
|
-
console.log("B", sessionB.
|
|
37
|
+
console.log("A", sessionA.status.get());
|
|
38
|
+
console.log("B", sessionB.status.get());
|
|
37
39
|
const response = await sessionA.send({ data: "test" });
|
|
38
40
|
console.log("response", response);
|
|
39
41
|
expect(response).toEqual({ result: "success" });
|
|
42
|
+
// A throwing handler fails the request at once instead of leaving it to time out.
|
|
43
|
+
await expect(sessionA.send({ data: "boom" })).rejects.toBeInstanceOf(RequestFailedError);
|
|
40
44
|
});
|
|
41
45
|
});
|
|
42
46
|
//# sourceMappingURL=index.test.js.map
|
package/dist/index.test.js.map
CHANGED
|
@@ -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,
|
|
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, RequestFailedError, 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 if (JSON.stringify(message).includes(\"boom\")) {\n throw new Error(\"failed to sign with privkey 12345\");\n }\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 // A throwing handler fails the request at once instead of leaving it to time out.\n await expect(sessionA.send({ data: \"boom\" })).rejects.toBeInstanceOf(RequestFailedError);\n });\n});\n"]}
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import type { EventEmitter } from "eventemitter3";
|
|
2
|
-
import type {
|
|
2
|
+
import type { SessionMessageAck, SessionMessageFailed, SessionMessageResponse } from "./index.js";
|
|
3
|
+
/** The peer's handler threw. The peer sends no detail, so there is none here. */
|
|
4
|
+
export declare class RequestFailedError extends Error {
|
|
5
|
+
name: string;
|
|
6
|
+
constructor();
|
|
7
|
+
}
|
|
8
|
+
export type CorrelationEvents = {
|
|
9
|
+
message: SessionMessageAck | SessionMessageFailed | SessionMessageResponse;
|
|
10
|
+
/** The session ended; every open request fails with `reason`. */
|
|
11
|
+
ended: (reason: string) => void;
|
|
12
|
+
};
|
|
3
13
|
/**
|
|
4
14
|
* Await the ack and response correlated to a sent request.
|
|
5
15
|
*
|
|
6
16
|
* Two-phase timeout: the peer must ack within `ackTimeoutMs`, confirming it
|
|
7
17
|
* received the message; after the ack the wait extends to `responseTimeoutMs`
|
|
8
|
-
*
|
|
18
|
+
* -- enough for user-interactive flows such as `eth_sendTransaction`.
|
|
9
19
|
*/
|
|
10
|
-
export declare const awaitCorrelatedResponse: (messages: EventEmitter<
|
|
11
|
-
message: SessionMessage;
|
|
12
|
-
}>, messageId: string, ackTimeoutMs: number, responseTimeoutMs: number) => Promise<unknown>;
|
|
20
|
+
export declare const awaitCorrelatedResponse: (messages: EventEmitter<CorrelationEvents>, messageId: string, ackTimeoutMs: number, responseTimeoutMs: number) => Promise<unknown>;
|
|
@@ -1,46 +1,54 @@
|
|
|
1
|
+
import { createScope } from "@openlv/core";
|
|
2
|
+
/** The peer's handler threw. The peer sends no detail, so there is none here. */
|
|
3
|
+
export class RequestFailedError extends Error {
|
|
4
|
+
constructor() {
|
|
5
|
+
super("The remote peer failed to handle the request");
|
|
6
|
+
Object.defineProperty(this, "name", {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
configurable: true,
|
|
9
|
+
writable: true,
|
|
10
|
+
value: "RequestFailedError"
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
}
|
|
1
14
|
/**
|
|
2
15
|
* Await the ack and response correlated to a sent request.
|
|
3
16
|
*
|
|
4
17
|
* Two-phase timeout: the peer must ack within `ackTimeoutMs`, confirming it
|
|
5
18
|
* received the message; after the ack the wait extends to `responseTimeoutMs`
|
|
6
|
-
*
|
|
19
|
+
* -- enough for user-interactive flows such as `eth_sendTransaction`.
|
|
7
20
|
*/
|
|
8
|
-
export const awaitCorrelatedResponse = (messages, messageId, ackTimeoutMs, responseTimeoutMs) =>
|
|
9
|
-
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
reject(new
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
cleanup();
|
|
43
|
-
reject(new Error("Request timed out: remote peer did not acknowledge"));
|
|
44
|
-
}, ackTimeoutMs);
|
|
45
|
-
});
|
|
21
|
+
export const awaitCorrelatedResponse = async (messages, messageId, ackTimeoutMs, responseTimeoutMs) => {
|
|
22
|
+
const request = createScope();
|
|
23
|
+
// Short window for the ack -- tells us the peer is alive and processing.
|
|
24
|
+
const ackWait = request.child();
|
|
25
|
+
const timeout = (scope, delayMs, reason) => new Promise((_resolve, reject) => {
|
|
26
|
+
scope.timeout(delayMs, () => reject(new Error(`Request timed out: ${reason}`)));
|
|
27
|
+
});
|
|
28
|
+
const acknowledged = new Promise((resolve) => {
|
|
29
|
+
ackWait.listen(messages, "message", (message) => {
|
|
30
|
+
if (message.messageId === messageId && message.type === "ack")
|
|
31
|
+
resolve();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
const response = new Promise((resolve, reject) => {
|
|
35
|
+
request.listen(messages, "ended", (reason) => reject(new Error(reason)));
|
|
36
|
+
request.listen(messages, "message", (message) => {
|
|
37
|
+
if (message.messageId !== messageId)
|
|
38
|
+
return;
|
|
39
|
+
if (message.type === "failed")
|
|
40
|
+
reject(new RequestFailedError());
|
|
41
|
+
else if (message.type === "response")
|
|
42
|
+
resolve(message.payload);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
try {
|
|
46
|
+
await Promise.race([acknowledged, response, timeout(ackWait, ackTimeoutMs, "remote peer did not acknowledge")]);
|
|
47
|
+
await ackWait.close();
|
|
48
|
+
return await Promise.race([response, timeout(request, responseTimeoutMs, "no response after acknowledgement")]);
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
await request.close();
|
|
52
|
+
}
|
|
53
|
+
};
|
|
46
54
|
//# sourceMappingURL=correlate.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"correlate.js","sourceRoot":"","sources":["../../src/messages/correlate.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"correlate.js","sourceRoot":"","sources":["../../src/messages/correlate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAc,MAAM,cAAc,CAAC;AAKvD,iFAAiF;AACjF,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAG3C;QACE,KAAK,CAAC,8CAA8C,CAAC,CAAC;QAH/C;;;;mBAAO,oBAAoB;WAAC;IAIrC,CAAC;CACF;AAQD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,KAAK,EAC1C,QAAyC,EACzC,SAAiB,EACjB,YAAoB,EACpB,iBAAyB,EACP,EAAE;IACpB,MAAM,OAAO,GAAG,WAAW,EAAE,CAAC;IAC9B,yEAAyE;IACzE,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC;IAEhC,MAAM,OAAO,GAAG,CAAC,KAAY,EAAE,OAAe,EAAE,MAAc,EAAE,EAAE,CAAC,IAAI,OAAO,CAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE;QACzG,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,sBAAsB,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC;IAClF,CAAC,CAAC,CAAC;IACH,MAAM,YAAY,GAAG,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;QACjD,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,SAAS,EAAE,CAAC,OAAqC,EAAE,EAAE;YAC5E,IAAI,OAAO,CAAC,SAAS,KAAK,SAAS,IAAI,OAAO,CAAC,IAAI,KAAK,KAAK;gBAAE,OAAO,EAAE,CAAC;QAC3E,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IACH,MAAM,QAAQ,GAAG,IAAI,OAAO,CAAU,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACxD,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,OAAO,EAAE,CAAC,MAAc,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACjF,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,SAAS,EAAE,CAAC,OAAqC,EAAE,EAAE;YAC5E,IAAI,OAAO,CAAC,SAAS,KAAK,SAAS;gBAAE,OAAO;YAE5C,IAAI,OAAO,CAAC,IAAI,KAAK,QAAQ;gBAAE,MAAM,CAAC,IAAI,kBAAkB,EAAE,CAAC,CAAC;iBAC3D,IAAI,OAAO,CAAC,IAAI,KAAK,UAAU;gBAAE,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACjE,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC;QACH,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,YAAY,EAAE,QAAQ,EAAE,OAAO,CAAC,OAAO,EAAE,YAAY,EAAE,iCAAiC,CAAC,CAAC,CAAC,CAAC;QAChH,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;QAEtB,OAAO,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,OAAO,CAAC,OAAO,EAAE,iBAAiB,EAAE,mCAAmC,CAAC,CAAC,CAAC,CAAC;IAClH,CAAC;YACO,CAAC;QACP,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC;AACH,CAAC,CAAC","sourcesContent":["import { createScope, type Scope } from \"@openlv/core\";\nimport type { EventEmitter } from \"eventemitter3\";\n\nimport type { SessionMessageAck, SessionMessageFailed, SessionMessageResponse } from \"./index.js\";\n\n/** The peer's handler threw. The peer sends no detail, so there is none here. */\nexport class RequestFailedError extends Error {\n override name = \"RequestFailedError\";\n\n constructor() {\n super(\"The remote peer failed to handle the request\");\n }\n}\n\nexport type CorrelationEvents = {\n message: SessionMessageAck | SessionMessageFailed | SessionMessageResponse;\n /** The session ended; every open request fails with `reason`. */\n ended: (reason: string) => void;\n};\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 = async (\n messages: EventEmitter<CorrelationEvents>,\n messageId: string,\n ackTimeoutMs: number,\n responseTimeoutMs: number,\n): Promise<unknown> => {\n const request = createScope();\n // Short window for the ack -- tells us the peer is alive and processing.\n const ackWait = request.child();\n\n const timeout = (scope: Scope, delayMs: number, reason: string) => new Promise<never>((_resolve, reject) => {\n scope.timeout(delayMs, () => reject(new Error(`Request timed out: ${reason}`)));\n });\n const acknowledged = new Promise<void>((resolve) => {\n ackWait.listen(messages, \"message\", (message: CorrelationEvents[\"message\"]) => {\n if (message.messageId === messageId && message.type === \"ack\") resolve();\n });\n });\n const response = new Promise<unknown>((resolve, reject) => {\n request.listen(messages, \"ended\", (reason: string) => reject(new Error(reason)));\n request.listen(messages, \"message\", (message: CorrelationEvents[\"message\"]) => {\n if (message.messageId !== messageId) return;\n\n if (message.type === \"failed\") reject(new RequestFailedError());\n else if (message.type === \"response\") resolve(message.payload);\n });\n });\n\n try {\n await Promise.race([acknowledged, response, timeout(ackWait, ackTimeoutMs, \"remote peer did not acknowledge\")]);\n await ackWait.close();\n\n return await Promise.race([response, timeout(request, responseTimeoutMs, \"no response after acknowledgement\")]);\n }\n finally {\n await request.close();\n }\n};\n"]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { EventEmitter } from "eventemitter3";
|
|
2
|
+
import { describe, expect, test } from "vitest";
|
|
3
|
+
import { awaitCorrelatedResponse, RequestFailedError } from "./correlate.js";
|
|
4
|
+
describe("awaitCorrelatedResponse", () => {
|
|
5
|
+
test.each([
|
|
6
|
+
["before any ack", false],
|
|
7
|
+
["after the ack", true],
|
|
8
|
+
])("rejects at once when the peer reports failure %s", async (_name, hasAck) => {
|
|
9
|
+
const messages = new EventEmitter();
|
|
10
|
+
const pending = awaitCorrelatedResponse(messages, "request-1", 60000, 60000);
|
|
11
|
+
if (hasAck)
|
|
12
|
+
messages.emit("message", { type: "ack", messageId: "request-1" });
|
|
13
|
+
messages.emit("message", { type: "failed", messageId: "another-request" });
|
|
14
|
+
messages.emit("message", { type: "failed", messageId: "request-1" });
|
|
15
|
+
await expect(pending).rejects.toBeInstanceOf(RequestFailedError);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
//# sourceMappingURL=correlate.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"correlate.test.js","sourceRoot":"","sources":["../../src/messages/correlate.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAEhD,OAAO,EAAE,uBAAuB,EAA0B,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAErG,QAAQ,CAAC,yBAAyB,EAAE,GAAG,EAAE;IACvC,IAAI,CAAC,IAAI,CAAC;QACR,CAAC,gBAAgB,EAAE,KAAK,CAAC;QACzB,CAAC,eAAe,EAAE,IAAI,CAAC;KACxB,CAAC,CAAC,kDAAkD,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;QAC7E,MAAM,QAAQ,GAAG,IAAI,YAAY,EAAqB,CAAC;QACvD,MAAM,OAAO,GAAG,uBAAuB,CAAC,QAAQ,EAAE,WAAW,EAAE,KAAM,EAAE,KAAM,CAAC,CAAC;QAE/E,IAAI,MAAM;YAAE,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC,CAAC;QAE9E,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,iBAAiB,EAAE,CAAC,CAAC;QAC3E,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC,CAAC;QAErE,MAAM,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,cAAc,CAAC,kBAAkB,CAAC,CAAC;IACnE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import { EventEmitter } from \"eventemitter3\";\nimport { describe, expect, test } from \"vitest\";\n\nimport { awaitCorrelatedResponse, type CorrelationEvents, RequestFailedError } from \"./correlate.js\";\n\ndescribe(\"awaitCorrelatedResponse\", () => {\n test.each([\n [\"before any ack\", false],\n [\"after the ack\", true],\n ])(\"rejects at once when the peer reports failure %s\", async (_name, hasAck) => {\n const messages = new EventEmitter<CorrelationEvents>();\n const pending = awaitCorrelatedResponse(messages, \"request-1\", 60_000, 60_000);\n\n if (hasAck) messages.emit(\"message\", { type: \"ack\", messageId: \"request-1\" });\n\n messages.emit(\"message\", { type: \"failed\", messageId: \"another-request\" });\n messages.emit(\"message\", { type: \"failed\", messageId: \"request-1\" });\n\n await expect(pending).rejects.toBeInstanceOf(RequestFailedError);\n });\n});\n"]}
|
package/dist/messages/index.d.ts
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
|
-
export type SessionMessage = SessionMessageRequest | SessionMessageResponse | SessionMessageAck;
|
|
1
|
+
export type SessionMessage = SessionMessageRequest | SessionMessageResponse | SessionMessageAck | SessionMessageFailed | SessionMessageClose;
|
|
2
2
|
export type SessionMessageRequest = {
|
|
3
3
|
type: "request";
|
|
4
4
|
messageId: string;
|
|
5
|
-
payload:
|
|
5
|
+
payload: unknown;
|
|
6
6
|
};
|
|
7
7
|
export type SessionMessageResponse = {
|
|
8
8
|
type: "response";
|
|
9
9
|
messageId: string;
|
|
10
|
-
payload:
|
|
10
|
+
payload: unknown;
|
|
11
11
|
};
|
|
12
12
|
export type SessionMessageAck = {
|
|
13
13
|
type: "ack";
|
|
14
14
|
messageId: string;
|
|
15
15
|
};
|
|
16
|
+
/** The request's handler threw; carries no detail on purpose. */
|
|
17
|
+
export type SessionMessageFailed = {
|
|
18
|
+
type: "failed";
|
|
19
|
+
messageId: string;
|
|
20
|
+
};
|
|
21
|
+
export type SessionMessageClose = {
|
|
22
|
+
type: "close";
|
|
23
|
+
};
|
|
24
|
+
export declare const isSessionMessage: (message: object) => message is SessionMessage;
|
package/dist/messages/index.js
CHANGED
|
@@ -1,2 +1,12 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export const isSessionMessage = (message) => {
|
|
2
|
+
if (!("type" in message) || typeof message.type !== "string")
|
|
3
|
+
return false;
|
|
4
|
+
if (message.type === "close")
|
|
5
|
+
return true;
|
|
6
|
+
if (!("messageId" in message) || typeof message.messageId !== "string")
|
|
7
|
+
return false;
|
|
8
|
+
return message.type === "ack"
|
|
9
|
+
|| message.type === "failed"
|
|
10
|
+
|| ((message.type === "request" || message.type === "response") && "payload" in message);
|
|
11
|
+
};
|
|
2
12
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/messages/index.ts"],"names":[],"mappings":"","sourcesContent":["export type SessionMessage\n = | SessionMessageRequest\n | SessionMessageResponse\n | SessionMessageAck;\n\nexport type SessionMessageRequest = {\n type: \"request\";\n messageId: string;\n payload:
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/messages/index.ts"],"names":[],"mappings":"AAkCA,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,OAAe,EAA6B,EAAE;IAC7E,IAAI,CAAC,CAAC,MAAM,IAAI,OAAO,CAAC,IAAI,OAAO,OAAO,CAAC,IAAI,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAE3E,IAAI,OAAO,CAAC,IAAI,KAAK,OAAO;QAAE,OAAO,IAAI,CAAC;IAE1C,IAAI,CAAC,CAAC,WAAW,IAAI,OAAO,CAAC,IAAI,OAAO,OAAO,CAAC,SAAS,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAErF,OAAO,OAAO,CAAC,IAAI,KAAK,KAAK;WACxB,OAAO,CAAC,IAAI,KAAK,QAAQ;WACzB,CAAC,CAAC,OAAO,CAAC,IAAI,KAAK,SAAS,IAAI,OAAO,CAAC,IAAI,KAAK,UAAU,CAAC,IAAI,SAAS,IAAI,OAAO,CAAC,CAAC;AAC7F,CAAC,CAAC","sourcesContent":["export type SessionMessage\n = | SessionMessageRequest\n | SessionMessageResponse\n | SessionMessageAck\n | SessionMessageFailed\n | SessionMessageClose;\n\nexport type SessionMessageRequest = {\n type: \"request\";\n messageId: string;\n payload: unknown;\n};\n\nexport type SessionMessageResponse = {\n type: \"response\";\n messageId: string;\n payload: unknown;\n};\n\nexport type SessionMessageAck = {\n type: \"ack\";\n messageId: string;\n};\n\n/** The request's handler threw; carries no detail on purpose. */\nexport type SessionMessageFailed = {\n type: \"failed\";\n messageId: string;\n};\n\nexport type SessionMessageClose = {\n type: \"close\";\n};\n\nexport const isSessionMessage = (message: object): message is SessionMessage => {\n if (!(\"type\" in message) || typeof message.type !== \"string\") return false;\n\n if (message.type === \"close\") return true;\n\n if (!(\"messageId\" in message) || typeof message.messageId !== \"string\") return false;\n\n return message.type === \"ack\"\n || message.type === \"failed\"\n || ((message.type === \"request\" || message.type === \"response\") && \"payload\" in message);\n};\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { isSessionMessage } from "./index.js";
|
|
3
|
+
describe("isSessionMessage", () => {
|
|
4
|
+
it("accepts every session message type", () => {
|
|
5
|
+
expect(isSessionMessage({ type: "request", messageId: "1", payload: null })).toBe(true);
|
|
6
|
+
expect(isSessionMessage({ type: "response", messageId: "1", payload: "0x1" })).toBe(true);
|
|
7
|
+
expect(isSessionMessage({ type: "ack", messageId: "1" })).toBe(true);
|
|
8
|
+
expect(isSessionMessage({ type: "close" })).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
it("rejects messages without a message id or payload", () => {
|
|
11
|
+
expect(isSessionMessage({ type: "ack" })).toBe(false);
|
|
12
|
+
expect(isSessionMessage({ type: "request", messageId: "1" })).toBe(false);
|
|
13
|
+
expect(isSessionMessage({ type: "response", messageId: 1, payload: {} })).toBe(false);
|
|
14
|
+
expect(isSessionMessage({ type: "unknown", messageId: "1", payload: {} })).toBe(false);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
//# sourceMappingURL=index.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.test.js","sourceRoot":"","sources":["../../src/messages/index.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE9C,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,MAAM,CAAC,gBAAgB,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACxF,MAAM,CAAC,gBAAgB,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC1F,MAAM,CAAC,gBAAgB,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACrE,MAAM,CAAC,gBAAgB,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACzD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAC1D,MAAM,CAAC,gBAAgB,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACtD,MAAM,CAAC,gBAAgB,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC1E,MAAM,CAAC,gBAAgB,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACtF,MAAM,CAAC,gBAAgB,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACzF,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import { describe, expect, it } from \"vitest\";\n\nimport { isSessionMessage } from \"./index.js\";\n\ndescribe(\"isSessionMessage\", () => {\n it(\"accepts every session message type\", () => {\n expect(isSessionMessage({ type: \"request\", messageId: \"1\", payload: null })).toBe(true);\n expect(isSessionMessage({ type: \"response\", messageId: \"1\", payload: \"0x1\" })).toBe(true);\n expect(isSessionMessage({ type: \"ack\", messageId: \"1\" })).toBe(true);\n expect(isSessionMessage({ type: \"close\" })).toBe(true);\n });\n\n it(\"rejects messages without a message id or payload\", () => {\n expect(isSessionMessage({ type: \"ack\" })).toBe(false);\n expect(isSessionMessage({ type: \"request\", messageId: \"1\" })).toBe(false);\n expect(isSessionMessage({ type: \"response\", messageId: 1, payload: {} })).toBe(false);\n expect(isSessionMessage({ type: \"unknown\", messageId: \"1\", payload: {} })).toBe(false);\n });\n});\n"]}
|
package/dist/options.test.js
CHANGED
|
@@ -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,
|
|
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,
|
|
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,
|
|
18
|
+
await expect(createSession(linkParameters, [webrtc()], onMessage, {
|
|
23
19
|
info: { identity: "", name: "Example dApp" },
|
|
24
20
|
})).rejects.toThrow(/identity/);
|
|
25
21
|
});
|
package/dist/options.test.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"options.test.js","sourceRoot":"","sources":["../src/options.test.ts"],"names":[],"mappings":"
|
|
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
|
+
"version": "0.3.1",
|
|
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.
|
|
41
|
-
"@openlv/signaling": "0.1
|
|
42
|
-
"@openlv/transport": "0.
|
|
40
|
+
"@openlv/core": "0.3.0",
|
|
41
|
+
"@openlv/signaling": "0.3.1",
|
|
42
|
+
"@openlv/transport": "0.3.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@vitest/browser-playwright": "^4.0.8",
|
|
46
|
-
"playwright": "1.
|
|
46
|
+
"playwright": "1.60.0",
|
|
47
47
|
"typescript": "^5",
|
|
48
48
|
"vitest": "^4"
|
|
49
49
|
},
|
package/dist/transports.d.ts
DELETED
|
@@ -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;
|
package/dist/transports.js
DELETED
|
@@ -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
|
package/dist/transports.js.map
DELETED
|
@@ -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"]}
|
package/dist/transports.test.js
DELETED
|
@@ -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"]}
|
|
File without changes
|