@openlv/transport 0.0.3 → 0.1.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.
@@ -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":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EACL,mBAAmB,EACnB,eAAe,GAGhB,MAAM,YAAY,CAAC;AAEpB,MAAM,mBAAmB,GAAG,GAAG,EAAE;IAC/B,IAAI,OAA8C,CAAC;IACnD,MAAM,IAAI,GAAa,EAAE,CAAC;IAE1B,MAAM,OAAO,GAAG,mBAAmB,CAAC,MAAM,EAAE,CAAC,UAAU,EAAE,EAAE;QACzD,OAAO,GAAG,UAAU,CAAC,OAAO,CAAC;QAE7B,OAAO;YACL,KAAK,EAAE,GAAG,EAAE,GAAE,CAAC;YACf,QAAQ,EAAE,GAAG,EAAE,GAAE,CAAC;YAClB,MAAM,EAAE,KAAK,IAAI,EAAE,GAAE,CAAC;YACtB,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;gBACtB,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACrB,CAAC;SACF,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,OAAO;QACL,OAAO;QACP,IAAI;QACJ,UAAU,EAAE,GAA8B,EAAE;YAC1C,IAAI,CAAC,OAAO;gBAAE,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAC;YAEvD,OAAO,OAAO,CAAC;QACjB,CAAC;KACF,CAAC;AACJ,CAAC,CAAC;AAEF,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE;IACnC,EAAE,CAAC,iDAAiD,EAAE,GAAG,EAAE;QACzD,MAAM,EAAE,OAAO,EAAE,GAAG,mBAAmB,EAAE,CAAC;QAE1C,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yCAAyC,EAAE,KAAK,IAAI,EAAE;QACvD,MAAM,EAAE,aAAa,EAAE,aAAa,EAAE,GAAG,MAAM,eAAe,EAAE,CAAC;QACjE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,mBAAmB,EAAE,CAAC;QACtD,MAAM,OAAO,GAAuB,EAAE,CAAC;QAEvC,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC;YAC3B,OAAO,EAAE,aAAa,CAAC,OAAO;YAC9B,OAAO,EAAE,aAAa,CAAC,OAAO;YAC9B,MAAM,EAAE,IAAI;YACZ,SAAS,EAAE,GAAG,EAAE,GAAE,CAAC;YACnB,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;gBACzB,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACxB,CAAC;SACF,CAAC,CAAC;QAEH,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAChC,MAAM,KAAK,CAAC,KAAK,EAAE,CAAC;QAEpB,UAAU,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QAClE,UAAU,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;QACvE,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;QAExB,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC;YACtB,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE;YACjC,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE;SACvC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uDAAuD,EAAE,KAAK,IAAI,EAAE;QACrE,MAAM,EAAE,aAAa,EAAE,aAAa,EAAE,GAAG,MAAM,eAAe,EAAE,CAAC;QACjE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,mBAAmB,EAAE,CAAC;QAC5D,MAAM,QAAQ,GAAa,EAAE,CAAC;QAE9B,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC;YAC3B,OAAO,EAAE,aAAa,CAAC,OAAO;YAC9B,OAAO,EAAE,aAAa,CAAC,OAAO;YAC9B,MAAM,EAAE,IAAI;YACZ,SAAS,EAAE,CAAC,OAAO,EAAE,EAAE;gBACrB,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACzB,CAAC;YACD,OAAO,EAAE,KAAK,IAAI,EAAE,GAAE,CAAC;SACxB,CAAC,CAAC;QAEH,MAAM,KAAK,CAAC,KAAK,EAAE,CAAC;QACpB,UAAU,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QAC/B,MAAM,KAAK,CAAC,OAAO,CAAC,eAAe,CAAC,SAAS,CAAC,CAAC;QAE/C,MAAM,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;QACzE,MAAM,CAAC,IAAI,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QAC7B,sDAAsD;QACtD,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;QAE3C,UAAU,EAAE,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;QACtC,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;QACtD,MAAM,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IACrF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iDAAiD,EAAE,KAAK,IAAI,EAAE;QAC/D,MAAM,EAAE,aAAa,EAAE,aAAa,EAAE,GAAG,MAAM,eAAe,EAAE,CAAC;QACjE,MAAM,EAAE,OAAO,EAAE,GAAG,mBAAmB,EAAE,CAAC;QAE1C,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC;YAC3B,OAAO,EAAE,aAAa,CAAC,OAAO;YAC9B,OAAO,EAAE,aAAa,CAAC,OAAO;YAC9B,MAAM,EAAE,IAAI;YACZ,SAAS,EAAE,GAAG,EAAE,GAAE,CAAC;YACnB,OAAO,EAAE,KAAK,IAAI,EAAE,GAAE,CAAC;SACxB,CAAC,CAAC;QAEH,MAAM,KAAK,CAAC,KAAK,EAAE,CAAC;QACpB,MAAM,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,yBAAyB,CAAC,CAAC;IAChF,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","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,6 +18,7 @@ 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
23
  export type TransportLayerSetupParameters = {
23
24
  isHost: boolean;
@@ -30,14 +31,13 @@ export type TransportLayerSetupParameters = {
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: "offer";
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 = {
@@ -49,22 +49,24 @@ export type TransportLayer = {
49
49
  waitFor: (state: TransportState) => Promise<void>;
50
50
  emitter: EventEmitter<TLayerEventMap>;
51
51
  };
52
- export type TransportProtocol = "webrtc" | string;
53
- export type TransportLayerFn = (parameters: TransportLayerSetupParameters) => TransportLayer;
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
+ };
54
58
  export type Transport = (config?: WebRTCConfig) => TransportLayerFn;
55
59
  export type TransportLayerImpl = {
56
- type: TransportProtocol;
57
60
  setup: () => MaybePromise<void>;
58
61
  teardown: () => MaybePromise<void>;
59
62
  handle: (message: TransportMessage) => Promise<void>;
60
63
  send: (message: string) => Promise<void>;
61
64
  };
62
65
  export type TransportLayerBaseEventMap = {
63
- offer: (offer: string) => void;
64
- answer: (answer: string) => void;
65
- candidate: (candidate: string) => void;
66
+ negotiate: (message: TransportMessage) => void;
66
67
  connected: () => void;
67
68
  message: (message: string) => void;
69
+ error: (reason?: string) => void;
68
70
  };
69
71
  export type TransportLayerBaseEmitter = EventEmitter<TransportLayerBaseEventMap>;
70
72
  export type TransportLayerBaseParameters = {
@@ -77,4 +79,4 @@ export type TransportLayerImplFn = (parameters: TransportLayerBaseParameters) =>
77
79
  *
78
80
  * https://openlv.sh/api/transport
79
81
  */
80
- export declare const createTransportBase: (init: TransportLayerImplFn) => TransportLayerFn;
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) => ({ encrypt, decrypt, subsend, isHost, onmessage }) => {
16
- const emitter = new EventEmitter();
17
- const internalEmitter = new EventEmitter();
18
- let state = TRANSPORT_STATE.STANDBY;
19
- const setState = (newState) => {
20
- state = newState;
21
- emitter.emit("state_change", newState);
22
- };
23
- internalEmitter.on("offer", (offer) => {
24
- log("onOffer", offer);
25
- subsend({ type: "offer", payload: offer });
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
- return {
68
- type,
69
- async setup() {
70
- setState(TRANSPORT_STATE.CONNECTING);
71
- await setup();
72
- setState(TRANSPORT_STATE.READY);
73
- },
74
- teardown,
75
- handle,
76
- send,
77
- waitFor,
78
- emitter,
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":"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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};\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\nexport type TransportMessage =\n | {\n type: \"offer\";\n payload: string;\n }\n | {\n type: \"answer\";\n payload: string;\n }\n | {\n type: \"candidate\";\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};\nexport type TransportProtocol = \"webrtc\" | string;\nexport type TransportLayerFn = (parameters: TransportLayerSetupParameters) => TransportLayer;\nexport type Transport = (config?: WebRTCConfig) => TransportLayerFn;\n\nexport type TransportLayerImpl = {\n type: TransportProtocol;\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 offer: (offer: string) => void;\n answer: (answer: string) => void;\n candidate: (candidate: string) => void;\n connected: () => void;\n message: (message: 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 = (init: TransportLayerImplFn): TransportLayerFn => ({ 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(\"offer\", (offer) => {\n log(\"onOffer\", offer);\n subsend({ type: \"offer\", payload: offer });\n });\n internalEmitter.on(\"answer\", (answer) => {\n log(\"onAnswer\", answer);\n subsend({ type: \"answer\", payload: answer });\n });\n internalEmitter.on(\"candidate\", (candidate) => {\n log(\"onCandidate\", candidate);\n subsend({ type: \"candidate\", payload: candidate });\n });\n internalEmitter.on(\"connected\", () => {\n log(\"onConnected\");\n setState(TRANSPORT_STATE.CONNECTED);\n });\n internalEmitter.on(\"message\", async (message) => {\n log(\"onMessage\", message);\n const data = await decrypt(message);\n\n onmessage(JSON.parse(data) as { type: string; payload: object; messageId: string; });\n });\n\n const {\n setup,\n teardown,\n type,\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 return new Promise<void>((resolve) => {\n const handler = (newState: TransportState) => {\n if (newState === targetState) {\n emitter.off(\"state_change\", handler);\n resolve();\n }\n };\n\n emitter.on(\"state_change\", handler);\n });\n };\n\n return {\n type,\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"]}
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"]}
@@ -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) => {
@@ -1 +1 @@
1
- {"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"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","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()({\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()({\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"]}
1
+ {"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"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","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"]}
@@ -1 +1 @@
1
- export declare const log: (...x: Parameters<typeof console.log>) => void;
1
+ export declare const log: (...args: Parameters<typeof console.log>) => void;
package/dist/utils/log.js CHANGED
@@ -1,6 +1,3 @@
1
- export const log = (...x) => console.log(
2
- // "%c[transport]%c",
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
@@ -1 +1 @@
1
- {"version":3,"file":"log.js","sourceRoot":"","sources":["../../src/utils/log.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,GAAG,GAAG,CAAC,GAAG,CAAiC,EAAE,EAAE,CAC1D,OAAO,CAAC,GAAG;AACT,qBAAqB;AACrB,sCAAsC;AACtC,yCAAyC;AACzC,GAAG,CAAC,CACL,CAAC","sourcesContent":["export const log = (...x: Parameters<typeof console.log>) =>\n console.log(\n // \"%c[transport]%c\",\n // \"color: orange; font-weight: bold\",\n // \"color: inherit; font-weight: normal\",\n ...x,\n );\n"]}
1
+ {"version":3,"file":"log.js","sourceRoot":"","sources":["../../src/utils/log.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE5C,MAAM,CAAC,MAAM,GAAG,GAAG,YAAY,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC","sourcesContent":["import { createLogger } from \"@openlv/core\";\n\nexport const log = createLogger(\"transport\", \"orange\");\n"]}
@@ -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
- // TODO: decide wether we want defaults, and if so what defaults
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
- console.log("WEBRTC ICE CONFIG", config);
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
- .with("disconnected", () => {
31
- // TODO: implement
32
- })
33
- .with("failed", () => { })
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
- log(ident, "onIceCandidate", c.candidate);
42
- emitter.emit("candidate", JSON.stringify(c.candidate?.toJSON()));
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(ident, "onDataChannel", channel);
76
+ log("onDataChannel");
48
77
  };
49
78
  const onDataChannelOpen = () => {
50
- log(ident, "onDataChannelOpen");
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(ident, "onNegotiationNeeded");
89
+ log("onNegotiationNeeded");
59
90
  if (isHost && connection) {
60
91
  await connection.setLocalDescription();
61
- if (connection.localDescription)
62
- emitter.emit("offer", JSON.stringify(connection.localDescription));
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(ident, "webrtc handle", message);
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(ident, "received unknown transport message type", message);
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 setupz", rtcConfig);
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
  }
@@ -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 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// 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: Transport = (\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,22 +1,22 @@
1
1
  {
2
2
  "name": "@openlv/transport",
3
- "version": "0.0.3",
3
+ "version": "0.1.1",
4
4
  "keywords": [
5
5
  "open-lavatory"
6
6
  ],
7
- "homepage": "https://github.com/v3xlabs/open-lavatory",
7
+ "homepage": "https://github.com/open-lavatory/open-lavatory",
8
8
  "bugs": {
9
- "url": "https://github.com/v3xlabs/open-lavatory/issues"
9
+ "url": "https://github.com/open-lavatory/open-lavatory/issues"
10
10
  },
11
11
  "repository": {
12
12
  "type": "git",
13
- "url": "git+https://github.com/v3xlabs/open-lavatory.git",
13
+ "url": "git+https://github.com/open-lavatory/open-lavatory.git",
14
14
  "directory": "packages/transport"
15
15
  },
16
16
  "license": "LGPL-3.0-only",
17
17
  "author": {
18
18
  "name": "Open-Lavatory",
19
- "url": "https://github.com/v3xlabs/open-lavatory"
19
+ "url": "https://github.com/open-lavatory/open-lavatory"
20
20
  },
21
21
  "sideEffects": false,
22
22
  "type": "module",
@@ -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.2"
45
+ "@openlv/core": "0.1.1"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@vitest/browser-playwright": "^4.0.8",