@openlv/transport 0.0.2 → 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/base.test.d.ts +1 -0
- package/dist/base.test.js +94 -0
- package/dist/base.test.js.map +1 -0
- package/dist/index.d.ts +21 -18
- package/dist/index.js +79 -65
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +2 -2
- package/dist/index.test.js.map +1 -1
- package/dist/utils/log.d.ts +1 -1
- package/dist/utils/log.js +2 -5
- package/dist/utils/log.js.map +1 -1
- package/dist/webrtc/index.d.ts +2 -2
- package/dist/webrtc/index.js +75 -30
- package/dist/webrtc/index.js.map +1 -1
- package/package.json +4 -10
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { generateKeyPair } from "@openlv/core/encryption";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { createTransportBase, TRANSPORT_STATE, } from "./index.js";
|
|
4
|
+
const createFakeTransport = () => {
|
|
5
|
+
let emitter;
|
|
6
|
+
const sent = [];
|
|
7
|
+
const factory = createTransportBase("fake", (parameters) => {
|
|
8
|
+
emitter = parameters.emitter;
|
|
9
|
+
return {
|
|
10
|
+
setup: () => { },
|
|
11
|
+
teardown: () => { },
|
|
12
|
+
handle: async () => { },
|
|
13
|
+
send: async (message) => {
|
|
14
|
+
sent.push(message);
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
return {
|
|
19
|
+
factory,
|
|
20
|
+
sent,
|
|
21
|
+
getEmitter: () => {
|
|
22
|
+
if (!emitter)
|
|
23
|
+
throw new Error("transport not created");
|
|
24
|
+
return emitter;
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
describe("createTransportBase", () => {
|
|
29
|
+
it("exposes the wire id on the factory and instance", () => {
|
|
30
|
+
const { factory } = createFakeTransport();
|
|
31
|
+
expect(factory.transportId).toBe("fake");
|
|
32
|
+
});
|
|
33
|
+
it("relays negotiate events through subsend", async () => {
|
|
34
|
+
const { encryptionKey, decryptionKey } = await generateKeyPair();
|
|
35
|
+
const { factory, getEmitter } = createFakeTransport();
|
|
36
|
+
const relayed = [];
|
|
37
|
+
const layer = factory.create({
|
|
38
|
+
encrypt: encryptionKey.encrypt,
|
|
39
|
+
decrypt: decryptionKey.decrypt,
|
|
40
|
+
isHost: true,
|
|
41
|
+
onmessage: () => { },
|
|
42
|
+
subsend: async (message) => {
|
|
43
|
+
relayed.push(message);
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
expect(layer.type).toBe("fake");
|
|
47
|
+
await layer.setup();
|
|
48
|
+
getEmitter().emit("negotiate", { type: "offer", payload: "sdp" });
|
|
49
|
+
getEmitter().emit("negotiate", { type: "candidate", payload: "cand" });
|
|
50
|
+
await Promise.resolve();
|
|
51
|
+
expect(relayed).toEqual([
|
|
52
|
+
{ type: "offer", payload: "sdp" },
|
|
53
|
+
{ type: "candidate", payload: "cand" },
|
|
54
|
+
]);
|
|
55
|
+
});
|
|
56
|
+
it("encrypts outgoing messages and decrypts incoming ones", async () => {
|
|
57
|
+
const { encryptionKey, decryptionKey } = await generateKeyPair();
|
|
58
|
+
const { factory, sent, getEmitter } = createFakeTransport();
|
|
59
|
+
const received = [];
|
|
60
|
+
const layer = factory.create({
|
|
61
|
+
encrypt: encryptionKey.encrypt,
|
|
62
|
+
decrypt: decryptionKey.decrypt,
|
|
63
|
+
isHost: true,
|
|
64
|
+
onmessage: (message) => {
|
|
65
|
+
received.push(message);
|
|
66
|
+
},
|
|
67
|
+
subsend: async () => { },
|
|
68
|
+
});
|
|
69
|
+
await layer.setup();
|
|
70
|
+
getEmitter().emit("connected");
|
|
71
|
+
await layer.waitFor(TRANSPORT_STATE.CONNECTED);
|
|
72
|
+
await layer.send({ type: "request", messageId: "1", payload: { a: 1 } });
|
|
73
|
+
expect(sent).toHaveLength(1);
|
|
74
|
+
// Ciphertext on the wire, not the plaintext envelope.
|
|
75
|
+
expect(sent[0]).not.toContain("messageId");
|
|
76
|
+
getEmitter().emit("message", sent[0]);
|
|
77
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
78
|
+
expect(received).toEqual([{ type: "request", messageId: "1", payload: { a: 1 } }]);
|
|
79
|
+
});
|
|
80
|
+
it("rejects sends before the transport is connected", async () => {
|
|
81
|
+
const { encryptionKey, decryptionKey } = await generateKeyPair();
|
|
82
|
+
const { factory } = createFakeTransport();
|
|
83
|
+
const layer = factory.create({
|
|
84
|
+
encrypt: encryptionKey.encrypt,
|
|
85
|
+
decrypt: decryptionKey.decrypt,
|
|
86
|
+
isHost: true,
|
|
87
|
+
onmessage: () => { },
|
|
88
|
+
subsend: async () => { },
|
|
89
|
+
});
|
|
90
|
+
await layer.setup();
|
|
91
|
+
await expect(layer.send({ a: 1 })).rejects.toThrow("Transport not connected");
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
//# sourceMappingURL=base.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base.test.js","sourceRoot":"","sources":["../src/base.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { generateKeyPair } from \"@openlv/core/encryption\";\nimport { describe, expect, it } from \"vitest\";\n\nimport {\n createTransportBase,\n TRANSPORT_STATE,\n type TransportLayerBaseEmitter,\n type TransportMessage,\n} from \"./index.js\";\n\nconst createFakeTransport = () => {\n let emitter: TransportLayerBaseEmitter | undefined;\n const sent: string[] = [];\n\n const factory = createTransportBase(\"fake\", (parameters) => {\n emitter = parameters.emitter;\n\n return {\n setup: () => {},\n teardown: () => {},\n handle: async () => {},\n send: async (message) => {\n sent.push(message);\n },\n };\n });\n\n return {\n factory,\n sent,\n getEmitter: (): TransportLayerBaseEmitter => {\n if (!emitter) throw new Error(\"transport not created\");\n\n return emitter;\n },\n };\n};\n\ndescribe(\"createTransportBase\", () => {\n it(\"exposes the wire id on the factory and instance\", () => {\n const { factory } = createFakeTransport();\n\n expect(factory.transportId).toBe(\"fake\");\n });\n\n it(\"relays negotiate events through subsend\", async () => {\n const { encryptionKey, decryptionKey } = await generateKeyPair();\n const { factory, getEmitter } = createFakeTransport();\n const relayed: TransportMessage[] = [];\n\n const layer = factory.create({\n encrypt: encryptionKey.encrypt,\n decrypt: decryptionKey.decrypt,\n isHost: true,\n onmessage: () => {},\n subsend: async (message) => {\n relayed.push(message);\n },\n });\n\n expect(layer.type).toBe(\"fake\");\n await layer.setup();\n\n getEmitter().emit(\"negotiate\", { type: \"offer\", payload: \"sdp\" });\n getEmitter().emit(\"negotiate\", { type: \"candidate\", payload: \"cand\" });\n await Promise.resolve();\n\n expect(relayed).toEqual([\n { type: \"offer\", payload: \"sdp\" },\n { type: \"candidate\", payload: \"cand\" },\n ]);\n });\n\n it(\"encrypts outgoing messages and decrypts incoming ones\", async () => {\n const { encryptionKey, decryptionKey } = await generateKeyPair();\n const { factory, sent, getEmitter } = createFakeTransport();\n const received: object[] = [];\n\n const layer = factory.create({\n encrypt: encryptionKey.encrypt,\n decrypt: decryptionKey.decrypt,\n isHost: true,\n onmessage: (message) => {\n received.push(message);\n },\n subsend: async () => {},\n });\n\n await layer.setup();\n getEmitter().emit(\"connected\");\n await layer.waitFor(TRANSPORT_STATE.CONNECTED);\n\n await layer.send({ type: \"request\", messageId: \"1\", payload: { a: 1 } });\n expect(sent).toHaveLength(1);\n // Ciphertext on the wire, not the plaintext envelope.\n expect(sent[0]).not.toContain(\"messageId\");\n\n getEmitter().emit(\"message\", sent[0]);\n await new Promise(resolve => setTimeout(resolve, 10));\n expect(received).toEqual([{ type: \"request\", messageId: \"1\", payload: { a: 1 } }]);\n });\n\n it(\"rejects sends before the transport is connected\", async () => {\n const { encryptionKey, decryptionKey } = await generateKeyPair();\n const { factory } = createFakeTransport();\n\n const layer = factory.create({\n encrypt: encryptionKey.encrypt,\n decrypt: decryptionKey.decrypt,\n isHost: true,\n onmessage: () => {},\n subsend: async () => {},\n });\n\n await layer.setup();\n await expect(layer.send({ a: 1 })).rejects.toThrow(\"Transport not connected\");\n });\n});\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -18,8 +18,9 @@ export declare const TRANSPORT_STATE: {
|
|
|
18
18
|
export type TransportState = (typeof TRANSPORT_STATE)[keyof typeof TRANSPORT_STATE];
|
|
19
19
|
export type TLayerEventMap = {
|
|
20
20
|
state_change: (state: TransportState) => void;
|
|
21
|
+
error: (reason?: string) => void;
|
|
21
22
|
};
|
|
22
|
-
export type
|
|
23
|
+
export type TransportLayerSetupParameters = {
|
|
23
24
|
isHost: boolean;
|
|
24
25
|
encrypt: EncryptionKey["encrypt"];
|
|
25
26
|
decrypt: DecryptionKey["decrypt"];
|
|
@@ -30,18 +31,17 @@ export type TransportLayerParameters = {
|
|
|
30
31
|
messageId: string;
|
|
31
32
|
}) => void;
|
|
32
33
|
};
|
|
34
|
+
/**
|
|
35
|
+
* A transport negotiation message, relayed over signaling before the direct
|
|
36
|
+
* connection exists. The shape of `type`/`payload` is owned by the selected
|
|
37
|
+
* transport; unknown types must be ignored, not fatal.
|
|
38
|
+
*/
|
|
33
39
|
export type TransportMessage = {
|
|
34
|
-
type:
|
|
35
|
-
payload: string;
|
|
36
|
-
} | {
|
|
37
|
-
type: "answer";
|
|
38
|
-
payload: string;
|
|
39
|
-
} | {
|
|
40
|
-
type: "candidate";
|
|
40
|
+
type: string;
|
|
41
41
|
payload: string;
|
|
42
42
|
};
|
|
43
43
|
export type TransportLayer = {
|
|
44
|
-
type:
|
|
44
|
+
type: TransportProtocol;
|
|
45
45
|
setup: () => MaybePromise<void>;
|
|
46
46
|
teardown: () => MaybePromise<void>;
|
|
47
47
|
send: (message: object) => Promise<void>;
|
|
@@ -49,31 +49,34 @@ export type TransportLayer = {
|
|
|
49
49
|
waitFor: (state: TransportState) => Promise<void>;
|
|
50
50
|
emitter: EventEmitter<TLayerEventMap>;
|
|
51
51
|
};
|
|
52
|
-
|
|
53
|
-
export type
|
|
54
|
-
export type
|
|
55
|
-
|
|
52
|
+
/** Wire identifier advertised in the `capabilities` handshake packet. */
|
|
53
|
+
export type TransportProtocol = "wrtc" | "ws" | (string & {});
|
|
54
|
+
export type TransportLayerFn = {
|
|
55
|
+
transportId: TransportProtocol;
|
|
56
|
+
create: (parameters: TransportLayerSetupParameters) => TransportLayer;
|
|
57
|
+
};
|
|
58
|
+
export type Transport = (config?: WebRTCConfig) => TransportLayerFn;
|
|
59
|
+
export type TransportLayerImpl = {
|
|
56
60
|
setup: () => MaybePromise<void>;
|
|
57
61
|
teardown: () => MaybePromise<void>;
|
|
58
62
|
handle: (message: TransportMessage) => Promise<void>;
|
|
59
63
|
send: (message: string) => Promise<void>;
|
|
60
64
|
};
|
|
61
65
|
export type TransportLayerBaseEventMap = {
|
|
62
|
-
|
|
63
|
-
answer: (answer: string) => void;
|
|
64
|
-
candidate: (candidate: string) => void;
|
|
66
|
+
negotiate: (message: TransportMessage) => void;
|
|
65
67
|
connected: () => void;
|
|
66
68
|
message: (message: string) => void;
|
|
69
|
+
error: (reason?: string) => void;
|
|
67
70
|
};
|
|
68
71
|
export type TransportLayerBaseEmitter = EventEmitter<TransportLayerBaseEventMap>;
|
|
69
72
|
export type TransportLayerBaseParameters = {
|
|
70
73
|
emitter: TransportLayerBaseEmitter;
|
|
71
74
|
isHost: boolean;
|
|
72
75
|
};
|
|
73
|
-
export type
|
|
76
|
+
export type TransportLayerImplFn = (parameters: TransportLayerBaseParameters) => TransportLayerImpl;
|
|
74
77
|
/**
|
|
75
78
|
* Base Transport Layer implementation
|
|
76
79
|
*
|
|
77
80
|
* https://openlv.sh/api/transport
|
|
78
81
|
*/
|
|
79
|
-
export declare const createTransportBase: (init:
|
|
82
|
+
export declare const createTransportBase: (transportId: TransportProtocol, init: TransportLayerImplFn) => TransportLayerFn;
|
package/dist/index.js
CHANGED
|
@@ -12,70 +12,84 @@ export const TRANSPORT_STATE = {
|
|
|
12
12
|
*
|
|
13
13
|
* https://openlv.sh/api/transport
|
|
14
14
|
*/
|
|
15
|
-
export const createTransportBase = (init) => ({
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
state =
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
internalEmitter.on("answer", (answer) => {
|
|
28
|
-
log("onAnswer", answer);
|
|
29
|
-
subsend({ type: "answer", payload: answer });
|
|
30
|
-
});
|
|
31
|
-
internalEmitter.on("candidate", (candidate) => {
|
|
32
|
-
log("onCandidate", candidate);
|
|
33
|
-
subsend({ type: "candidate", payload: candidate });
|
|
34
|
-
});
|
|
35
|
-
internalEmitter.on("connected", () => {
|
|
36
|
-
log("onConnected");
|
|
37
|
-
setState(TRANSPORT_STATE.CONNECTED);
|
|
38
|
-
});
|
|
39
|
-
internalEmitter.on("message", async (message) => {
|
|
40
|
-
log("onMessage", message);
|
|
41
|
-
const data = await decrypt(message);
|
|
42
|
-
onmessage(JSON.parse(data));
|
|
43
|
-
});
|
|
44
|
-
const { setup, teardown, type, send: sendLayer, handle, } = init({
|
|
45
|
-
emitter: internalEmitter,
|
|
46
|
-
isHost,
|
|
47
|
-
});
|
|
48
|
-
const send = async (message) => {
|
|
49
|
-
if (state !== TRANSPORT_STATE.CONNECTED)
|
|
50
|
-
throw new Error("Transport not connected");
|
|
51
|
-
const payload = await encrypt(JSON.stringify(message));
|
|
52
|
-
await sendLayer(payload);
|
|
53
|
-
};
|
|
54
|
-
const waitFor = async (targetState) => {
|
|
55
|
-
if (state === targetState)
|
|
56
|
-
return;
|
|
57
|
-
return new Promise((resolve) => {
|
|
58
|
-
const handler = (newState) => {
|
|
59
|
-
if (newState === targetState) {
|
|
60
|
-
emitter.off("state_change", handler);
|
|
61
|
-
resolve();
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
emitter.on("state_change", handler);
|
|
15
|
+
export const createTransportBase = (transportId, init) => ({
|
|
16
|
+
transportId,
|
|
17
|
+
create: ({ encrypt, decrypt, subsend, isHost, onmessage }) => {
|
|
18
|
+
const emitter = new EventEmitter();
|
|
19
|
+
const internalEmitter = new EventEmitter();
|
|
20
|
+
let state = TRANSPORT_STATE.STANDBY;
|
|
21
|
+
const setState = (newState) => {
|
|
22
|
+
state = newState;
|
|
23
|
+
emitter.emit("state_change", newState);
|
|
24
|
+
};
|
|
25
|
+
internalEmitter.on("negotiate", (message) => {
|
|
26
|
+
subsend(message).catch(error => log("failed to relay negotiation message", error));
|
|
65
27
|
});
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
28
|
+
internalEmitter.on("connected", () => {
|
|
29
|
+
log("onConnected");
|
|
30
|
+
setState(TRANSPORT_STATE.CONNECTED);
|
|
31
|
+
});
|
|
32
|
+
internalEmitter.on("error", (reason) => {
|
|
33
|
+
log("transport error", reason);
|
|
34
|
+
// Surface the reason before the state flips so listeners reading
|
|
35
|
+
// state on state_change already see it.
|
|
36
|
+
emitter.emit("error", reason);
|
|
37
|
+
setState(TRANSPORT_STATE.ERROR);
|
|
38
|
+
});
|
|
39
|
+
internalEmitter.on("message", async (message) => {
|
|
40
|
+
// Peer data is untrusted until decrypted AND parsed; drop anything that
|
|
41
|
+
// fails either step rather than surfacing an unhandled rejection.
|
|
42
|
+
try {
|
|
43
|
+
const data = await decrypt(message);
|
|
44
|
+
onmessage(JSON.parse(data));
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
log("dropping undecryptable transport message", error);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
const { setup, teardown, send: sendLayer, handle, } = init({
|
|
51
|
+
emitter: internalEmitter,
|
|
52
|
+
isHost,
|
|
53
|
+
});
|
|
54
|
+
const send = async (message) => {
|
|
55
|
+
if (state !== TRANSPORT_STATE.CONNECTED)
|
|
56
|
+
throw new Error("Transport not connected");
|
|
57
|
+
const payload = await encrypt(JSON.stringify(message));
|
|
58
|
+
await sendLayer(payload);
|
|
59
|
+
};
|
|
60
|
+
const waitFor = async (targetState) => {
|
|
61
|
+
if (state === targetState)
|
|
62
|
+
return;
|
|
63
|
+
if (state === TRANSPORT_STATE.ERROR) {
|
|
64
|
+
throw new Error("Transport is in error state");
|
|
65
|
+
}
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
const handler = (newState) => {
|
|
68
|
+
if (newState === targetState) {
|
|
69
|
+
emitter.off("state_change", handler);
|
|
70
|
+
resolve();
|
|
71
|
+
}
|
|
72
|
+
else if (newState === TRANSPORT_STATE.ERROR) {
|
|
73
|
+
emitter.off("state_change", handler);
|
|
74
|
+
reject(new Error("Transport is in error state"));
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
emitter.on("state_change", handler);
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
return {
|
|
81
|
+
type: transportId,
|
|
82
|
+
async setup() {
|
|
83
|
+
setState(TRANSPORT_STATE.CONNECTING);
|
|
84
|
+
await setup();
|
|
85
|
+
setState(TRANSPORT_STATE.READY);
|
|
86
|
+
},
|
|
87
|
+
teardown,
|
|
88
|
+
handle,
|
|
89
|
+
send,
|
|
90
|
+
waitFor,
|
|
91
|
+
emitter,
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
});
|
|
81
95
|
//# 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":"AAOA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAG7C,OAAO,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAC;AAGrC,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,OAAO,EAAE,SAAS;IAClB,UAAU,EAAE,YAAY;IACxB,KAAK,EAAE,OAAO;IACd,SAAS,EAAE,WAAW;IACtB,KAAK,EAAE,OAAO;CACN,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Open Lavatory Protocol - Decentralized Wallet Connection Library\n *\n * A privacy-first, peer-to-peer protocol for connecting dApps with wallets\n * without relying on centralized infrastructure.\n */\nimport type { DecryptionKey, EncryptionKey } from \"@openlv/core/encryption\";\nimport { EventEmitter } from \"eventemitter3\";\nimport type { MaybePromise } from \"viem\";\n\nimport { log } from \"./utils/log.js\";\nimport type { WebRTCConfig } from \"./webrtc/index.js\";\n\nexport const TRANSPORT_STATE = {\n STANDBY: \"standby\",\n CONNECTING: \"connecting\",\n READY: \"ready\",\n CONNECTED: \"connected\",\n ERROR: \"error\",\n} as const;\nexport type TransportState =\n (typeof TRANSPORT_STATE)[keyof typeof TRANSPORT_STATE];\n\nexport type TLayerEventMap = {\n state_change: (state: TransportState) => void;\n error: (reason?: string) => void;\n};\n\nexport type TransportLayerSetupParameters = {\n isHost: boolean;\n encrypt: EncryptionKey[\"encrypt\"];\n decrypt: DecryptionKey[\"decrypt\"];\n subsend: (message: TransportMessage) => Promise<void>;\n onmessage: (message: { type: string; payload: object; messageId: string; }) => void;\n};\n\n/**\n * A transport negotiation message, relayed over signaling before the direct\n * connection exists. The shape of `type`/`payload` is owned by the selected\n * transport; unknown types must be ignored, not fatal.\n */\nexport type TransportMessage = {\n type: string;\n payload: string;\n};\nexport type TransportLayer = {\n type: TransportProtocol;\n setup: () => MaybePromise<void>;\n teardown: () => MaybePromise<void>;\n send: (message: object) => Promise<void>;\n handle: (message: TransportMessage) => Promise<void>;\n waitFor: (state: TransportState) => Promise<void>;\n emitter: EventEmitter<TLayerEventMap>;\n};\n/** Wire identifier advertised in the `capabilities` handshake packet. */\nexport type TransportProtocol = \"wrtc\" | \"ws\" | (string & {});\nexport type TransportLayerFn = {\n transportId: TransportProtocol;\n create: (parameters: TransportLayerSetupParameters) => TransportLayer;\n};\nexport type Transport = (config?: WebRTCConfig) => TransportLayerFn;\n\nexport type TransportLayerImpl = {\n setup: () => MaybePromise<void>;\n teardown: () => MaybePromise<void>;\n handle: (message: TransportMessage) => Promise<void>;\n send: (message: string) => Promise<void>;\n};\nexport type TransportLayerBaseEventMap = {\n negotiate: (message: TransportMessage) => void;\n connected: () => void;\n message: (message: string) => void;\n error: (reason?: string) => void;\n};\nexport type TransportLayerBaseEmitter =\n EventEmitter<TransportLayerBaseEventMap>;\nexport type TransportLayerBaseParameters = {\n emitter: TransportLayerBaseEmitter;\n isHost: boolean;\n};\nexport type TransportLayerImplFn = (\n parameters: TransportLayerBaseParameters,\n) => TransportLayerImpl;\n\n/**\n * Base Transport Layer implementation\n *\n * https://openlv.sh/api/transport\n */\nexport const createTransportBase = (\n transportId: TransportProtocol,\n init: TransportLayerImplFn,\n): TransportLayerFn => ({\n transportId,\n create: ({ encrypt, decrypt, subsend, isHost, onmessage }) => {\n const emitter = new EventEmitter<TLayerEventMap>();\n const internalEmitter = new EventEmitter<TransportLayerBaseEventMap>();\n let state: TransportState = TRANSPORT_STATE.STANDBY;\n\n const setState = (newState: TransportState) => {\n state = newState;\n emitter.emit(\"state_change\", newState);\n };\n\n internalEmitter.on(\"negotiate\", (message) => {\n subsend(message).catch(error => log(\"failed to relay negotiation message\", error));\n });\n internalEmitter.on(\"connected\", () => {\n log(\"onConnected\");\n setState(TRANSPORT_STATE.CONNECTED);\n });\n internalEmitter.on(\"error\", (reason) => {\n log(\"transport error\", reason);\n // Surface the reason before the state flips so listeners reading\n // state on state_change already see it.\n emitter.emit(\"error\", reason);\n setState(TRANSPORT_STATE.ERROR);\n });\n internalEmitter.on(\"message\", async (message) => {\n // Peer data is untrusted until decrypted AND parsed; drop anything that\n // fails either step rather than surfacing an unhandled rejection.\n try {\n const data = await decrypt(message);\n\n onmessage(JSON.parse(data) as { type: string; payload: object; messageId: string; });\n }\n catch (error) {\n log(\"dropping undecryptable transport message\", error);\n }\n });\n\n const {\n setup,\n teardown,\n send: sendLayer,\n handle,\n } = init({\n emitter: internalEmitter,\n isHost,\n });\n\n const send = async (message: object) => {\n if (state !== TRANSPORT_STATE.CONNECTED)\n throw new Error(\"Transport not connected\");\n\n const payload = await encrypt(JSON.stringify(message));\n\n await sendLayer(payload);\n };\n\n const waitFor = async (targetState: TransportState) => {\n if (state === targetState) return;\n\n if (state === TRANSPORT_STATE.ERROR) {\n throw new Error(\"Transport is in error state\");\n }\n\n return new Promise<void>((resolve, reject) => {\n const handler = (newState: TransportState) => {\n if (newState === targetState) {\n emitter.off(\"state_change\", handler);\n resolve();\n }\n else if (newState === TRANSPORT_STATE.ERROR) {\n emitter.off(\"state_change\", handler);\n reject(new Error(\"Transport is in error state\"));\n }\n };\n\n emitter.on(\"state_change\", handler);\n });\n };\n\n return {\n type: transportId,\n async setup() {\n setState(TRANSPORT_STATE.CONNECTING);\n await setup();\n setState(TRANSPORT_STATE.READY);\n },\n teardown,\n handle,\n send,\n waitFor,\n emitter,\n };\n },\n});\n"]}
|
package/dist/index.test.js
CHANGED
|
@@ -10,7 +10,7 @@ describe("Transport", () => {
|
|
|
10
10
|
const { decrypt } = decryptionKey;
|
|
11
11
|
const signalA = new EventEmitter();
|
|
12
12
|
const signalB = new EventEmitter();
|
|
13
|
-
const transportA = webrtc()({
|
|
13
|
+
const transportA = webrtc().create({
|
|
14
14
|
encrypt,
|
|
15
15
|
decrypt,
|
|
16
16
|
subsend: async (m) => {
|
|
@@ -21,7 +21,7 @@ describe("Transport", () => {
|
|
|
21
21
|
signalA.emit("message", m);
|
|
22
22
|
},
|
|
23
23
|
});
|
|
24
|
-
const transportB = webrtc()({
|
|
24
|
+
const transportB = webrtc().create({
|
|
25
25
|
encrypt,
|
|
26
26
|
decrypt,
|
|
27
27
|
subsend: async (m) => {
|
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,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAE3C,QAAQ,CAAC,WAAW,EAAE,GAAG,EAAE;IACzB,EAAE,CAAC,sCAAsC,EAAE,KAAK,IAAI,EAAE;QACpD,MAAM,EAAE,aAAa,EAAE,SAAS,EAAE,aAAa,EAAE,GAAG,MAAM,eAAe,EAAE,CAAC;QAC5E,MAAM,EAAE,OAAO,EAAE,GAAG,SAAS,CAAC;QAC9B,MAAM,EAAE,OAAO,EAAE,GAAG,aAAa,CAAC;QAClC,MAAM,OAAO,GAAG,IAAI,YAAY,EAAwC,CAAC;QACzE,MAAM,OAAO,GAAG,IAAI,YAAY,EAAwC,CAAC;QAEzE,MAAM,UAAU,GAAG,MAAM,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAE3C,QAAQ,CAAC,WAAW,EAAE,GAAG,EAAE;IACzB,EAAE,CAAC,sCAAsC,EAAE,KAAK,IAAI,EAAE;QACpD,MAAM,EAAE,aAAa,EAAE,SAAS,EAAE,aAAa,EAAE,GAAG,MAAM,eAAe,EAAE,CAAC;QAC5E,MAAM,EAAE,OAAO,EAAE,GAAG,SAAS,CAAC;QAC9B,MAAM,EAAE,OAAO,EAAE,GAAG,aAAa,CAAC;QAClC,MAAM,OAAO,GAAG,IAAI,YAAY,EAAwC,CAAC;QACzE,MAAM,OAAO,GAAG,IAAI,YAAY,EAAwC,CAAC;QAEzE,MAAM,UAAU,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC;YACjC,OAAO;YACP,OAAO;YACP,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;gBACnB,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;YAC5B,CAAC;YACD,MAAM,EAAE,IAAI;YACZ,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE;gBACf,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC;YAC7B,CAAC;SACF,CAAC,CAAC;QACH,MAAM,UAAU,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC;YACjC,OAAO;YACP,OAAO;YACP,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;gBACnB,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;YAC5B,CAAC;YACD,MAAM,EAAE,KAAK;YACb,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE;gBACf,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC;YAC7B,CAAC;SACF,CAAC,CAAC;QAEH,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE;YACzB,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC;YAC3B,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACvB,CAAC,CAAC,CAAC;QACH,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE;YACzB,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC;YAC3B,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACvB,CAAC,CAAC,CAAC;QAEH,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;QAC3B,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,UAAU,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;QAE5D,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC;QACvC,MAAM,OAAO,CAAC,GAAG,CAAC;YAChB,UAAU,CAAC,OAAO,CAAC,eAAe,CAAC,SAAS,CAAC;YAC7C,UAAU,CAAC,OAAO,CAAC,eAAe,CAAC,SAAS,CAAC;SAC9C,CAAC,CAAC;QAEH,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;QAE/B,MAAM,iBAAiB,GAAG,IAAI,OAAO,CAAS,CAAC,OAAO,EAAE,EAAE;YACxD,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE;gBAC1B,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC;gBAC3B,OAAO,CAAC,CAAC,CAAC,CAAC;YACb,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QAEH,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC;QACvC,MAAM,UAAU,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC;QAE5C,OAAO,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAC;QACxC,MAAM,UAAU,GAAG,MAAM,iBAAiB,CAAC;QAE3C,MAAM,CAAC,UAAU,CAAC,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC;QAEjD,OAAO,CAAC,GAAG,CAAC,oBAAoB,EAAE,UAAU,CAAC,CAAC;QAC9C,MAAM,iBAAiB,GAAG,IAAI,OAAO,CAAS,CAAC,OAAO,EAAE,EAAE;YACxD,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE;gBAC1B,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC;gBAC3B,OAAO,CAAC,CAAC,CAAC,CAAC;YACb,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QAEH,MAAM,UAAU,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC;QAC5C,MAAM,UAAU,GAAG,MAAM,iBAAiB,CAAC;QAE3C,MAAM,CAAC,UAAU,CAAC,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC;QAEjD,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,UAAU,CAAC,QAAQ,EAAE,EAAE,UAAU,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;QAElE,MAAM,CAAC,UAAU,CAAC,CAAC,WAAW,EAAE,CAAC;QACjC,MAAM,CAAC,UAAU,CAAC,CAAC,WAAW,EAAE,CAAC;IACnC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import { generateKeyPair } from \"@openlv/core/encryption\";\nimport { EventEmitter } from \"eventemitter3\";\nimport { describe, expect, it } from \"vitest\";\n\nimport { TRANSPORT_STATE } from \"./index.js\";\nimport { webrtc } from \"./webrtc/index.js\";\n\ndescribe(\"Transport\", () => {\n it(\"should be able to create a transport\", async () => {\n const { encryptionKey: publicKey, decryptionKey } = await generateKeyPair();\n const { encrypt } = publicKey;\n const { decrypt } = decryptionKey;\n const signalA = new EventEmitter<{ signal: string; message: object; }>();\n const signalB = new EventEmitter<{ signal: string; message: object; }>();\n\n const transportA = webrtc().create({\n encrypt,\n decrypt,\n subsend: async (m) => {\n signalA.emit(\"signal\", m);\n },\n isHost: true,\n onmessage: (m) => {\n signalA.emit(\"message\", m);\n },\n });\n const transportB = webrtc().create({\n encrypt,\n decrypt,\n subsend: async (m) => {\n signalB.emit(\"signal\", m);\n },\n isHost: false,\n onmessage: (m) => {\n signalB.emit(\"message\", m);\n },\n });\n\n signalA.on(\"signal\", (m) => {\n console.log(\"messageA\", m);\n transportB.handle(m);\n });\n signalB.on(\"signal\", (m) => {\n console.log(\"messageB\", m);\n transportA.handle(m);\n });\n\n console.log(\"test: setup\");\n await Promise.all([transportA.setup(), transportB.setup()]);\n\n console.log(\"test: waitFor connected\");\n await Promise.all([\n transportA.waitFor(TRANSPORT_STATE.CONNECTED),\n transportB.waitFor(TRANSPORT_STATE.CONNECTED),\n ]);\n\n console.log(\"test: connected\");\n\n const awaitedMessageAtB = new Promise<object>((resolve) => {\n signalB.on(\"message\", (m) => {\n console.log(\"messageB\", m);\n resolve(m);\n });\n });\n\n console.log(\"test: send message to A\");\n await transportA.send({ data: \"test_123\" });\n\n console.log(\"test: await message at B\");\n const messageAtB = await awaitedMessageAtB;\n\n expect(messageAtB).toEqual({ data: \"test_123\" });\n\n console.log(\"test: message at B\", messageAtB);\n const awaitedMessageAtA = new Promise<object>((resolve) => {\n signalA.on(\"message\", (m) => {\n console.log(\"messageA\", m);\n resolve(m);\n });\n });\n\n await transportB.send({ data: \"test_456\" });\n const messageAtA = await awaitedMessageAtA;\n\n expect(messageAtA).toEqual({ data: \"test_456\" });\n\n await Promise.all([transportA.teardown(), transportB.teardown()]);\n\n expect(transportA).toBeDefined();\n expect(transportB).toBeDefined();\n });\n});\n"]}
|
package/dist/utils/log.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const log: (...
|
|
1
|
+
export declare const log: (...args: Parameters<typeof console.log>) => void;
|
package/dist/utils/log.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
// "color: orange; font-weight: bold",
|
|
4
|
-
// "color: inherit; font-weight: normal",
|
|
5
|
-
...x);
|
|
1
|
+
import { createLogger } from "@openlv/core";
|
|
2
|
+
export const log = createLogger("transport", "orange");
|
|
6
3
|
//# sourceMappingURL=log.js.map
|
package/dist/utils/log.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"log.js","sourceRoot":"","sources":["../../src/utils/log.ts"],"names":[],"mappings":"AAAA,
|
|
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,WAAW,EAAE,QAAQ,CAAC,CAAC","sourcesContent":["import { createLogger } from \"@openlv/core\";\n\nexport const log = createLogger(\"transport\", \"orange\");\n"]}
|
package/dist/webrtc/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type Transport } from "../index.js";
|
|
2
2
|
export type WebRTCConfig = {
|
|
3
3
|
iceServers?: RTCConfiguration["iceServers"];
|
|
4
4
|
};
|
|
5
|
-
export declare const webrtc:
|
|
5
|
+
export declare const webrtc: Transport;
|
package/dist/webrtc/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { match } from "ts-pattern";
|
|
2
2
|
import { createTransportBase, } from "../index.js";
|
|
3
3
|
import { log } from "../utils/log.js";
|
|
4
|
-
//
|
|
4
|
+
// Deliberately STUN-only: TURN relays all traffic through a third party, so
|
|
5
|
+
// operators should opt in explicitly with their own infrastructure.
|
|
6
|
+
// (The previously bundled openrelay.metered.ca TURN service is discontinued.)
|
|
5
7
|
const defaultConfig = {
|
|
6
8
|
iceServers: [
|
|
7
9
|
{ urls: "stun:stun.l.google.com:19302" },
|
|
@@ -16,84 +18,122 @@ const defaultConfig = {
|
|
|
16
18
|
};
|
|
17
19
|
export const webrtc = (config = defaultConfig) => {
|
|
18
20
|
const { iceServers = defaultConfig.iceServers } = config;
|
|
19
|
-
|
|
20
|
-
const ident = Math.random().toString(36)
|
|
21
|
-
.slice(2, 4) + "#";
|
|
22
|
-
return createTransportBase(({ emitter, isHost }) => {
|
|
21
|
+
return createTransportBase("wrtc", ({ emitter, isHost }) => {
|
|
23
22
|
const rtcConfig = { iceServers };
|
|
24
23
|
let connection;
|
|
25
24
|
let channel;
|
|
25
|
+
// Candidates can arrive over signaling before the offer/answer has been
|
|
26
|
+
// applied; addIceCandidate would reject, so they are buffered until a
|
|
27
|
+
// remote description exists.
|
|
28
|
+
let pendingCandidates = [];
|
|
29
|
+
const flushPendingCandidates = async () => {
|
|
30
|
+
const queued = pendingCandidates;
|
|
31
|
+
pendingCandidates = [];
|
|
32
|
+
for (const candidate of queued) {
|
|
33
|
+
await connection?.addIceCandidate(new RTCIceCandidate(candidate));
|
|
34
|
+
}
|
|
35
|
+
};
|
|
26
36
|
const onConnectionStateChange = () => {
|
|
27
|
-
log(ident, "onConnectionStateChange", connection?.connectionState);
|
|
28
37
|
const state = connection?.connectionState;
|
|
38
|
+
log("onConnectionStateChange", state);
|
|
29
39
|
match(state)
|
|
30
|
-
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
.with("
|
|
34
|
-
.otherwise(() => {
|
|
35
|
-
log(ident, "onConnectionStateChangeUnknown", state);
|
|
36
|
-
});
|
|
40
|
+
// "disconnected" is transient and may recover on its own; only
|
|
41
|
+
// "failed"/"closed" are terminal.
|
|
42
|
+
.with("failed", () => emitter.emit("error", "WebRTC connection failed"))
|
|
43
|
+
.with("closed", () => emitter.emit("error", "WebRTC connection closed"))
|
|
44
|
+
.otherwise(() => { });
|
|
37
45
|
};
|
|
46
|
+
let localCandidates = 0;
|
|
38
47
|
const onIceCandidate = (c) => {
|
|
39
48
|
if (channel?.readyState === "open")
|
|
40
49
|
return;
|
|
41
|
-
|
|
42
|
-
|
|
50
|
+
// End-of-candidates (null) needs no relay; the peer simply stops
|
|
51
|
+
// receiving candidates.
|
|
52
|
+
if (!c.candidate)
|
|
53
|
+
return;
|
|
54
|
+
localCandidates += 1;
|
|
55
|
+
log("local ICE candidate", c.candidate.type, c.candidate.protocol);
|
|
56
|
+
emitter.emit("negotiate", {
|
|
57
|
+
type: "candidate",
|
|
58
|
+
payload: JSON.stringify(c.candidate.toJSON()),
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
const onIceGatheringStateChange = () => {
|
|
62
|
+
log("iceGatheringState", connection?.iceGatheringState);
|
|
63
|
+
if (connection?.iceGatheringState === "complete" && localCandidates === 0) {
|
|
64
|
+
// Deliberately not debug-gated: without a single local candidate the
|
|
65
|
+
// connection can never establish, and the cause is environmental
|
|
66
|
+
// (blocked UDP, no usable interface, unreachable STUN/TURN).
|
|
67
|
+
console.warn("[openlv] WebRTC gathered zero local ICE candidates — "
|
|
68
|
+
+ "the peer-to-peer connection cannot establish. "
|
|
69
|
+
+ "Check network/UDP access or configure reachable STUN/TURN servers.");
|
|
70
|
+
emitter.emit("error", "no local ICE candidates");
|
|
71
|
+
}
|
|
43
72
|
};
|
|
44
73
|
const onDataChannel = (e) => {
|
|
45
74
|
channel = e.channel;
|
|
46
75
|
hookChannel(channel);
|
|
47
|
-
log(
|
|
76
|
+
log("onDataChannel");
|
|
48
77
|
};
|
|
49
78
|
const onDataChannelOpen = () => {
|
|
50
|
-
log(
|
|
79
|
+
log("onDataChannelOpen");
|
|
51
80
|
emitter.emit("connected");
|
|
52
81
|
};
|
|
53
82
|
const onDataChannelMessage = (e) => {
|
|
54
|
-
log(ident, "onDataChannelMessage", e.data);
|
|
55
83
|
emitter.emit("message", e.data);
|
|
56
84
|
};
|
|
85
|
+
const onDataChannelClose = () => {
|
|
86
|
+
emitter.emit("error", "Data channel closed");
|
|
87
|
+
};
|
|
57
88
|
const onNegotiationNeeded = async () => {
|
|
58
|
-
log(
|
|
89
|
+
log("onNegotiationNeeded");
|
|
59
90
|
if (isHost && connection) {
|
|
60
91
|
await connection.setLocalDescription();
|
|
61
|
-
if (connection.localDescription)
|
|
62
|
-
emitter.emit("
|
|
92
|
+
if (connection.localDescription) {
|
|
93
|
+
emitter.emit("negotiate", {
|
|
94
|
+
type: "offer",
|
|
95
|
+
payload: JSON.stringify(connection.localDescription),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
63
98
|
}
|
|
64
99
|
};
|
|
65
100
|
const hookChannel = (channel) => {
|
|
66
101
|
channel.addEventListener("open", onDataChannelOpen);
|
|
67
102
|
channel.addEventListener("message", onDataChannelMessage);
|
|
103
|
+
channel.addEventListener("close", onDataChannelClose);
|
|
68
104
|
};
|
|
69
105
|
const handle = async (message) => {
|
|
70
|
-
log(
|
|
106
|
+
log("webrtc handle", message.type);
|
|
71
107
|
if (!connection)
|
|
72
108
|
throw new Error("Connection not found");
|
|
73
109
|
return match(message)
|
|
74
110
|
.with({ type: "offer" }, async ({ payload }) => {
|
|
75
111
|
const offer = JSON.parse(payload);
|
|
76
112
|
await connection.setRemoteDescription(new RTCSessionDescription(offer));
|
|
113
|
+
await flushPendingCandidates();
|
|
77
114
|
const answer = await connection.createAnswer();
|
|
78
115
|
await connection.setLocalDescription(answer);
|
|
79
|
-
emitter.emit("answer", JSON.stringify(answer));
|
|
116
|
+
emitter.emit("negotiate", { type: "answer", payload: JSON.stringify(answer) });
|
|
80
117
|
})
|
|
81
118
|
.with({ type: "answer" }, async ({ payload }) => {
|
|
82
|
-
if (!connection)
|
|
83
|
-
throw new Error("Connection not found");
|
|
84
119
|
const answer = JSON.parse(payload);
|
|
85
120
|
await connection.setRemoteDescription(new RTCSessionDescription(answer));
|
|
121
|
+
await flushPendingCandidates();
|
|
86
122
|
})
|
|
87
123
|
.with({ type: "candidate" }, async ({ payload }) => {
|
|
88
|
-
if (!connection)
|
|
89
|
-
throw new Error("Connection not found");
|
|
90
124
|
if (!payload)
|
|
91
125
|
return;
|
|
92
126
|
const candidate = JSON.parse(payload);
|
|
127
|
+
if (!connection.remoteDescription) {
|
|
128
|
+
log("buffering remote ICE candidate until remote description is set");
|
|
129
|
+
pendingCandidates.push(candidate);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
log("applying remote ICE candidate");
|
|
93
133
|
await connection.addIceCandidate(new RTCIceCandidate(candidate));
|
|
94
134
|
})
|
|
95
135
|
.otherwise(() => {
|
|
96
|
-
log(
|
|
136
|
+
log("received unknown transport message type", message);
|
|
97
137
|
});
|
|
98
138
|
};
|
|
99
139
|
const send = async (message) => {
|
|
@@ -102,12 +142,12 @@ export const webrtc = (config = defaultConfig) => {
|
|
|
102
142
|
channel.send(message);
|
|
103
143
|
};
|
|
104
144
|
return {
|
|
105
|
-
type: "webrtc",
|
|
106
145
|
async setup() {
|
|
107
|
-
log("webrtc
|
|
146
|
+
log("webrtc setup");
|
|
108
147
|
connection = new RTCPeerConnection(rtcConfig);
|
|
109
148
|
connection.onconnectionstatechange = onConnectionStateChange;
|
|
110
149
|
connection.onicecandidate = onIceCandidate;
|
|
150
|
+
connection.onicegatheringstatechange = onIceGatheringStateChange;
|
|
111
151
|
connection.ondatachannel = onDataChannel;
|
|
112
152
|
connection.onnegotiationneeded = onNegotiationNeeded;
|
|
113
153
|
if (isHost) {
|
|
@@ -117,11 +157,16 @@ export const webrtc = (config = defaultConfig) => {
|
|
|
117
157
|
},
|
|
118
158
|
teardown() {
|
|
119
159
|
log("webrtc teardown");
|
|
160
|
+
pendingCandidates = [];
|
|
120
161
|
if (channel) {
|
|
162
|
+
// Detach the close listener first: a deliberate teardown must not
|
|
163
|
+
// surface as a transport error.
|
|
164
|
+
channel.removeEventListener("close", onDataChannelClose);
|
|
121
165
|
channel.close();
|
|
122
166
|
channel = undefined;
|
|
123
167
|
}
|
|
124
168
|
if (connection) {
|
|
169
|
+
connection.onconnectionstatechange = null;
|
|
125
170
|
connection.close();
|
|
126
171
|
connection = undefined;
|
|
127
172
|
}
|
package/dist/webrtc/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/webrtc/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { match } from \"ts-pattern\";\n\nimport {\n createTransportBase,\n type CreateTransportLayerFn,\n type TransportMessage,\n} from \"../index.js\";\nimport { log } from \"../utils/log.js\";\n\nexport type WebRTCConfig = {\n iceServers?: RTCConfiguration[\"iceServers\"];\n};\n\n// TODO: decide wether we want defaults, and if so what defaults\nconst defaultConfig: WebRTCConfig = {\n iceServers: [\n { urls: \"stun:stun.l.google.com:19302\" },\n { urls: \"stun:stun1.l.google.com:19302\" },\n { urls: \"stun:stun.services.mozilla.com:3478\" },\n {\n urls: [\"turn:openrelay.metered.ca:80\", \"turn:openrelay.metered.ca:443\"],\n username: \"openrelayproject\",\n credential: \"openrelayproject\",\n },\n ],\n};\n\nexport const webrtc: CreateTransportLayerFn = (\n config: WebRTCConfig = defaultConfig,\n) => {\n const { iceServers = defaultConfig.iceServers } = config;\n\n console.log(\"WEBRTC ICE CONFIG\", config);\n const ident = Math.random().toString(36)\n .slice(2, 4) + \"#\";\n\n return createTransportBase(({ emitter, isHost }) => {\n const rtcConfig: RTCConfiguration = { iceServers };\n let connection: RTCPeerConnection | undefined;\n let channel: RTCDataChannel | undefined;\n\n const onConnectionStateChange = () => {\n log(ident, \"onConnectionStateChange\", connection?.connectionState);\n\n const state = connection?.connectionState;\n\n match(state)\n .with(\"disconnected\", () => {\n // TODO: implement\n })\n .with(\"failed\", () => {})\n .otherwise(() => {\n log(ident, \"onConnectionStateChangeUnknown\", state);\n });\n };\n const onIceCandidate = (c: RTCPeerConnectionIceEvent) => {\n if (channel?.readyState === \"open\") return;\n\n log(ident, \"onIceCandidate\", c.candidate);\n emitter.emit(\"candidate\", JSON.stringify(c.candidate?.toJSON()));\n };\n const onDataChannel = (e: RTCDataChannelEvent) => {\n channel = e.channel;\n hookChannel(channel);\n log(ident, \"onDataChannel\", channel);\n };\n const onDataChannelOpen = () => {\n log(ident, \"onDataChannelOpen\");\n emitter.emit(\"connected\");\n };\n const onDataChannelMessage = (e: MessageEvent<string>) => {\n log(ident, \"onDataChannelMessage\", e.data);\n emitter.emit(\"message\", e.data);\n };\n const onNegotiationNeeded = async () => {\n log(ident, \"onNegotiationNeeded\");\n\n if (isHost && connection) {\n await connection.setLocalDescription();\n\n if (connection.localDescription)\n emitter.emit(\"offer\", JSON.stringify(connection.localDescription));\n }\n };\n\n const hookChannel = (channel: RTCDataChannel) => {\n channel.addEventListener(\"open\", onDataChannelOpen);\n channel.addEventListener(\"message\", onDataChannelMessage);\n };\n\n const handle = async (message: TransportMessage): Promise<void> => {\n log(ident, \"webrtc handle\", message);\n\n if (!connection) throw new Error(\"Connection not found\");\n\n return match(message)\n .with({ type: \"offer\" }, async ({ payload }) => {\n const offer = JSON.parse(payload) as RTCSessionDescriptionInit;\n\n await connection!.setRemoteDescription(\n new RTCSessionDescription(offer),\n );\n\n const answer = await connection!.createAnswer();\n\n await connection!.setLocalDescription(answer);\n emitter.emit(\"answer\", JSON.stringify(answer));\n })\n .with({ type: \"answer\" }, async ({ payload }) => {\n if (!connection) throw new Error(\"Connection not found\");\n\n const answer = JSON.parse(payload) as RTCSessionDescriptionInit;\n\n await connection.setRemoteDescription(\n new RTCSessionDescription(answer),\n );\n })\n .with({ type: \"candidate\" }, async ({ payload }) => {\n if (!connection) throw new Error(\"Connection not found\");\n\n if (!payload) return;\n\n const candidate = JSON.parse(payload) as RTCIceCandidateInit;\n\n await connection.addIceCandidate(new RTCIceCandidate(candidate));\n })\n .otherwise(() => {\n log(ident, \"received unknown transport message type\", message);\n });\n };\n\n const send = async (message: string) => {\n if (!channel) throw new Error(\"Channel not found\");\n\n channel.send(message);\n };\n\n return {\n type: \"webrtc\",\n async setup() {\n log(\"webrtc setupz\", rtcConfig);\n\n connection = new RTCPeerConnection(rtcConfig);\n connection.onconnectionstatechange = onConnectionStateChange;\n connection.onicecandidate = onIceCandidate;\n connection.ondatachannel = onDataChannel;\n connection.onnegotiationneeded = onNegotiationNeeded;\n\n if (isHost) {\n channel = connection.createDataChannel(\"openlv-data\");\n hookChannel(channel);\n }\n },\n teardown() {\n log(\"webrtc teardown\");\n\n if (channel) {\n channel.close();\n channel = undefined;\n }\n\n if (connection) {\n connection.close();\n connection = undefined;\n }\n },\n handle,\n send,\n };\n });\n};\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/webrtc/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { match } from \"ts-pattern\";\n\nimport {\n createTransportBase,\n type Transport,\n type TransportMessage,\n} from \"../index.js\";\nimport { log } from \"../utils/log.js\";\n\nexport type WebRTCConfig = {\n iceServers?: RTCConfiguration[\"iceServers\"];\n};\n\n// Deliberately STUN-only: TURN relays all traffic through a third party, so\n// operators should opt in explicitly with their own infrastructure.\n// (The previously bundled openrelay.metered.ca TURN service is discontinued.)\nconst defaultConfig: WebRTCConfig = {\n iceServers: [\n { urls: \"stun:stun.l.google.com:19302\" },\n { urls: \"stun:stun1.l.google.com:19302\" },\n { urls: \"stun:stun.services.mozilla.com:3478\" },\n {\n urls: [\"turn:openrelay.metered.ca:80\", \"turn:openrelay.metered.ca:443\"],\n username: \"openrelayproject\",\n credential: \"openrelayproject\",\n },\n ],\n};\n\nexport const webrtc: Transport = (\n config: WebRTCConfig = defaultConfig,\n) => {\n const { iceServers = defaultConfig.iceServers } = config;\n\n return createTransportBase(\"wrtc\", ({ emitter, isHost }) => {\n const rtcConfig: RTCConfiguration = { iceServers };\n let connection: RTCPeerConnection | undefined;\n let channel: RTCDataChannel | undefined;\n // Candidates can arrive over signaling before the offer/answer has been\n // applied; addIceCandidate would reject, so they are buffered until a\n // remote description exists.\n let pendingCandidates: RTCIceCandidateInit[] = [];\n\n const flushPendingCandidates = async () => {\n const queued = pendingCandidates;\n\n pendingCandidates = [];\n\n for (const candidate of queued) {\n await connection?.addIceCandidate(new RTCIceCandidate(candidate));\n }\n };\n\n const onConnectionStateChange = () => {\n const state = connection?.connectionState;\n\n log(\"onConnectionStateChange\", state);\n\n match(state)\n // \"disconnected\" is transient and may recover on its own; only\n // \"failed\"/\"closed\" are terminal.\n .with(\"failed\", () => emitter.emit(\"error\", \"WebRTC connection failed\"))\n .with(\"closed\", () => emitter.emit(\"error\", \"WebRTC connection closed\"))\n .otherwise(() => {});\n };\n let localCandidates = 0;\n\n const onIceCandidate = (c: RTCPeerConnectionIceEvent) => {\n if (channel?.readyState === \"open\") return;\n\n // End-of-candidates (null) needs no relay; the peer simply stops\n // receiving candidates.\n if (!c.candidate) return;\n\n localCandidates += 1;\n log(\"local ICE candidate\", c.candidate.type, c.candidate.protocol);\n emitter.emit(\"negotiate\", {\n type: \"candidate\",\n payload: JSON.stringify(c.candidate.toJSON()),\n });\n };\n const onIceGatheringStateChange = () => {\n log(\"iceGatheringState\", connection?.iceGatheringState);\n\n if (connection?.iceGatheringState === \"complete\" && localCandidates === 0) {\n // Deliberately not debug-gated: without a single local candidate the\n // connection can never establish, and the cause is environmental\n // (blocked UDP, no usable interface, unreachable STUN/TURN).\n console.warn(\n \"[openlv] WebRTC gathered zero local ICE candidates — \"\n + \"the peer-to-peer connection cannot establish. \"\n + \"Check network/UDP access or configure reachable STUN/TURN servers.\",\n );\n emitter.emit(\"error\", \"no local ICE candidates\");\n }\n };\n const onDataChannel = (e: RTCDataChannelEvent) => {\n channel = e.channel;\n hookChannel(channel);\n log(\"onDataChannel\");\n };\n const onDataChannelOpen = () => {\n log(\"onDataChannelOpen\");\n emitter.emit(\"connected\");\n };\n const onDataChannelMessage = (e: MessageEvent<string>) => {\n emitter.emit(\"message\", e.data);\n };\n const onDataChannelClose = () => {\n emitter.emit(\"error\", \"Data channel closed\");\n };\n const onNegotiationNeeded = async () => {\n log(\"onNegotiationNeeded\");\n\n if (isHost && connection) {\n await connection.setLocalDescription();\n\n if (connection.localDescription) {\n emitter.emit(\"negotiate\", {\n type: \"offer\",\n payload: JSON.stringify(connection.localDescription),\n });\n }\n }\n };\n\n const hookChannel = (channel: RTCDataChannel) => {\n channel.addEventListener(\"open\", onDataChannelOpen);\n channel.addEventListener(\"message\", onDataChannelMessage);\n channel.addEventListener(\"close\", onDataChannelClose);\n };\n\n const handle = async (message: TransportMessage): Promise<void> => {\n log(\"webrtc handle\", message.type);\n\n if (!connection) throw new Error(\"Connection not found\");\n\n return match(message)\n .with({ type: \"offer\" }, async ({ payload }) => {\n const offer = JSON.parse(payload) as RTCSessionDescriptionInit;\n\n await connection!.setRemoteDescription(\n new RTCSessionDescription(offer),\n );\n await flushPendingCandidates();\n\n const answer = await connection!.createAnswer();\n\n await connection!.setLocalDescription(answer);\n emitter.emit(\"negotiate\", { type: \"answer\", payload: JSON.stringify(answer) });\n })\n .with({ type: \"answer\" }, async ({ payload }) => {\n const answer = JSON.parse(payload) as RTCSessionDescriptionInit;\n\n await connection!.setRemoteDescription(\n new RTCSessionDescription(answer),\n );\n await flushPendingCandidates();\n })\n .with({ type: \"candidate\" }, async ({ payload }) => {\n if (!payload) return;\n\n const candidate = JSON.parse(payload) as RTCIceCandidateInit;\n\n if (!connection!.remoteDescription) {\n log(\"buffering remote ICE candidate until remote description is set\");\n pendingCandidates.push(candidate);\n\n return;\n }\n\n log(\"applying remote ICE candidate\");\n await connection!.addIceCandidate(new RTCIceCandidate(candidate));\n })\n .otherwise(() => {\n log(\"received unknown transport message type\", message);\n });\n };\n\n const send = async (message: string) => {\n if (!channel) throw new Error(\"Channel not found\");\n\n channel.send(message);\n };\n\n return {\n async setup() {\n log(\"webrtc setup\");\n\n connection = new RTCPeerConnection(rtcConfig);\n connection.onconnectionstatechange = onConnectionStateChange;\n connection.onicecandidate = onIceCandidate;\n connection.onicegatheringstatechange = onIceGatheringStateChange;\n connection.ondatachannel = onDataChannel;\n connection.onnegotiationneeded = onNegotiationNeeded;\n\n if (isHost) {\n channel = connection.createDataChannel(\"openlv-data\");\n hookChannel(channel);\n }\n },\n teardown() {\n log(\"webrtc teardown\");\n pendingCandidates = [];\n\n if (channel) {\n // Detach the close listener first: a deliberate teardown must not\n // surface as a transport error.\n channel.removeEventListener(\"close\", onDataChannelClose);\n channel.close();\n channel = undefined;\n }\n\n if (connection) {\n connection.onconnectionstatechange = null;\n connection.close();\n connection = undefined;\n }\n },\n handle,\n send,\n };\n });\n};\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openlv/transport",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"keywords": [
|
|
5
5
|
"open-lavatory"
|
|
6
6
|
],
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"repository": {
|
|
12
12
|
"type": "git",
|
|
13
|
-
"url": "https://github.com/v3xlabs/open-lavatory.git",
|
|
13
|
+
"url": "git+https://github.com/v3xlabs/open-lavatory.git",
|
|
14
14
|
"directory": "packages/transport"
|
|
15
15
|
},
|
|
16
16
|
"license": "LGPL-3.0-only",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"eventemitter3": "^5.0.1",
|
|
43
43
|
"globals": "^16.2.0",
|
|
44
44
|
"ts-pattern": "^5",
|
|
45
|
-
"@openlv/core": "0.0
|
|
45
|
+
"@openlv/core": "0.1.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@vitest/browser-playwright": "^4.0.8",
|
|
@@ -50,14 +50,8 @@
|
|
|
50
50
|
"typescript": "^5",
|
|
51
51
|
"vitest": "^4"
|
|
52
52
|
},
|
|
53
|
-
"peerDependencies": {
|
|
54
|
-
"@wagmi/core": "2",
|
|
55
|
-
"viem": "2",
|
|
56
|
-
"wagmi": "2"
|
|
57
|
-
},
|
|
58
53
|
"publishConfig": {
|
|
59
|
-
"access": "public"
|
|
60
|
-
"tag": "beta"
|
|
54
|
+
"access": "public"
|
|
61
55
|
},
|
|
62
56
|
"scripts": {
|
|
63
57
|
"build": "tsc",
|