@openlv/signaling 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.
package/dist/messages.js CHANGED
@@ -1,2 +1,112 @@
1
- export {};
1
+ const MAX_TRANSPORTS = 8;
2
+ const MAX_TRANSPORT_ID_LENGTH = 32;
3
+ const MAX_TEXT_LENGTH = 128;
4
+ // Relay message limits (ntfy in particular) leave little room after base64
5
+ // and encryption overhead; oversized icons would silently fail to deliver,
6
+ // so they are rejected here instead.
7
+ export const MAX_ICON_LENGTH = 8192;
8
+ const isBoundedString = (value, maxLength) => typeof value === "string" && value.length > 0 && value.length <= maxLength;
9
+ /**
10
+ * Check outgoing peer info against the wire limits enforced by receivers.
11
+ * Returns a human-readable problem, or undefined when the info is sendable.
12
+ * Senders must use this: a receiver silently drops an oversized packet, which
13
+ * would otherwise surface only as a generic handshake timeout.
14
+ */
15
+ export const validatePeerInfo = (info) => {
16
+ if (!isBoundedString(info.identity, MAX_TEXT_LENGTH)) {
17
+ return `info.identity must be 1-${MAX_TEXT_LENGTH} characters`;
18
+ }
19
+ if (!isBoundedString(info.name, MAX_TEXT_LENGTH)) {
20
+ return `info.name must be 1-${MAX_TEXT_LENGTH} characters`;
21
+ }
22
+ if (info.icon !== undefined && !isBoundedString(info.icon, MAX_ICON_LENGTH)) {
23
+ return `info.icon must be 1-${MAX_ICON_LENGTH} characters — use a smaller image or an image URL`;
24
+ }
25
+ return undefined;
26
+ };
27
+ const parsePeerInfo = (raw) => {
28
+ if (typeof raw !== "object" || raw === null)
29
+ return undefined;
30
+ const info = raw;
31
+ if (!isBoundedString(info["identity"], MAX_TEXT_LENGTH))
32
+ return undefined;
33
+ if (!isBoundedString(info["name"], MAX_TEXT_LENGTH))
34
+ return undefined;
35
+ if (info["icon"] !== undefined && !isBoundedString(info["icon"], MAX_ICON_LENGTH)) {
36
+ return undefined;
37
+ }
38
+ return {
39
+ identity: info["identity"],
40
+ name: info["name"],
41
+ ...(info["icon"] === undefined ? {} : { icon: info["icon"] }),
42
+ };
43
+ };
44
+ export const parseCapabilities = (raw) => {
45
+ if (typeof raw !== "object" || raw === null)
46
+ return undefined;
47
+ const payload = raw;
48
+ const transports = payload["transports"];
49
+ if (!Array.isArray(transports)
50
+ || transports.length === 0
51
+ || transports.length > MAX_TRANSPORTS
52
+ || !transports.every(transportId => isBoundedString(transportId, MAX_TRANSPORT_ID_LENGTH))) {
53
+ return undefined;
54
+ }
55
+ if (payload["info"] === undefined)
56
+ return { transports };
57
+ const info = parsePeerInfo(payload["info"]);
58
+ if (!info)
59
+ return undefined;
60
+ return { transports, info };
61
+ };
62
+ /**
63
+ * Parse and shape-check a decrypted signaling payload. The relay topic is
64
+ * public, so decrypted JSON is still untrusted input: anything that does not
65
+ * match one of the four message shapes is dropped (returns undefined).
66
+ */
67
+ export const parseSignalMessage = (raw) => {
68
+ let parsed;
69
+ try {
70
+ parsed = JSON.parse(raw);
71
+ }
72
+ catch {
73
+ return undefined;
74
+ }
75
+ if (typeof parsed !== "object" || parsed === null)
76
+ return undefined;
77
+ const msg = parsed;
78
+ switch (msg["type"]) {
79
+ case "flash": {
80
+ return typeof msg["payload"] === "object" && msg["payload"] !== null
81
+ ? msg
82
+ : undefined;
83
+ }
84
+ case "pubkey": {
85
+ const payload = msg["payload"];
86
+ return typeof payload === "object"
87
+ && payload !== null
88
+ && typeof payload["publicKey"] === "string"
89
+ ? msg
90
+ : undefined;
91
+ }
92
+ case "capabilities": {
93
+ const capabilities = parseCapabilities(msg["payload"]);
94
+ if (!capabilities)
95
+ return undefined;
96
+ return {
97
+ type: "capabilities",
98
+ payload: capabilities,
99
+ timestamp: msg["timestamp"],
100
+ };
101
+ }
102
+ case "data": {
103
+ return typeof msg["payload"] === "object" && msg["payload"] !== null
104
+ ? msg
105
+ : undefined;
106
+ }
107
+ default: {
108
+ return undefined;
109
+ }
110
+ }
111
+ };
2
112
  //# sourceMappingURL=messages.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"messages.js","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"","sourcesContent":["export type SignalMessageBase<T extends string, P> = {\n type: T;\n payload: P;\n timestamp: number;\n};\n\n/**\n * Flash message\n * Sent to initiate handshake by non-host\n */\nexport type SignalMessageFlash = SignalMessageBase<\"flash\", object>;\n\nexport type SignalMessagePubkey = SignalMessageBase<\n \"pubkey\",\n {\n publicKey: string;\n dAppInfo?: {\n name: string;\n url: string;\n icon: string;\n };\n }\n>;\n\nexport type SignalMessageAck = SignalMessageBase<\"ack\", undefined>;\n\nexport type SignalMessageData = SignalMessageBase<\"data\", object>;\n\nexport type SignalMessage =\n | SignalMessageFlash\n | SignalMessagePubkey\n | SignalMessageAck\n | SignalMessageData;\n"]}
1
+ {"version":3,"file":"messages.js","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"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","sourcesContent":["export type SignalMessageBase<T extends string, P> = {\n type: T;\n payload: P;\n timestamp: number;\n};\n\n/**\n * Flash message\n * Sent to initiate handshake by non-host\n */\nexport type SignalMessageFlash = SignalMessageBase<\"flash\", object>;\n\nexport type SignalMessagePubkey = SignalMessageBase<\n \"pubkey\",\n {\n publicKey: string;\n }\n>;\n\n/** Optional self-description shown in the other peer's UI. */\nexport type PeerInfo = {\n /** Reverse-DNS application identifier, e.g. \"com.example.wallet\". */\n identity: string;\n name: string;\n /**\n * A data URI or an image URL. Only size is enforced on the wire — anyone\n * rendering or fetching it is responsible for sanity-checking it first.\n */\n icon?: string;\n};\n\nexport type PeerCapabilities = {\n /** Supported transport identifiers, in preference order. */\n transports: string[];\n info?: PeerInfo;\n};\n\n/**\n * Completes the handshake: acknowledges the previous step, negotiates the\n * transport, and shares peer metadata in a single packet.\n */\nexport type SignalMessageCapabilities = SignalMessageBase<\n \"capabilities\",\n PeerCapabilities\n>;\n\nexport type SignalMessageData = SignalMessageBase<\"data\", object>;\n\nexport type SignalMessage =\n | SignalMessageFlash\n | SignalMessagePubkey\n | SignalMessageCapabilities\n | SignalMessageData;\n\nconst MAX_TRANSPORTS = 8;\nconst MAX_TRANSPORT_ID_LENGTH = 32;\nconst MAX_TEXT_LENGTH = 128;\n\n// Relay message limits (ntfy in particular) leave little room after base64\n// and encryption overhead; oversized icons would silently fail to deliver,\n// so they are rejected here instead.\nexport const MAX_ICON_LENGTH = 8192;\n\nconst isBoundedString = (value: unknown, maxLength: number): value is string =>\n typeof value === \"string\" && value.length > 0 && value.length <= maxLength;\n\n/**\n * Check outgoing peer info against the wire limits enforced by receivers.\n * Returns a human-readable problem, or undefined when the info is sendable.\n * Senders must use this: a receiver silently drops an oversized packet, which\n * would otherwise surface only as a generic handshake timeout.\n */\nexport const validatePeerInfo = (info: PeerInfo): string | undefined => {\n if (!isBoundedString(info.identity, MAX_TEXT_LENGTH)) {\n return `info.identity must be 1-${MAX_TEXT_LENGTH} characters`;\n }\n\n if (!isBoundedString(info.name, MAX_TEXT_LENGTH)) {\n return `info.name must be 1-${MAX_TEXT_LENGTH} characters`;\n }\n\n if (info.icon !== undefined && !isBoundedString(info.icon, MAX_ICON_LENGTH)) {\n return `info.icon must be 1-${MAX_ICON_LENGTH} characters — use a smaller image or an image URL`;\n }\n\n return undefined;\n};\n\nconst parsePeerInfo = (raw: unknown): PeerInfo | undefined => {\n if (typeof raw !== \"object\" || raw === null) return undefined;\n\n const info = raw as Record<string, unknown>;\n\n if (!isBoundedString(info[\"identity\"], MAX_TEXT_LENGTH)) return undefined;\n\n if (!isBoundedString(info[\"name\"], MAX_TEXT_LENGTH)) return undefined;\n\n if (info[\"icon\"] !== undefined && !isBoundedString(info[\"icon\"], MAX_ICON_LENGTH)) {\n return undefined;\n }\n\n return {\n identity: info[\"identity\"],\n name: info[\"name\"],\n ...(info[\"icon\"] === undefined ? {} : { icon: info[\"icon\"] }),\n };\n};\n\nexport const parseCapabilities = (raw: unknown): PeerCapabilities | undefined => {\n if (typeof raw !== \"object\" || raw === null) return undefined;\n\n const payload = raw as Record<string, unknown>;\n const transports = payload[\"transports\"];\n\n if (\n !Array.isArray(transports)\n || transports.length === 0\n || transports.length > MAX_TRANSPORTS\n || !transports.every(transportId => isBoundedString(transportId, MAX_TRANSPORT_ID_LENGTH))\n ) {\n return undefined;\n }\n\n if (payload[\"info\"] === undefined) return { transports };\n\n const info = parsePeerInfo(payload[\"info\"]);\n\n if (!info) return undefined;\n\n return { transports, info };\n};\n\n/**\n * Parse and shape-check a decrypted signaling payload. The relay topic is\n * public, so decrypted JSON is still untrusted input: anything that does not\n * match one of the four message shapes is dropped (returns undefined).\n */\nexport const parseSignalMessage = (raw: string): SignalMessage | undefined => {\n let parsed: unknown;\n\n try {\n parsed = JSON.parse(raw);\n }\n catch {\n return undefined;\n }\n\n if (typeof parsed !== \"object\" || parsed === null) return undefined;\n\n const msg = parsed as Record<string, unknown>;\n\n switch (msg[\"type\"]) {\n case \"flash\": {\n return typeof msg[\"payload\"] === \"object\" && msg[\"payload\"] !== null\n ? (msg as SignalMessageFlash)\n : undefined;\n }\n case \"pubkey\": {\n const payload = msg[\"payload\"] as Record<string, unknown> | null;\n\n return typeof payload === \"object\"\n && payload !== null\n && typeof payload[\"publicKey\"] === \"string\"\n ? (msg as SignalMessagePubkey)\n : undefined;\n }\n case \"capabilities\": {\n const capabilities = parseCapabilities(msg[\"payload\"]);\n\n if (!capabilities) return undefined;\n\n return {\n type: \"capabilities\",\n payload: capabilities,\n timestamp: msg[\"timestamp\"] as number,\n };\n }\n case \"data\": {\n return typeof msg[\"payload\"] === \"object\" && msg[\"payload\"] !== null\n ? (msg as SignalMessageData)\n : undefined;\n }\n default: {\n return undefined;\n }\n }\n};\n"]}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,92 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { MAX_ICON_LENGTH, parseSignalMessage, validatePeerInfo } from "./messages.js";
3
+ const frame = (type, payload) => JSON.stringify({ type, payload, timestamp: 1750000000000 });
4
+ describe("parseSignalMessage", () => {
5
+ it("parses a capabilities packet with info", () => {
6
+ const message = parseSignalMessage(frame("capabilities", {
7
+ transports: ["wrtc", "ws"],
8
+ info: {
9
+ identity: "com.example.wallet",
10
+ name: "Example Wallet",
11
+ icon: "data:image/png;base64,aaaa",
12
+ },
13
+ }));
14
+ expect(message).toEqual({
15
+ type: "capabilities",
16
+ payload: {
17
+ transports: ["wrtc", "ws"],
18
+ info: {
19
+ identity: "com.example.wallet",
20
+ name: "Example Wallet",
21
+ icon: "data:image/png;base64,aaaa",
22
+ },
23
+ },
24
+ timestamp: 1750000000000,
25
+ });
26
+ });
27
+ it("accepts an icon that is a URL — content vetting is the renderer's job", () => {
28
+ const message = parseSignalMessage(frame("capabilities", {
29
+ transports: ["wrtc"],
30
+ info: {
31
+ identity: "com.example.wallet",
32
+ name: "Example Wallet",
33
+ icon: "https://example.com/icon.png",
34
+ },
35
+ }));
36
+ expect(message?.payload).toMatchObject({
37
+ info: { icon: "https://example.com/icon.png" },
38
+ });
39
+ });
40
+ it("parses a capabilities packet without info", () => {
41
+ expect(parseSignalMessage(frame("capabilities", { transports: ["wrtc"] }))).toMatchObject({ payload: { transports: ["wrtc"] } });
42
+ });
43
+ it("drops unknown extra fields from capabilities payloads", () => {
44
+ const message = parseSignalMessage(frame("capabilities", { transports: ["wrtc"], extra: "field" }));
45
+ expect(message?.payload).toEqual({ transports: ["wrtc"] });
46
+ });
47
+ it("rejects capabilities without transports", () => {
48
+ expect(parseSignalMessage(frame("capabilities", {}))).toBeUndefined();
49
+ expect(parseSignalMessage(frame("capabilities", { transports: [] }))).toBeUndefined();
50
+ expect(parseSignalMessage(frame("capabilities", { transports: [42] }))).toBeUndefined();
51
+ });
52
+ it("rejects capabilities with malformed info", () => {
53
+ expect(parseSignalMessage(frame("capabilities", { transports: ["wrtc"], info: { name: "No identity" } }))).toBeUndefined();
54
+ expect(parseSignalMessage(frame("capabilities", {
55
+ transports: ["wrtc"],
56
+ info: {
57
+ identity: "com.example.wallet",
58
+ name: "Example Wallet",
59
+ icon: "x".repeat(10000),
60
+ },
61
+ }))).toBeUndefined();
62
+ });
63
+ it("rejects retired and unknown packet types", () => {
64
+ expect(parseSignalMessage(frame("ack", undefined))).toBeUndefined();
65
+ expect(parseSignalMessage(frame("nonsense", {}))).toBeUndefined();
66
+ expect(parseSignalMessage("not json")).toBeUndefined();
67
+ });
68
+ it("validates outgoing peer info with descriptive reasons", () => {
69
+ expect(validatePeerInfo({ identity: "com.example.wallet", name: "Example Wallet" }))
70
+ .toBeUndefined();
71
+ expect(validatePeerInfo({
72
+ identity: "com.example.wallet",
73
+ name: "Example Wallet",
74
+ icon: "data:image/png;base64,aaaa",
75
+ })).toBeUndefined();
76
+ expect(validatePeerInfo({ identity: "", name: "Example Wallet" }))
77
+ .toMatch(/identity/);
78
+ expect(validatePeerInfo({ identity: "com.example.wallet", name: "x".repeat(200) }))
79
+ .toMatch(/name/);
80
+ expect(validatePeerInfo({
81
+ identity: "com.example.wallet",
82
+ name: "Example Wallet",
83
+ icon: "x".repeat(MAX_ICON_LENGTH + 1),
84
+ })).toMatch(/icon/);
85
+ });
86
+ it("still parses flash, pubkey, and data", () => {
87
+ expect(parseSignalMessage(frame("flash", {}))).toMatchObject({ type: "flash" });
88
+ expect(parseSignalMessage(frame("pubkey", { publicKey: "abc" }))).toMatchObject({ type: "pubkey" });
89
+ expect(parseSignalMessage(frame("data", { anything: true }))).toMatchObject({ type: "data" });
90
+ });
91
+ });
92
+ //# sourceMappingURL=messages.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"messages.test.js","sourceRoot":"","sources":["../src/messages.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, expect, it } from \"vitest\";\n\nimport { MAX_ICON_LENGTH, parseSignalMessage, validatePeerInfo } from \"./messages.js\";\n\nconst frame = (type: string, payload: unknown) =>\n JSON.stringify({ type, payload, timestamp: 1_750_000_000_000 });\n\ndescribe(\"parseSignalMessage\", () => {\n it(\"parses a capabilities packet with info\", () => {\n const message = parseSignalMessage(\n frame(\"capabilities\", {\n transports: [\"wrtc\", \"ws\"],\n info: {\n identity: \"com.example.wallet\",\n name: \"Example Wallet\",\n icon: \"data:image/png;base64,aaaa\",\n },\n }),\n );\n\n expect(message).toEqual({\n type: \"capabilities\",\n payload: {\n transports: [\"wrtc\", \"ws\"],\n info: {\n identity: \"com.example.wallet\",\n name: \"Example Wallet\",\n icon: \"data:image/png;base64,aaaa\",\n },\n },\n timestamp: 1_750_000_000_000,\n });\n });\n\n it(\"accepts an icon that is a URL — content vetting is the renderer's job\", () => {\n const message = parseSignalMessage(\n frame(\"capabilities\", {\n transports: [\"wrtc\"],\n info: {\n identity: \"com.example.wallet\",\n name: \"Example Wallet\",\n icon: \"https://example.com/icon.png\",\n },\n }),\n );\n\n expect(message?.payload).toMatchObject({\n info: { icon: \"https://example.com/icon.png\" },\n });\n });\n\n it(\"parses a capabilities packet without info\", () => {\n expect(\n parseSignalMessage(frame(\"capabilities\", { transports: [\"wrtc\"] })),\n ).toMatchObject({ payload: { transports: [\"wrtc\"] } });\n });\n\n it(\"drops unknown extra fields from capabilities payloads\", () => {\n const message = parseSignalMessage(\n frame(\"capabilities\", { transports: [\"wrtc\"], extra: \"field\" }),\n );\n\n expect(message?.payload).toEqual({ transports: [\"wrtc\"] });\n });\n\n it(\"rejects capabilities without transports\", () => {\n expect(parseSignalMessage(frame(\"capabilities\", {}))).toBeUndefined();\n expect(parseSignalMessage(frame(\"capabilities\", { transports: [] }))).toBeUndefined();\n expect(\n parseSignalMessage(frame(\"capabilities\", { transports: [42] })),\n ).toBeUndefined();\n });\n\n it(\"rejects capabilities with malformed info\", () => {\n expect(\n parseSignalMessage(\n frame(\"capabilities\", { transports: [\"wrtc\"], info: { name: \"No identity\" } }),\n ),\n ).toBeUndefined();\n expect(\n parseSignalMessage(\n frame(\"capabilities\", {\n transports: [\"wrtc\"],\n info: {\n identity: \"com.example.wallet\",\n name: \"Example Wallet\",\n icon: \"x\".repeat(10_000),\n },\n }),\n ),\n ).toBeUndefined();\n });\n\n it(\"rejects retired and unknown packet types\", () => {\n expect(parseSignalMessage(frame(\"ack\", undefined))).toBeUndefined();\n expect(parseSignalMessage(frame(\"nonsense\", {}))).toBeUndefined();\n expect(parseSignalMessage(\"not json\")).toBeUndefined();\n });\n\n it(\"validates outgoing peer info with descriptive reasons\", () => {\n expect(validatePeerInfo({ identity: \"com.example.wallet\", name: \"Example Wallet\" }))\n .toBeUndefined();\n expect(validatePeerInfo({\n identity: \"com.example.wallet\",\n name: \"Example Wallet\",\n icon: \"data:image/png;base64,aaaa\",\n })).toBeUndefined();\n expect(validatePeerInfo({ identity: \"\", name: \"Example Wallet\" }))\n .toMatch(/identity/);\n expect(validatePeerInfo({ identity: \"com.example.wallet\", name: \"x\".repeat(200) }))\n .toMatch(/name/);\n expect(validatePeerInfo({\n identity: \"com.example.wallet\",\n name: \"Example Wallet\",\n icon: \"x\".repeat(MAX_ICON_LENGTH + 1),\n })).toMatch(/icon/);\n });\n\n it(\"still parses flash, pubkey, and data\", () => {\n expect(parseSignalMessage(frame(\"flash\", {}))).toMatchObject({ type: \"flash\" });\n expect(\n parseSignalMessage(frame(\"pubkey\", { publicKey: \"abc\" })),\n ).toMatchObject({ type: \"pubkey\" });\n expect(\n parseSignalMessage(frame(\"data\", { anything: true })),\n ).toMatchObject({ type: \"data\" });\n });\n});\n"]}
@@ -7,24 +7,21 @@ import { log } from "../utils/log.js";
7
7
  *
8
8
  * https://openlv.sh/api/signaling/mqtt
9
9
  */
10
- export const mqtt = ({ url = "wss://test.mosquitto.org:8081/mqtt", topic, }) => {
10
+ export const mqtt = ({ url, topic }) => {
11
+ // `url` may be an empty string when the session URI omits `s`.
12
+ const endpoint = url || "wss://test.mosquitto.org:8081/mqtt";
11
13
  let connection;
12
14
  return createSignalingLayer({
13
15
  type: "mqtt",
14
16
  setup() {
15
17
  return new Promise((resolve, reject) => {
16
- connection = createMqtt({
17
- url,
18
- // reconnectOnConnackError: false,
19
- // reconnectPeriod: 5000,
20
- // connectTimeout: 5000,
21
- });
18
+ connection = createMqtt({ url: endpoint });
22
19
  connection.on("connect", () => {
23
20
  resolve();
24
21
  });
25
22
  connection.on("error", (error) => {
26
- console.error("MQTT: Error connecting to URL", error);
27
- reject(error);
23
+ log("MQTT: Error connecting to URL", error);
24
+ reject(error instanceof Error ? error : new Error(String(error)));
28
25
  });
29
26
  connection.on("close", () => {
30
27
  reject(new Error("MQTT: Closed connection to URL"));
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/mqtt/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,EAAE,UAAU,EAAmB,MAAM,gBAAgB,CAAC;AAE7D,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEnD,OAAO,EAAE,GAAG,EAAE,MAAM,iBAAiB,CAAC;AAEtC;;;;GAIG;AACH,MAAM,CAAC,MAAM,IAAI,GAAsB,CAAC,EACtC,GAAG,GAAG,oCAAoC,EAC1C,KAAK,GACN,EAAE,EAAE;IACH,IAAI,UAAkC,CAAC;IAEvC,OAAO,oBAAoB,CAAC;QAC1B,IAAI,EAAE,MAAM;QACZ,KAAK;YACH,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;gBACrC,UAAU,GAAG,UAAU,CAAC;oBACtB,GAAG;oBACH,kCAAkC;oBAClC,yBAAyB;oBACzB,wBAAwB;iBACzB,CAAC,CAAC;gBAEH,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE;oBAC5B,OAAO,EAAE,CAAC;gBACZ,CAAC,CAAC,CAAC;gBACH,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;oBAC/B,OAAO,CAAC,KAAK,CAAC,+BAA+B,EAAE,KAAK,CAAC,CAAC;oBACtD,MAAM,CAAC,KAAK,CAAC,CAAC;gBAChB,CAAC,CAAC,CAAC;gBAEH,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;oBAC1B,MAAM,CAAC,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC,CAAC;gBACtD,CAAC,CAAC,CAAC;gBAEH,UAAU,CAAC,OAAO,EAAE,CAAC;YACvB,CAAC,CAAC,CAAC;QACL,CAAC;QACD,QAAQ;YACN,UAAU,EAAE,KAAK,EAAE,CAAC;YACpB,UAAU,GAAG,SAAS,CAAC;QACzB,CAAC;QACD,KAAK,CAAC,OAAO,CAAC,OAAO;YACnB,IAAI,CAAC,UAAU,EAAE,CAAC;gBAChB,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;YACvD,CAAC;YAED,UAAU,EAAE,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;QACzD,CAAC;QACD,KAAK,CAAC,SAAS,CAAC,OAAO;YACrB,IAAI,CAAC,UAAU;gBAAE,MAAM,IAAI,uBAAuB,EAAE,CAAC;YAErD,GAAG,CAAC,4BAA4B,EAAE,KAAK,CAAC,CAAC;YAEzC,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,aAAa,EAAE,OAAO,EAAE,EAAE;gBAClD,GAAG,CAAC,iCAAiC,EAAE,KAAK,CAAC,CAAC;gBAE9C,IAAI,aAAa,KAAK,KAAK;oBAAE,OAAO;gBAEpC,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;gBAElD,OAAO,CAAC,OAAO,CAAC,CAAC;YACnB,CAAC,CAAC,CAAC;YAEH,MAAM,UAAU,EAAE,SAAS,CAAC,KAAK,CAAC,CAAC;QACrC,CAAC;KACF,CAAC,CAAC;AACL,CAAC,CAAC;AAEF,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC","sourcesContent":["import { SignalNoConnectionError } from \"@openlv/core/errors\";\nimport { createMqtt, type MqttClient } from \"websocket-mqtt\";\n\nimport { createSignalingLayer } from \"../index.js\";\nimport type { SignalingProtocol } from \"../protocol.js\";\nimport { log } from \"../utils/log.js\";\n\n/**\n * MQTT Signaling Layer\n *\n * https://openlv.sh/api/signaling/mqtt\n */\nexport const mqtt: SignalingProtocol = ({\n url = \"wss://test.mosquitto.org:8081/mqtt\",\n topic,\n}) => {\n let connection: MqttClient | undefined;\n\n return createSignalingLayer({\n type: \"mqtt\",\n setup() {\n return new Promise((resolve, reject) => {\n connection = createMqtt({\n url,\n // reconnectOnConnackError: false,\n // reconnectPeriod: 5000,\n // connectTimeout: 5000,\n });\n\n connection.on(\"connect\", () => {\n resolve();\n });\n connection.on(\"error\", (error) => {\n console.error(\"MQTT: Error connecting to URL\", error);\n reject(error);\n });\n\n connection.on(\"close\", () => {\n reject(new Error(\"MQTT: Closed connection to URL\"));\n });\n\n connection.connect();\n });\n },\n teardown() {\n connection?.close();\n connection = undefined;\n },\n async publish(payload) {\n if (!connection) {\n throw new Error(\"MQTT: No connection to publish to\");\n }\n\n connection?.publish(topic, payload, { retain: false });\n },\n async subscribe(handler) {\n if (!connection) throw new SignalNoConnectionError();\n\n log(\"MQTT: Subscribing to topic\", topic);\n\n connection.on(\"message\", (receivedTopic, message) => {\n log(\"MQTT: Received message on topic\", topic);\n\n if (receivedTopic !== topic) return;\n\n const decoded = new TextDecoder().decode(message);\n\n handler(decoded);\n });\n\n await connection?.subscribe(topic);\n },\n });\n};\n\nObject.defineProperty(mqtt, \"__name\", { value: \"mqtt\" });\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/mqtt/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,EAAE,UAAU,EAAmB,MAAM,gBAAgB,CAAC;AAE7D,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEnD,OAAO,EAAE,GAAG,EAAE,MAAM,iBAAiB,CAAC;AAEtC;;;;GAIG;AACH,MAAM,CAAC,MAAM,IAAI,GAAsB,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,EAAE;IACxD,+DAA+D;IAC/D,MAAM,QAAQ,GAAG,GAAG,IAAI,oCAAoC,CAAC;IAC7D,IAAI,UAAkC,CAAC;IAEvC,OAAO,oBAAoB,CAAC;QAC1B,IAAI,EAAE,MAAM;QACZ,KAAK;YACH,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;gBACrC,UAAU,GAAG,UAAU,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC;gBAE3C,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE;oBAC5B,OAAO,EAAE,CAAC;gBACZ,CAAC,CAAC,CAAC;gBACH,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;oBAC/B,GAAG,CAAC,+BAA+B,EAAE,KAAK,CAAC,CAAC;oBAC5C,MAAM,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;gBACpE,CAAC,CAAC,CAAC;gBAEH,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;oBAC1B,MAAM,CAAC,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC,CAAC;gBACtD,CAAC,CAAC,CAAC;gBAEH,UAAU,CAAC,OAAO,EAAE,CAAC;YACvB,CAAC,CAAC,CAAC;QACL,CAAC;QACD,QAAQ;YACN,UAAU,EAAE,KAAK,EAAE,CAAC;YACpB,UAAU,GAAG,SAAS,CAAC;QACzB,CAAC;QACD,KAAK,CAAC,OAAO,CAAC,OAAO;YACnB,IAAI,CAAC,UAAU,EAAE,CAAC;gBAChB,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;YACvD,CAAC;YAED,UAAU,EAAE,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;QACzD,CAAC;QACD,KAAK,CAAC,SAAS,CAAC,OAAO;YACrB,IAAI,CAAC,UAAU;gBAAE,MAAM,IAAI,uBAAuB,EAAE,CAAC;YAErD,GAAG,CAAC,4BAA4B,EAAE,KAAK,CAAC,CAAC;YAEzC,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,aAAa,EAAE,OAAO,EAAE,EAAE;gBAClD,GAAG,CAAC,iCAAiC,EAAE,KAAK,CAAC,CAAC;gBAE9C,IAAI,aAAa,KAAK,KAAK;oBAAE,OAAO;gBAEpC,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;gBAElD,OAAO,CAAC,OAAO,CAAC,CAAC;YACnB,CAAC,CAAC,CAAC;YAEH,MAAM,UAAU,EAAE,SAAS,CAAC,KAAK,CAAC,CAAC;QACrC,CAAC;KACF,CAAC,CAAC;AACL,CAAC,CAAC;AAEF,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC","sourcesContent":["import { SignalNoConnectionError } from \"@openlv/core/errors\";\nimport { createMqtt, type MqttClient } from \"websocket-mqtt\";\n\nimport { createSignalingLayer } from \"../index.js\";\nimport type { SignalingProtocol } from \"../protocol.js\";\nimport { log } from \"../utils/log.js\";\n\n/**\n * MQTT Signaling Layer\n *\n * https://openlv.sh/api/signaling/mqtt\n */\nexport const mqtt: SignalingProtocol = ({ url, topic }) => {\n // `url` may be an empty string when the session URI omits `s`.\n const endpoint = url || \"wss://test.mosquitto.org:8081/mqtt\";\n let connection: MqttClient | undefined;\n\n return createSignalingLayer({\n type: \"mqtt\",\n setup() {\n return new Promise((resolve, reject) => {\n connection = createMqtt({ url: endpoint });\n\n connection.on(\"connect\", () => {\n resolve();\n });\n connection.on(\"error\", (error) => {\n log(\"MQTT: Error connecting to URL\", error);\n reject(error instanceof Error ? error : new Error(String(error)));\n });\n\n connection.on(\"close\", () => {\n reject(new Error(\"MQTT: Closed connection to URL\"));\n });\n\n connection.connect();\n });\n },\n teardown() {\n connection?.close();\n connection = undefined;\n },\n async publish(payload) {\n if (!connection) {\n throw new Error(\"MQTT: No connection to publish to\");\n }\n\n connection?.publish(topic, payload, { retain: false });\n },\n async subscribe(handler) {\n if (!connection) throw new SignalNoConnectionError();\n\n log(\"MQTT: Subscribing to topic\", topic);\n\n connection.on(\"message\", (receivedTopic, message) => {\n log(\"MQTT: Received message on topic\", topic);\n\n if (receivedTopic !== topic) return;\n\n const decoded = new TextDecoder().decode(message);\n\n handler(decoded);\n });\n\n await connection?.subscribe(topic);\n },\n });\n};\n\nObject.defineProperty(mqtt, \"__name\", { value: \"mqtt\" });\n"]}
@@ -1,4 +1,10 @@
1
1
  import type { SignalingProtocol } from "../protocol.js";
2
+ type NtfyAttachment = {
3
+ url: string;
4
+ name: string;
5
+ size: number;
6
+ type: string;
7
+ };
2
8
  export type NtfyMessage = {
3
9
  id: string;
4
10
  time: number;
@@ -6,6 +12,7 @@ export type NtfyMessage = {
6
12
  event: string;
7
13
  topic: string;
8
14
  message: string;
15
+ attachment?: NtfyAttachment;
9
16
  };
10
17
  /**
11
18
  * Ntfy Signaling Layer
@@ -24,3 +31,4 @@ export type NtfyMessage = {
24
31
  * https://openlv.sh/api/signaling/ntfy
25
32
  */
26
33
  export declare const ntfy: SignalingProtocol;
34
+ export {};
@@ -21,7 +21,7 @@ import { parseNtfyUrl } from "./url.js";
21
21
  */
22
22
  export const ntfy = ({ topic, url }) => {
23
23
  let connection;
24
- const connectionInfo = parseNtfyUrl(url);
24
+ const connectionInfo = parseNtfyUrl(url || "https://ntfy.sh");
25
25
  const wsProtocol = match(connectionInfo.protocol)
26
26
  .with("https", () => "wss")
27
27
  .with("http", () => "ws")
@@ -40,40 +40,51 @@ export const ntfy = ({ topic, url }) => {
40
40
  + (connectionInfo.parameters || "");
41
41
  log("NTFY: Connecting to WebSocket", wsUrl);
42
42
  connection = new WebSocket(wsUrl);
43
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
44
- connection?.addEventListener("error", (_event) => { });
45
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
46
- connection.addEventListener("close", (_event) => { });
47
- const awaitOpenConfirm = new Promise((resolve) => {
43
+ // Resolves once the server confirms the subscription ("open" event);
44
+ // rejects on socket error/close so setup cannot hang forever.
45
+ await new Promise((resolve, reject) => {
46
+ connection.addEventListener("error", () => {
47
+ reject(new Error(`NTFY: WebSocket error connecting to ${connectionInfo.host}`));
48
+ });
49
+ connection.addEventListener("close", () => {
50
+ reject(new Error("NTFY: WebSocket closed before subscription was confirmed"));
51
+ });
48
52
  connection.addEventListener("message", (event) => {
49
- log("NTFY: Received message:", event.data);
50
- const data = JSON.parse(event.data);
53
+ let data;
54
+ try {
55
+ data = JSON.parse(event.data);
56
+ }
57
+ catch {
58
+ log("NTFY: Dropping non-JSON frame");
59
+ return;
60
+ }
51
61
  if (data.event === "open") {
52
62
  resolve();
53
63
  }
54
- else if (data.event === "message") {
55
- events.emit("message", data.message);
64
+ else if (data.event === "message" && typeof data.message === "string") {
65
+ ((async () => {
66
+ if (data.attachment) {
67
+ const response = await fetch(data.attachment.url);
68
+ const text = await response.text();
69
+ events.emit("message", text);
70
+ }
71
+ else {
72
+ events.emit("message", data.message);
73
+ }
74
+ })());
56
75
  }
57
76
  });
58
77
  });
59
- const awaitOpen = new Promise((resolve) => {
60
- connection.addEventListener("open", () => {
61
- log("NTFY: Connected to WebSocket");
62
- resolve();
63
- });
64
- });
65
- await awaitOpen;
66
- await awaitOpenConfirm;
67
78
  },
68
79
  teardown() {
69
80
  connection?.close();
70
81
  },
71
82
  async publish(body) {
72
83
  const headers = { "Content-Type": "application/json" };
73
- // TODO: Add response handling
74
- // @ts-expect-error - TODO: Add response handling
75
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
76
- const _response = await fetch(`${connectionInfo.protocol}://${connectionInfo.host}/${topic}`, { method: "POST", body, headers });
84
+ const response = await fetch(`${connectionInfo.protocol}://${connectionInfo.host}/${topic}`, { method: "POST", body, headers });
85
+ if (!response.ok) {
86
+ throw new Error(`NTFY: publish failed with status ${response.status}`);
87
+ }
77
88
  },
78
89
  subscribe: (handler) => {
79
90
  events.on("message", handler);
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ntfy/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { EventEmitter } from \"eventemitter3\";\nimport { match } from \"ts-pattern\";\n\nimport { createSignalingLayer } from \"../index.js\";\nimport type { SignalingProtocol } from \"../protocol.js\";\nimport { log } from \"../utils/log.js\";\nimport { parseNtfyUrl } from \"./url.js\";\n\nexport type NtfyMessage = {\n // eslint-disable-next-line no-restricted-syntax\n id: string;\n time: number;\n expires: number;\n event: string;\n topic: string;\n message: string;\n};\n\n/**\n * Ntfy Signaling Layer\n *\n * Uses HTTP Post for publishing\n * Uses WebSocket for subscribing\n *\n * URL format supports the ?auth= parameter\n * example: https://ntfy.sh/mytopic?auth=mytoken\n *\n * The same goes for apprise ntfy urls\n * example: ntfy://{token}@{hostname}/{topic}\n * example: ntfy://{user}:{password}@{hostname}/{topic}\n * https variant: ntfys://{user}:{password}@{hostname}/{topic}\n *\n * https://openlv.sh/api/signaling/ntfy\n */\nexport const ntfy: SignalingProtocol = ({ topic, url }) => {\n let connection: WebSocket | undefined;\n const connectionInfo = parseNtfyUrl(url);\n const wsProtocol = match(connectionInfo.protocol)\n .with(\"https\", () => \"wss\")\n .with(\"http\", () => \"ws\")\n .exhaustive();\n const events = new EventEmitter<{ message: string; }>();\n\n return createSignalingLayer({\n type: \"ntfy\",\n async setup() {\n log(\"NTFY: Setting up\");\n\n const wsUrl\n = wsProtocol\n + \"://\"\n + connectionInfo.host\n + \"/\"\n + topic\n + \"/ws\"\n + (connectionInfo.parameters || \"\");\n\n log(\"NTFY: Connecting to WebSocket\", wsUrl);\n connection = new WebSocket(wsUrl);\n\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n connection?.addEventListener(\"error\", (_event) => {});\n\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n connection.addEventListener(\"close\", (_event) => {});\n\n const awaitOpenConfirm = new Promise<void>((resolve) => {\n connection!.addEventListener(\"message\", (event) => {\n log(\"NTFY: Received message:\", event.data);\n const data = JSON.parse(event.data) as NtfyMessage;\n\n if (data.event === \"open\") {\n resolve();\n }\n else if (data.event === \"message\") {\n events.emit(\"message\", data.message);\n }\n });\n });\n\n const awaitOpen = new Promise<void>((resolve) => {\n connection!.addEventListener(\"open\", () => {\n log(\"NTFY: Connected to WebSocket\");\n resolve();\n });\n });\n\n await awaitOpen;\n await awaitOpenConfirm;\n },\n teardown() {\n connection?.close();\n },\n async publish(body) {\n const headers = { \"Content-Type\": \"application/json\" } as HeadersInit;\n\n // TODO: Add response handling\n // @ts-expect-error - TODO: Add response handling\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const _response = await fetch(\n `${connectionInfo.protocol}://${connectionInfo.host}/${topic}`,\n { method: \"POST\", body, headers },\n );\n },\n subscribe: (handler) => {\n events.on(\"message\", handler);\n },\n });\n};\n\nObject.defineProperty(ntfy, \"__name\", { value: \"ntfy\" });\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ntfy/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { EventEmitter } from \"eventemitter3\";\nimport { match } from \"ts-pattern\";\n\nimport { createSignalingLayer } from \"../index.js\";\nimport type { SignalingProtocol } from \"../protocol.js\";\nimport { log } from \"../utils/log.js\";\nimport { parseNtfyUrl } from \"./url.js\";\n\ntype NtfyAttachment = {\n url: string;\n name: string;\n size: number;\n type: string;\n};\n\nexport type NtfyMessage = {\n // eslint-disable-next-line no-restricted-syntax\n id: string;\n time: number;\n expires: number;\n event: string;\n topic: string;\n message: string;\n attachment?: NtfyAttachment;\n};\n\n/**\n * Ntfy Signaling Layer\n *\n * Uses HTTP Post for publishing\n * Uses WebSocket for subscribing\n *\n * URL format supports the ?auth= parameter\n * example: https://ntfy.sh/mytopic?auth=mytoken\n *\n * The same goes for apprise ntfy urls\n * example: ntfy://{token}@{hostname}/{topic}\n * example: ntfy://{user}:{password}@{hostname}/{topic}\n * https variant: ntfys://{user}:{password}@{hostname}/{topic}\n *\n * https://openlv.sh/api/signaling/ntfy\n */\nexport const ntfy: SignalingProtocol = ({ topic, url }) => {\n let connection: WebSocket | undefined;\n const connectionInfo = parseNtfyUrl(url || \"https://ntfy.sh\");\n const wsProtocol = match(connectionInfo.protocol)\n .with(\"https\", () => \"wss\")\n .with(\"http\", () => \"ws\")\n .exhaustive();\n const events = new EventEmitter<{ message: string; }>();\n\n return createSignalingLayer({\n type: \"ntfy\",\n async setup() {\n log(\"NTFY: Setting up\");\n\n const wsUrl\n = wsProtocol\n + \"://\"\n + connectionInfo.host\n + \"/\"\n + topic\n + \"/ws\"\n + (connectionInfo.parameters || \"\");\n\n log(\"NTFY: Connecting to WebSocket\", wsUrl);\n connection = new WebSocket(wsUrl);\n\n // Resolves once the server confirms the subscription (\"open\" event);\n // rejects on socket error/close so setup cannot hang forever.\n await new Promise<void>((resolve, reject) => {\n connection!.addEventListener(\"error\", () => {\n reject(new Error(`NTFY: WebSocket error connecting to ${connectionInfo.host}`));\n });\n\n connection!.addEventListener(\"close\", () => {\n reject(new Error(\"NTFY: WebSocket closed before subscription was confirmed\"));\n });\n\n connection!.addEventListener(\"message\", (event) => {\n let data: NtfyMessage;\n\n try {\n data = JSON.parse(event.data) as NtfyMessage;\n }\n catch {\n log(\"NTFY: Dropping non-JSON frame\");\n\n return;\n }\n\n if (data.event === \"open\") {\n resolve();\n }\n else if (data.event === \"message\" && typeof data.message === \"string\") {\n ((async () => {\n if (data.attachment) {\n const response = await fetch(data.attachment.url);\n const text = await response.text();\n\n events.emit(\"message\", text);\n }\n else {\n events.emit(\"message\", data.message);\n }\n })());\n }\n });\n });\n },\n teardown() {\n connection?.close();\n },\n async publish(body) {\n const headers = { \"Content-Type\": \"application/json\" } as HeadersInit;\n\n const response = await fetch(\n `${connectionInfo.protocol}://${connectionInfo.host}/${topic}`,\n { method: \"POST\", body, headers },\n );\n\n if (!response.ok) {\n throw new Error(`NTFY: publish failed with status ${response.status}`);\n }\n },\n subscribe: (handler) => {\n events.on(\"message\", handler);\n },\n });\n};\n\nObject.defineProperty(ntfy, \"__name\", { value: \"ntfy\" });\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,2 +1,3 @@
1
- export const log = (...x) => console.log("%c[signaling]%c", "color: lightblue; font-weight: bold", "color: inherit; font-weight: normal", ...x);
1
+ import { createLogger } from "@openlv/core";
2
+ export const log = createLogger("signaling", "lightblue");
2
3
  //# sourceMappingURL=log.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"log.js","sourceRoot":"","sources":["../../src/utils/log.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,GAAG,GAAG,CAAC,GAAG,CAAiC,EAAE,EAAE,CAC1D,OAAO,CAAC,GAAG,CACT,iBAAiB,EACjB,qCAAqC,EACrC,qCAAqC,EACrC,GAAG,CAAC,CACL,CAAC","sourcesContent":["export const log = (...x: Parameters<typeof console.log>) =>\n console.log(\n \"%c[signaling]%c\",\n \"color: lightblue; 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,WAAW,CAAC,CAAC","sourcesContent":["import { createLogger } from \"@openlv/core\";\n\nexport const log = createLogger(\"signaling\", \"lightblue\");\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openlv/signaling",
3
- "version": "0.0.3",
3
+ "version": "0.1.1",
4
4
  "keywords": [
5
5
  "open-lavatory"
6
6
  ],
@@ -59,7 +59,7 @@
59
59
  "gun": "^0.2020.1241",
60
60
  "websocket-mqtt": "0.0.7",
61
61
  "ts-pattern": "^5",
62
- "@openlv/core": "0.0.2"
62
+ "@openlv/core": "0.1.0"
63
63
  },
64
64
  "devDependencies": {
65
65
  "typescript": "^5",