@vex-chat/spire 0.7.4 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/README.md +82 -26
  2. package/dist/ClientManager.d.ts +24 -25
  3. package/dist/ClientManager.js +232 -509
  4. package/dist/ClientManager.js.map +1 -0
  5. package/dist/Database.d.ts +49 -41
  6. package/dist/Database.js +698 -716
  7. package/dist/Database.js.map +1 -0
  8. package/dist/Spire.d.ts +23 -15
  9. package/dist/Spire.js +518 -218
  10. package/dist/Spire.js.map +1 -0
  11. package/dist/__tests__/Database.spec.js +113 -73
  12. package/dist/__tests__/Database.spec.js.map +1 -0
  13. package/dist/db/schema.d.ts +134 -0
  14. package/dist/db/schema.js +2 -0
  15. package/dist/db/schema.js.map +1 -0
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.js +3 -5
  18. package/dist/index.js.map +1 -0
  19. package/dist/middleware/validate.d.ts +12 -0
  20. package/dist/middleware/validate.js +35 -0
  21. package/dist/middleware/validate.js.map +1 -0
  22. package/dist/migrations/2026-04-06_initial-schema.d.ts +3 -0
  23. package/dist/migrations/2026-04-06_initial-schema.js +192 -0
  24. package/dist/migrations/2026-04-06_initial-schema.js.map +1 -0
  25. package/dist/run.js +26 -21
  26. package/dist/run.js.map +1 -0
  27. package/dist/server/avatar.d.ts +3 -4
  28. package/dist/server/avatar.js +85 -67
  29. package/dist/server/avatar.js.map +1 -0
  30. package/dist/server/errors.d.ts +59 -0
  31. package/dist/server/errors.js +94 -0
  32. package/dist/server/errors.js.map +1 -0
  33. package/dist/server/file.d.ts +3 -4
  34. package/dist/server/file.js +101 -61
  35. package/dist/server/file.js.map +1 -0
  36. package/dist/server/index.d.ts +9 -6
  37. package/dist/server/index.js +595 -70
  38. package/dist/server/index.js.map +1 -0
  39. package/dist/server/invite.d.ts +4 -5
  40. package/dist/server/invite.js +21 -103
  41. package/dist/server/invite.js.map +1 -0
  42. package/dist/server/openapi.d.ts +2 -0
  43. package/dist/server/openapi.js +40 -0
  44. package/dist/server/openapi.js.map +1 -0
  45. package/dist/server/permissions.d.ts +16 -0
  46. package/dist/server/permissions.js +22 -0
  47. package/dist/server/permissions.js.map +1 -0
  48. package/dist/server/rateLimit.d.ts +28 -0
  49. package/dist/server/rateLimit.js +58 -0
  50. package/dist/server/rateLimit.js.map +1 -0
  51. package/dist/server/user.d.ts +4 -7
  52. package/dist/server/user.js +66 -76
  53. package/dist/server/user.js.map +1 -0
  54. package/dist/server/utils.d.ts +35 -7
  55. package/dist/server/utils.js +50 -6
  56. package/dist/server/utils.js.map +1 -0
  57. package/dist/types/express.d.ts +20 -0
  58. package/dist/types/express.js +2 -0
  59. package/dist/types/express.js.map +1 -0
  60. package/dist/utils/createLogger.js +13 -19
  61. package/dist/utils/createLogger.js.map +1 -0
  62. package/dist/utils/createUint8UUID.js +6 -10
  63. package/dist/utils/createUint8UUID.js.map +1 -0
  64. package/dist/utils/jwtSecret.d.ts +7 -0
  65. package/dist/utils/jwtSecret.js +15 -0
  66. package/dist/utils/jwtSecret.js.map +1 -0
  67. package/dist/utils/loadEnv.js +7 -22
  68. package/dist/utils/loadEnv.js.map +1 -0
  69. package/dist/utils/msgpack.d.ts +2 -0
  70. package/dist/utils/msgpack.js +4 -0
  71. package/dist/utils/msgpack.js.map +1 -0
  72. package/package.json +91 -63
  73. package/src/ClientManager.ts +434 -0
  74. package/src/Database.ts +925 -0
  75. package/src/Spire.ts +878 -0
  76. package/src/__tests__/Database.spec.ts +167 -0
  77. package/src/ambient-modules.d.ts +1 -0
  78. package/src/db/schema.ts +165 -0
  79. package/src/index.ts +3 -0
  80. package/src/middleware/validate.ts +38 -0
  81. package/src/migrations/2026-04-06_initial-schema.ts +218 -0
  82. package/src/run.ts +37 -0
  83. package/src/server/avatar.ts +141 -0
  84. package/src/server/errors.ts +133 -0
  85. package/src/server/file.ts +172 -0
  86. package/src/server/index.ts +855 -0
  87. package/src/server/invite.ts +65 -0
  88. package/src/server/openapi.ts +51 -0
  89. package/src/server/permissions.ts +40 -0
  90. package/src/server/rateLimit.ts +86 -0
  91. package/src/server/user.ts +125 -0
  92. package/src/server/utils.ts +59 -0
  93. package/src/types/express.ts +23 -0
  94. package/src/utils/createLogger.ts +47 -0
  95. package/src/utils/createUint8UUID.ts +9 -0
  96. package/src/utils/jwtSecret.ts +16 -0
  97. package/src/utils/loadEnv.ts +15 -0
  98. package/src/utils/msgpack.ts +4 -0
  99. package/avatars/169d76cb-6e7c-4e24-8224-017673eed8ff +0 -0
  100. package/avatars/1cae8d0b-0c6b-4c73-b25c-2d2349a57122 +0 -0
  101. package/avatars/1d87bc2b-71fc-4818-8004-40d04093e5fc +0 -0
  102. package/avatars/1f2c3d62-8b4d-465a-9895-51a24caef00d +0 -0
  103. package/avatars/245ee7fc-1004-41ab-adab-a04c9ceb9d7a +0 -0
  104. package/avatars/2465c28c-bdaf-4fa2-b42a-d054f04dc39b +0 -0
  105. package/avatars/3900a674-a2dd-4996-a61d-8c29b3270f41 +0 -0
  106. package/avatars/3c3b9c77-ea50-45e7-bb25-65d6f3d2a219 +0 -0
  107. package/avatars/414a2ff4-ad2f-4a7d-aa27-3b09ad3522b2 +0 -0
  108. package/avatars/522fe504-f0ad-4ed4-9dc6-e5dc2338e531 +0 -0
  109. package/avatars/53e5eb29-e7d1-4d58-add9-d44f39f0cfb7 +0 -0
  110. package/avatars/54d3f757-1038-41c8-bfb9-efd37b6e8ebe +0 -0
  111. package/avatars/623e86d7-c49c-46f6-9b76-ca70c9dbc43b +0 -0
  112. package/avatars/66e2abae-60f5-4dd1-b9fb-297a4bedfeb0 +0 -0
  113. package/avatars/6f37980e-f6fa-4d6d-9206-24050b403f45 +0 -0
  114. package/avatars/80138ece-eb5c-4b20-817b-903d6b0f54ae +0 -0
  115. package/avatars/841c77d3-37c4-431b-be22-672888062874 +0 -0
  116. package/avatars/88051a61-2bda-4750-95a3-5fb0b4918149 +0 -0
  117. package/avatars/89540973-c421-4bf1-8b89-9f1eaa51c1b5 +0 -0
  118. package/avatars/8a802fad-8c99-4942-8f80-47cec600149c +0 -0
  119. package/avatars/90531d8a-907a-4a1a-ac45-c85e4acb0df9 +0 -0
  120. package/avatars/9b7d0da9-b8d6-4801-b128-9993f79f464a +0 -0
  121. package/avatars/9bc456f1-c4c4-48a1-b9e6-fd44dd744a72 +0 -0
  122. package/avatars/9cf878bf-7430-49ec-a47a-78f3c93793dd +0 -0
  123. package/avatars/9ee82847-6ad3-45e5-92b9-f474a6c54d96 +0 -0
  124. package/avatars/ab44c857-d81d-4c88-85db-32f9532e5376 +0 -0
  125. package/avatars/b396a8d2-dc14-48d2-aac4-3755dc637051 +0 -0
  126. package/avatars/b6ac11c5-a8b2-4e0a-995a-9c87f1e58787 +0 -0
  127. package/avatars/b79d6855-b738-434c-be32-809637e62b9b +0 -0
  128. package/avatars/bbcd0188-d6a5-48ae-90fb-be5ff30599ab +0 -0
  129. package/avatars/bc7a9e0e-4720-4a6e-a90d-c11fec94d380 +0 -0
  130. package/avatars/c1c4889f-8383-4041-8bdd-9fded4046f37 +0 -0
  131. package/avatars/c4c7203c-d93a-4749-ade2-17053acf1d2a +0 -0
  132. package/avatars/ca974a70-0a23-4668-8b80-c4304dc7f793 +0 -0
  133. package/avatars/cf119a0d-eb3f-4bed-905b-f14a876c3535 +0 -0
  134. package/avatars/d464b03d-30c2-49e3-a666-80aefa8a1b35 +0 -0
  135. package/avatars/da0eee89-82f0-4d45-ab48-7d2786b634c5 +0 -0
  136. package/avatars/de4c77a5-68e9-4bb2-b40d-d964bf377d61 +0 -0
  137. package/avatars/dea95395-7d0b-42aa-a9ed-40c7d4fb4c48 +0 -0
  138. package/avatars/edb30749-59ba-4aa2-9c52-0fb22048f4cf +0 -0
  139. package/avatars/f17c245a-af7d-445b-9365-49f7f54b1eeb +0 -0
  140. package/avatars/f1ee6a35-b262-4dbf-99f5-3d011e3b98ec +0 -0
  141. package/avatars/f802bdd0-345d-41f6-9184-0f30e1258fb3 +0 -0
  142. package/dist/migrations/20210103192527_users.d.ts +0 -3
  143. package/dist/migrations/20210103192527_users.js +0 -30
  144. package/dist/migrations/20210103193502_mail.d.ts +0 -3
  145. package/dist/migrations/20210103193502_mail.js +0 -35
  146. package/dist/migrations/20210103193525_preKeys.d.ts +0 -3
  147. package/dist/migrations/20210103193525_preKeys.js +0 -30
  148. package/dist/migrations/20210103193553_oneTimeKeys.d.ts +0 -3
  149. package/dist/migrations/20210103193553_oneTimeKeys.js +0 -30
  150. package/dist/migrations/20210103193615_servers.d.ts +0 -3
  151. package/dist/migrations/20210103193615_servers.js +0 -28
  152. package/dist/migrations/20210103193729_channels.d.ts +0 -3
  153. package/dist/migrations/20210103193729_channels.js +0 -28
  154. package/dist/migrations/20210103193749_permissions.d.ts +0 -3
  155. package/dist/migrations/20210103193749_permissions.js +0 -30
  156. package/dist/migrations/20210103193801_files.d.ts +0 -3
  157. package/dist/migrations/20210103193801_files.js +0 -28
  158. package/files/00ea7368-45a7-4f6a-a199-974da14be1a0 +0 -0
  159. package/files/039503d2-a170-4962-b921-c97994ba64ff +0 -0
  160. package/files/14b6fa02-4cbb-40df-be4f-a07187cb619e +0 -0
  161. package/files/15c04cb1-dc6a-4f19-aa6f-4a3b92d05bf7 +0 -0
  162. package/files/2a8d411c-8b92-4532-b84d-d64c638d6293 +0 -0
  163. package/files/37de2cd3-08a8-4044-9c37-e13386765f3d +0 -0
  164. package/files/42452029-284e-4c81-9f18-feb6ce309eed +0 -0
  165. package/files/43d09f2f-29c8-415f-8c4a-23f2a32eb79e +0 -0
  166. package/files/52992923-33ab-44a1-8118-605e9b4856a7 +0 -0
  167. package/files/53180681-36e2-49c0-8382-94dca0da09bf +0 -0
  168. package/files/5a56cd7b-1d04-4619-b60f-1e5515b9164a +0 -0
  169. package/files/5ced7676-20c4-4219-a4f2-70a25eb7eea8 +0 -0
  170. package/files/60e787ff-8ec5-444d-b963-0aaf5313b53e +0 -0
  171. package/files/67a17729-fcb7-4339-9499-1fc08fea72ca +0 -0
  172. package/files/68d09565-908d-4a67-8f09-e183f8708eb4 +0 -0
  173. package/files/70e587c5-56e8-47f0-bd36-691efcb0cc2e +0 -4
  174. package/files/7a227619-b715-4e2c-a79d-b2158d56a799 +0 -0
  175. package/files/7e3cc3ea-b706-4835-994d-65d33acaf369 +0 -0
  176. package/files/816f72a0-65dc-40c1-8862-1d22065cca3c +0 -0
  177. package/files/8bf84972-5086-4631-a752-093d7b1a098b +0 -0
  178. package/files/8c46e3bc-3f2e-441d-b8cc-17428fc3d219 +0 -0
  179. package/files/8eb83364-8826-4eee-895a-ba5cd3ab85e9 +0 -0
  180. package/files/8faefaea-14e3-49e4-ac74-9710622bfae9 +0 -0
  181. package/files/91af08c2-9dca-41f4-b6c6-b7bf33505df9 +0 -0
  182. package/files/9349dffa-35dd-49a0-a651-10b438038f95 +0 -0
  183. package/files/b0a12a41-6283-4f27-b2c8-66774991d96c +0 -0
  184. package/files/b28afb08-d18b-48a8-93c3-6a59c66d8fee +0 -0
  185. package/files/be60fe01-1578-4363-a908-41500092b77d +0 -0
  186. package/files/cf7b90e3-734a-4453-9ff3-9a331404b5ef +0 -0
  187. package/files/d1be30aa-8ff4-41c1-b360-f65922029047 +0 -0
  188. package/files/d2d31a57-a413-443c-9b97-fa9ca394ef0b +0 -0
  189. package/files/d357f223-b786-478d-a113-b53cb62acdab +0 -0
  190. package/files/d4a69ee0-05c4-4ff0-8753-624cdd0f24e9 +0 -0
  191. package/files/d57f411b-1874-4f00-a6b0-2f86de6b229d +0 -0
  192. package/files/d85aaa33-7f70-4a70-b3d2-cad667beb38c +0 -0
  193. package/files/db2fc236-dbe0-4d24-bfaf-884829fd090a +0 -0
  194. package/files/df2e20ec-6dcc-4402-94ee-7d1163f84197 +0 -0
  195. package/files/e0880ab5-6d49-4dc0-a5ec-0d3793a8a7b8 +0 -0
  196. package/files/e59ee9c6-5aea-48ea-b3d9-94a324dc2260 +0 -0
  197. package/files/e6d7aad6-4220-4ce7-85f8-89d0b1f0cc89 +0 -0
  198. package/files/f0bfc98c-3534-459d-931a-7c6e77bde153 +0 -0
  199. package/files/f8443740-050c-4714-9bf6-334783ae6ffd +0 -0
  200. package/files/fc405ae7-f9fb-455d-ac8c-0ca0d88d4115 +0 -0
  201. package/jest.config.js +0 -13
  202. package/spire.sqlite +0 -0
@@ -0,0 +1,434 @@
1
+ import type { Database } from "./Database.ts";
2
+ import type {
3
+ BaseMsg,
4
+ ChallMsg,
5
+ Device,
6
+ ErrMsg,
7
+ ReceiptMsg,
8
+ ResourceMsg,
9
+ RespMsg,
10
+ SuccessMsg,
11
+ User,
12
+ UserRecord,
13
+ } from "@vex-chat/types";
14
+ import type winston from "winston";
15
+ import type WebSocket from "ws";
16
+
17
+ import { EventEmitter } from "events";
18
+ import { setTimeout as sleep } from "node:timers/promises";
19
+
20
+ import { xConcat, XUtils } from "@vex-chat/crypto";
21
+ import { xSignOpen } from "@vex-chat/crypto";
22
+ import { MailWSSchema, SocketAuthErrors } from "@vex-chat/types";
23
+
24
+ import pc from "picocolors";
25
+ import { parse as uuidParse, validate as uuidValidate } from "uuid";
26
+
27
+ import { type SpireOptions, TOKEN_EXPIRY } from "./Spire.ts";
28
+ import { createLogger } from "./utils/createLogger.ts";
29
+ import { createUint8UUID } from "./utils/createUint8UUID.ts";
30
+ import { msgpack } from "./utils/msgpack.ts";
31
+
32
+ export const POWER_LEVELS = {
33
+ CREATE: 50,
34
+ DELETE: 50,
35
+ EMOJI: 25,
36
+ INVITE: 25,
37
+ };
38
+
39
+ function emptyHeader() {
40
+ return new Uint8Array(32);
41
+ }
42
+
43
+ const MAX_MSG_SIZE = 2048;
44
+
45
+ export class ClientManager extends EventEmitter {
46
+ private alive: boolean = true;
47
+ private authed: boolean = false;
48
+ private challengeID: Uint8Array = createUint8UUID();
49
+ private conn: WebSocket;
50
+ private db: Database;
51
+ private device: Device | null;
52
+ private failed: boolean = false;
53
+ private log: winston.Logger;
54
+ private notify: (
55
+ userID: string,
56
+ event: string,
57
+ transmissionID: string,
58
+ data?: unknown,
59
+ deviceID?: string,
60
+ ) => void;
61
+ private user: null | UserRecord;
62
+ private userDetails: User;
63
+
64
+ constructor(
65
+ ws: WebSocket,
66
+ db: Database,
67
+ notify: (userID: string, event: string, transmissionID: string) => void,
68
+ userDetails: User,
69
+ options?: SpireOptions,
70
+ ) {
71
+ super();
72
+ this.conn = ws;
73
+ this.db = db;
74
+ this.user = null;
75
+ this.userDetails = userDetails;
76
+ this.device = null;
77
+ this.notify = notify;
78
+ this.log = createLogger("client-manager", options?.logLevel || "error");
79
+
80
+ this.initListeners();
81
+ this.challenge();
82
+ }
83
+
84
+ public getDevice(): Device {
85
+ if (!this.device) {
86
+ throw new Error("No device set on this client.");
87
+ }
88
+ return this.device;
89
+ }
90
+
91
+ public getUser(): UserRecord {
92
+ if (!this.authed || !this.user) {
93
+ throw new Error("You must be authed before getting user info.");
94
+ }
95
+ return this.user;
96
+ }
97
+
98
+ public send(msg: BaseMsg, header?: Uint8Array) {
99
+ if (header) {
100
+ this.log.debug(pc.bold(pc.red("OUTH")), header.toString());
101
+ } else {
102
+ this.log.debug(pc.bold(pc.red("OUTH")), emptyHeader.toString());
103
+ }
104
+
105
+ const packedMessage = packMessage(msg, header);
106
+
107
+ this.log.info(
108
+ pc.bold("⟶ ") +
109
+ responseColor(msg.type.toUpperCase()) +
110
+ " " +
111
+ this.toString() +
112
+ " " +
113
+ pc.yellow(Buffer.byteLength(packedMessage)),
114
+ );
115
+
116
+ this.log.debug(pc.bold(pc.red("OUT")), msg);
117
+ try {
118
+ this.conn.send(packedMessage);
119
+ } catch (err: unknown) {
120
+ this.log.warn(String(err));
121
+ this.fail();
122
+ }
123
+ }
124
+
125
+ public override toString() {
126
+ if (!this.user || !this.device) {
127
+ return "Unauthorized#0000";
128
+ }
129
+ return this.user.username + "<" + this.getDevice().deviceID + ">";
130
+ }
131
+
132
+ private authorize(transmissionID: string) {
133
+ this.authed = true;
134
+ this.sendAuthedMessage(transmissionID);
135
+ void this.db.markDeviceLogin(this.getDevice());
136
+ this.emit("authed");
137
+ }
138
+
139
+ private challenge() {
140
+ this.challengeID = new Uint8Array(uuidParse(crypto.randomUUID()));
141
+ const challenge: ChallMsg = {
142
+ challenge: this.challengeID,
143
+ transmissionID: crypto.randomUUID(),
144
+ type: "challenge",
145
+ };
146
+ this.send(challenge);
147
+ }
148
+
149
+ private fail() {
150
+ if (this.failed) {
151
+ return;
152
+ }
153
+ this.log.warn("Connection closed.");
154
+ this.conn.close();
155
+ this.failed = true;
156
+ this.emit("fail");
157
+ }
158
+
159
+ private async handleReceipt(msg: ReceiptMsg) {
160
+ await this.db.deleteMail(msg.nonce, this.getDevice().deviceID);
161
+ }
162
+
163
+ private initListeners() {
164
+ this.conn.on("open", () => {
165
+ setTimeout(() => {
166
+ if (!this.authed) {
167
+ this.conn.close();
168
+ }
169
+ }, TOKEN_EXPIRY);
170
+ void this.pingLoop();
171
+ });
172
+ this.conn.on("close", () => {
173
+ this.fail();
174
+ });
175
+ this.conn.on("message", (message: Buffer) => {
176
+ const [header, msg] = unpackMessage(message);
177
+ const size = Buffer.byteLength(message);
178
+
179
+ if (size > MAX_MSG_SIZE) {
180
+ this.sendErr(
181
+ msg.transmissionID,
182
+ "Message is too big. Received size " +
183
+ String(size) +
184
+ " while max size is " +
185
+ String(MAX_MSG_SIZE),
186
+ );
187
+ return;
188
+ }
189
+
190
+ this.log.info(
191
+ pc.bold("⟵ ") +
192
+ pc.bold(msg.type.toUpperCase()) +
193
+ " " +
194
+ this.toString() +
195
+ " " +
196
+ pc.yellow(String(size)),
197
+ );
198
+ this.log.debug(pc.bold(pc.red("INH")), header.toString());
199
+ this.log.debug(pc.bold(pc.red("IN")), msg);
200
+
201
+ if (!msg.type) {
202
+ this.sendErr(msg.transmissionID, "Message type is required.");
203
+ return;
204
+ }
205
+
206
+ if (!uuidValidate(msg.transmissionID)) {
207
+ this.sendErr(
208
+ crypto.randomUUID(),
209
+ "transmissionID is required and must be a valid uuid.",
210
+ );
211
+ return;
212
+ }
213
+
214
+ switch (msg.type) {
215
+ case "ping":
216
+ this.pong(msg.transmissionID);
217
+ break;
218
+ case "pong":
219
+ this.setAlive(true);
220
+ break;
221
+ case "receipt":
222
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- narrowed by msg.type
223
+ void this.handleReceipt(msg as ReceiptMsg);
224
+ break;
225
+ case "resource":
226
+ if (!this.authed) {
227
+ this.sendErr(
228
+ msg.transmissionID,
229
+ "You are not authenticated.",
230
+ );
231
+ break;
232
+ }
233
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- narrowed by msg.type
234
+ void this.parseResourceMsg(msg as ResourceMsg, header);
235
+ break;
236
+ case "response":
237
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- narrowed by msg.type
238
+ void this.verifyResponse(msg as RespMsg);
239
+ break;
240
+ default:
241
+ this.log.info("unsupported message %s", msg.type);
242
+ break;
243
+ }
244
+ });
245
+ }
246
+
247
+ private async parseResourceMsg(msg: ResourceMsg, header: Uint8Array) {
248
+ switch (msg.resourceType) {
249
+ case "mail":
250
+ if (msg.action === "CREATE") {
251
+ const mailResult = MailWSSchema.safeParse(msg.data);
252
+ if (!mailResult.success) {
253
+ this.sendErr(
254
+ msg.transmissionID,
255
+ "Invalid mail payload: " +
256
+ JSON.stringify(mailResult.error.issues),
257
+ );
258
+ return;
259
+ }
260
+ const mail = mailResult.data;
261
+
262
+ try {
263
+ await this.db.saveMail(
264
+ mail,
265
+ header,
266
+ this.getDevice().deviceID,
267
+ this.getUser().userID,
268
+ );
269
+ this.log.info("Received mail for " + mail.recipient);
270
+
271
+ const deviceDetails = await this.db.retrieveDevice(
272
+ mail.recipient,
273
+ );
274
+ if (!deviceDetails) {
275
+ this.sendErr(
276
+ msg.transmissionID,
277
+ "No associated user record found for device.",
278
+ );
279
+ return;
280
+ }
281
+
282
+ this.sendSuccess(msg.transmissionID, null);
283
+ this.notify(
284
+ deviceDetails.owner,
285
+ "mail",
286
+ msg.transmissionID,
287
+ null,
288
+ mail.recipient,
289
+ );
290
+ } catch (err: unknown) {
291
+ this.log.error(String(err));
292
+ this.sendErr(msg.transmissionID, String(err));
293
+ }
294
+ }
295
+ break;
296
+ default:
297
+ this.log.info("Unsupported resource type " + msg.resourceType);
298
+ }
299
+ }
300
+
301
+ private ping() {
302
+ if (!this.alive) {
303
+ this.fail();
304
+ return;
305
+ }
306
+ this.setAlive(false);
307
+ const p = { transmissionID: crypto.randomUUID(), type: "ping" };
308
+ this.send(p);
309
+ }
310
+
311
+ private async pingLoop() {
312
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- intentional infinite loop
313
+ while (true) {
314
+ this.ping();
315
+ await sleep(5000);
316
+ }
317
+ }
318
+
319
+ private pong(transmissionID: string) {
320
+ // ping is allowed before auth
321
+ if (this.user) {
322
+ void this.db.markUserSeen(this.user);
323
+ }
324
+
325
+ const p = { transmissionID, type: "pong" };
326
+ this.send(p);
327
+ }
328
+
329
+ private sendAuthedMessage(transmissionID: string) {
330
+ this.send({ transmissionID, type: "authorized" });
331
+ }
332
+
333
+ private sendAuthError(error: SocketAuthErrors) {
334
+ const msg = {
335
+ error,
336
+ transmissionID: crypto.randomUUID(),
337
+ type: "authErr",
338
+ };
339
+ this.send(msg);
340
+ }
341
+
342
+ private sendErr(transmissionID: string, message: string, data?: unknown) {
343
+ const error: ErrMsg = {
344
+ data,
345
+ error: message,
346
+ transmissionID,
347
+ type: "error",
348
+ };
349
+ this.send(error);
350
+ }
351
+
352
+ private sendSuccess(
353
+ transmissionID: string,
354
+ data: unknown,
355
+ header?: Uint8Array,
356
+ timestamp?: string,
357
+ ) {
358
+ const msg: SuccessMsg = {
359
+ data,
360
+ timestamp,
361
+ transmissionID,
362
+ type: "success",
363
+ };
364
+ this.send(msg, header);
365
+ }
366
+
367
+ private setAlive(status: boolean) {
368
+ this.alive = status;
369
+ }
370
+
371
+ private async verifyResponse(msg: RespMsg) {
372
+ const user = await this.db.retrieveUser(this.userDetails.userID);
373
+ if (user) {
374
+ const devices = await this.db.retrieveUserDeviceList([user.userID]);
375
+ let message: null | Uint8Array = null;
376
+ for (const device of devices) {
377
+ const verified = xSignOpen(
378
+ msg.signed,
379
+ XUtils.decodeHex(device.signKey),
380
+ );
381
+ if (verified) {
382
+ message = verified;
383
+ this.device = device;
384
+ }
385
+ }
386
+ if (!message) {
387
+ this.log.warn("Signature verification failed!");
388
+ this.sendAuthError(SocketAuthErrors.BadSignature);
389
+ this.fail();
390
+ return;
391
+ }
392
+
393
+ if (XUtils.bytesEqual(this.challengeID, message)) {
394
+ this.user = user;
395
+ this.authorize(msg.transmissionID);
396
+ } else {
397
+ this.log.warn("Token is bad!");
398
+ this.sendAuthError(SocketAuthErrors.InvalidToken);
399
+ }
400
+ } else {
401
+ this.log.info("User is not registered.");
402
+ this.sendAuthError(SocketAuthErrors.UserNotRegistered);
403
+
404
+ this.fail();
405
+ }
406
+ }
407
+ }
408
+
409
+ function packMessage(msg: unknown, header?: Uint8Array) {
410
+ const msgb = Uint8Array.from(msgpack.encode(msg));
411
+ const msgh = header || emptyHeader();
412
+ return xConcat(msgh, msgb);
413
+ }
414
+
415
+ function unpackMessage(msg: Buffer): [Uint8Array, BaseMsg] {
416
+ const msgp = Uint8Array.from(msg);
417
+
418
+ const msgh = msgp.slice(0, 32);
419
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- msgpack.decode returns any
420
+ const msgb: BaseMsg = msgpack.decode(msgp.slice(32));
421
+
422
+ return [msgh, msgb];
423
+ }
424
+
425
+ const responseColor = (status: string): string => {
426
+ switch (status) {
427
+ case "ERROR":
428
+ return pc.bold(pc.red(status));
429
+ case "SUCCESS":
430
+ return pc.bold(pc.green(status));
431
+ default:
432
+ return status;
433
+ }
434
+ };