@vex-chat/spire 0.8.0 → 1.0.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.
Files changed (373) hide show
  1. package/README.md +83 -28
  2. package/dist/ClientManager.d.ts +22 -26
  3. package/dist/ClientManager.js +198 -259
  4. package/dist/ClientManager.js.map +1 -0
  5. package/dist/Database.d.ts +71 -49
  6. package/dist/Database.js +737 -774
  7. package/dist/Database.js.map +1 -0
  8. package/dist/Spire.d.ts +20 -15
  9. package/dist/Spire.js +471 -249
  10. package/dist/Spire.js.map +1 -0
  11. package/dist/__tests__/Database.spec.js +102 -75
  12. package/dist/__tests__/Database.spec.js.map +1 -0
  13. package/dist/db/schema.d.ts +135 -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/migrations/2026-04-14_argon2id-password-hashing.d.ts +3 -0
  26. package/dist/migrations/2026-04-14_argon2id-password-hashing.js +12 -0
  27. package/dist/migrations/2026-04-14_argon2id-password-hashing.js.map +1 -0
  28. package/dist/run.js +25 -21
  29. package/dist/run.js.map +1 -0
  30. package/dist/server/avatar.d.ts +1 -4
  31. package/dist/server/avatar.js +63 -67
  32. package/dist/server/avatar.js.map +1 -0
  33. package/dist/server/errors.d.ts +56 -0
  34. package/dist/server/errors.js +68 -0
  35. package/dist/server/errors.js.map +1 -0
  36. package/dist/server/file.d.ts +2 -4
  37. package/dist/server/file.js +81 -64
  38. package/dist/server/file.js.map +1 -0
  39. package/dist/server/index.d.ts +7 -10
  40. package/dist/server/index.js +414 -416
  41. package/dist/server/index.js.map +1 -0
  42. package/dist/server/invite.d.ts +3 -5
  43. package/dist/server/invite.js +18 -52
  44. package/dist/server/invite.js.map +1 -0
  45. package/dist/server/openapi.d.ts +2 -0
  46. package/dist/server/openapi.js +40 -0
  47. package/dist/server/openapi.js.map +1 -0
  48. package/dist/server/permissions.d.ts +16 -0
  49. package/dist/server/permissions.js +22 -0
  50. package/dist/server/permissions.js.map +1 -0
  51. package/dist/server/rateLimit.d.ts +28 -0
  52. package/dist/server/rateLimit.js +58 -0
  53. package/dist/server/rateLimit.js.map +1 -0
  54. package/dist/server/user.d.ts +3 -7
  55. package/dist/server/user.js +56 -69
  56. package/dist/server/user.js.map +1 -0
  57. package/dist/server/utils.d.ts +35 -7
  58. package/dist/server/utils.js +50 -6
  59. package/dist/server/utils.js.map +1 -0
  60. package/dist/types/express.d.ts +20 -0
  61. package/dist/types/express.js +2 -0
  62. package/dist/types/express.js.map +1 -0
  63. package/dist/utils/createUint8UUID.js +6 -10
  64. package/dist/utils/createUint8UUID.js.map +1 -0
  65. package/dist/utils/jwtSecret.d.ts +7 -0
  66. package/dist/utils/jwtSecret.js +14 -0
  67. package/dist/utils/jwtSecret.js.map +1 -0
  68. package/dist/utils/loadEnv.js +7 -22
  69. package/dist/utils/loadEnv.js.map +1 -0
  70. package/dist/utils/msgpack.d.ts +2 -0
  71. package/dist/utils/msgpack.js +4 -0
  72. package/dist/utils/msgpack.js.map +1 -0
  73. package/package.json +93 -65
  74. package/src/ClientManager.ts +382 -0
  75. package/src/Database.ts +987 -0
  76. package/src/Spire.ts +807 -0
  77. package/src/__tests__/Database.spec.ts +150 -0
  78. package/src/ambient-modules.d.ts +1 -0
  79. package/src/db/schema.ts +166 -0
  80. package/src/index.ts +3 -0
  81. package/src/middleware/validate.ts +38 -0
  82. package/src/migrations/2026-04-06_initial-schema.ts +218 -0
  83. package/src/migrations/2026-04-14_argon2id-password-hashing.ts +18 -0
  84. package/src/run.ts +36 -0
  85. package/src/server/avatar.ts +134 -0
  86. package/src/server/errors.ts +104 -0
  87. package/src/server/file.ts +167 -0
  88. package/src/server/index.ts +841 -0
  89. package/src/server/invite.ts +63 -0
  90. package/src/server/openapi.ts +51 -0
  91. package/src/server/permissions.ts +40 -0
  92. package/src/server/rateLimit.ts +86 -0
  93. package/src/server/user.ts +121 -0
  94. package/src/server/utils.ts +59 -0
  95. package/src/types/express.ts +23 -0
  96. package/src/utils/createUint8UUID.ts +9 -0
  97. package/src/utils/jwtSecret.ts +15 -0
  98. package/src/utils/loadEnv.ts +15 -0
  99. package/src/utils/msgpack.ts +4 -0
  100. package/avatars/052242d0-4129-4a6e-8076-22709c157549 +0 -0
  101. package/avatars/0a677c9a-4986-4b2c-ae2e-12faf22f55db +0 -0
  102. package/avatars/0ba21b91-decb-4a3e-ac86-4dd54d805a9a +0 -0
  103. package/avatars/0c48d8b6-1d1b-4297-a6c6-2fe50af6fc35 +0 -0
  104. package/avatars/0d993cdf-19a6-4299-a4ee-a06579d106cf +0 -0
  105. package/avatars/17b000e4-ac38-46ec-9dec-d2568086129a +0 -0
  106. package/avatars/19dc5594-0f06-4ac6-af18-8740dd39ef6b +0 -0
  107. package/avatars/20444fa3-6d5e-429e-b55f-b81c3d2c61ee +0 -0
  108. package/avatars/21c0512a-5630-4931-9442-d66db66737be +0 -0
  109. package/avatars/22830a60-0b6f-4912-83a5-72245465f332 +0 -0
  110. package/avatars/243639ce-f59f-4404-a1f1-4ec0eb5d2af3 +0 -0
  111. package/avatars/30d2c01d-7b7f-4ea9-9859-1c90837a23f7 +0 -0
  112. package/avatars/315a04f0-9a6f-4b0f-bb9f-5fa774c4752b +0 -0
  113. package/avatars/3563d333-53fe-4885-ac2d-9a4f761db85e +0 -0
  114. package/avatars/36a10c00-3b4c-437f-8e1f-4428ecde0003 +0 -0
  115. package/avatars/40b83eeb-c6e8-4268-82ab-69799a796405 +0 -0
  116. package/avatars/45b5ddb9-ad2c-4404-8ab3-cb4699e6d61c +0 -0
  117. package/avatars/4e4f0ffb-9a75-479a-bccb-446d0bf85020 +0 -0
  118. package/avatars/4e62c3bd-08c6-4fdd-bd65-f01c7322ed64 +0 -0
  119. package/avatars/5004d2e7-51af-44af-8776-6c71f7019843 +0 -0
  120. package/avatars/5041eb29-5c4b-4dea-8c1b-31ba4473161a +0 -0
  121. package/avatars/5065cf78-31c5-46cb-8d5c-c0b6be2d994e +0 -0
  122. package/avatars/51b91d2c-8956-4d73-b4ad-ca6a8d9da9a8 +0 -0
  123. package/avatars/58264a2c-5651-4a42-8ca2-a9907b311e48 +0 -0
  124. package/avatars/58c2357c-8080-4725-a0ce-182c96b037c4 +0 -0
  125. package/avatars/59b5f6dd-8e04-4d15-b4dc-c1c652558a74 +0 -0
  126. package/avatars/5b417a78-b274-48bc-98a4-6e54b74ee62d +0 -0
  127. package/avatars/611f5a93-1ed4-45a1-bc8e-e8e413f9b171 +0 -0
  128. package/avatars/65abe919-9921-46f6-9bc9-183e9cc53c8a +0 -0
  129. package/avatars/6934202d-1546-4270-8a15-97ba8b8c6fa9 +0 -0
  130. package/avatars/6acd24be-f4b7-4399-9e7c-807821828d29 +0 -0
  131. package/avatars/6b2d6ac5-e35c-4297-994e-f0eb6fe56740 +0 -0
  132. package/avatars/6cae9ddd-f163-44e7-a632-30425716a159 +0 -0
  133. package/avatars/6d90e79e-b9c1-4b89-843b-96636de8d26b +0 -0
  134. package/avatars/6f82c7bb-a974-4372-8a64-ce287e668c8c +0 -0
  135. package/avatars/74a45091-5a76-4bb7-ae8a-cd7373adc128 +0 -0
  136. package/avatars/7d071ab2-e0b5-4dbb-8bf5-1fae50c3663f +0 -0
  137. package/avatars/7de818c8-bda1-4f51-976e-160fc087184b +0 -0
  138. package/avatars/873937df-8cb1-427a-92bf-f829f4259624 +0 -0
  139. package/avatars/8b45ffa5-3322-4109-bfa0-1be088336135 +0 -0
  140. package/avatars/8efaa426-22a9-42a2-b4ac-a275717b812f +0 -0
  141. package/avatars/903fd1a6-d6ea-431c-b98f-f21e424a2852 +0 -0
  142. package/avatars/943d8533-5174-4199-990f-1ec69e5d60c4 +0 -0
  143. package/avatars/952d014e-3804-4cd2-a4a0-ffe40a11e4ac +0 -0
  144. package/avatars/95d3acb0-724d-4413-b20f-edad55812d5d +0 -0
  145. package/avatars/9641a946-f613-471b-bedd-c1730b96b51e +0 -0
  146. package/avatars/9b01cbbf-f6b2-43fb-b569-589b6f2a8134 +0 -0
  147. package/avatars/9cd4424d-a34f-4467-acc0-93cf82703e0d +0 -0
  148. package/avatars/9d9ad3b0-e5a6-420a-a6c5-fb9085b70376 +0 -0
  149. package/avatars/9e9e34b5-4e63-4c4b-9722-c7f5674b47aa +0 -0
  150. package/avatars/a387d5c1-59eb-4a6b-80c1-a8982ed12c33 +0 -0
  151. package/avatars/a3e86d21-d881-4824-8ebf-45e3bf0f9186 +0 -0
  152. package/avatars/a8d5cc1c-3f42-4b7b-8d33-f9a9ef77f96b +0 -0
  153. package/avatars/a91d815f-badc-4604-a7be-6c7a44e6101d +0 -0
  154. package/avatars/aa8d0324-bcec-4737-a8c4-bdbff914148d +0 -0
  155. package/avatars/abb8a941-8b6b-47d7-a2f9-8b153ba44aa2 +0 -0
  156. package/avatars/b011bb38-1ef3-4d22-82fa-8bf60faf7b5d +0 -0
  157. package/avatars/b24bcbc1-11f0-473d-a8b9-ba8ce4ca127d +0 -0
  158. package/avatars/b2607346-af1c-4e98-b725-7650a766db2a +0 -0
  159. package/avatars/b6300f7c-cb37-459b-b1bf-8a0a0e797a52 +0 -0
  160. package/avatars/b7d3cff3-84dd-4547-93a1-de1aaa8aa34c +0 -0
  161. package/avatars/baa4b51c-e97f-4f51-bcb3-f27bc506cfaf +0 -0
  162. package/avatars/be7022e4-e292-4515-80d5-f9b61ebeb4ce +0 -0
  163. package/avatars/bed596a3-7569-4854-9e76-f52d33c0a541 +0 -0
  164. package/avatars/bf69992f-3f72-4930-99bb-0ffe17f3aebf +0 -0
  165. package/avatars/ca00c250-c6d4-464d-a6de-1c8467a18fe8 +0 -0
  166. package/avatars/ca19d78f-c0bc-4bd5-b26f-6923cb19996d +0 -0
  167. package/avatars/cda4d6e1-e0a4-4024-ac95-6de98e713b98 +0 -0
  168. package/avatars/cf72c30d-2da8-4e81-aa71-735b9e714274 +0 -0
  169. package/avatars/d5a35b78-99b3-4564-b6b9-b2ccab28c470 +0 -0
  170. package/avatars/dadb38c1-2a9d-47a3-8d92-b56b6166973c +0 -0
  171. package/avatars/e68705c7-375d-4423-9a86-29a16bd3ee0e +0 -0
  172. package/avatars/e9af3e4c-1f62-4302-8b99-b68ce93b7a86 +0 -0
  173. package/avatars/ea7e7331-e845-4189-8248-5f5b1d63f5e3 +0 -0
  174. package/avatars/ef4f8dcb-ef6c-4e7a-9be1-0476161bfce5 +0 -0
  175. package/avatars/ef7d0917-a206-4f88-8b60-93f8253774dc +0 -0
  176. package/avatars/f1a554d6-1db3-4ff5-b0dc-b607d6c3b4ff +0 -0
  177. package/avatars/f1fecc21-c81f-49a8-88f3-f942a0a679f6 +0 -0
  178. package/avatars/f30a2427-1755-4053-813e-129a179e1dd3 +0 -0
  179. package/avatars/f5370717-5109-46a5-a8d7-e1dd996d0615 +0 -0
  180. package/avatars/f6dd7126-1144-4998-bbd3-d4e0fbee2e95 +0 -0
  181. package/avatars/f83413d5-0003-4756-9ece-745fd61cc468 +0 -0
  182. package/avatars/f9b3149e-7ec8-4bb3-a9b9-dcbe66dac197 +0 -0
  183. package/avatars/fa41d70b-857e-4423-bd7d-26ddcddc13b9 +0 -0
  184. package/avatars/fb551ee8-99c7-400b-8e1d-322ce4619998 +0 -0
  185. package/avatars/fe83a6d4-abb0-4ab0-b61d-76a7cc08be84 +0 -0
  186. package/dist/migrations/20210103192527_users.d.ts +0 -3
  187. package/dist/migrations/20210103192527_users.js +0 -30
  188. package/dist/migrations/20210103193502_mail.d.ts +0 -3
  189. package/dist/migrations/20210103193502_mail.js +0 -35
  190. package/dist/migrations/20210103193525_preKeys.d.ts +0 -3
  191. package/dist/migrations/20210103193525_preKeys.js +0 -30
  192. package/dist/migrations/20210103193553_oneTimeKeys.d.ts +0 -3
  193. package/dist/migrations/20210103193553_oneTimeKeys.js +0 -30
  194. package/dist/migrations/20210103193615_servers.d.ts +0 -3
  195. package/dist/migrations/20210103193615_servers.js +0 -28
  196. package/dist/migrations/20210103193729_channels.d.ts +0 -3
  197. package/dist/migrations/20210103193729_channels.js +0 -28
  198. package/dist/migrations/20210103193749_permissions.d.ts +0 -3
  199. package/dist/migrations/20210103193749_permissions.js +0 -30
  200. package/dist/migrations/20210103193801_files.d.ts +0 -3
  201. package/dist/migrations/20210103193801_files.js +0 -28
  202. package/dist/utils/createLogger.d.ts +0 -5
  203. package/dist/utils/createLogger.js +0 -41
  204. package/emoji/04d98632-2c86-421b-a407-17f14fe86f8f +0 -0
  205. package/emoji/1160ed6e-1163-4043-9808-4029e863ed30 +0 -0
  206. package/emoji/1547ab18-1635-4a80-a82d-ebbb767b9932 +0 -0
  207. package/emoji/16922521-f6cb-4de4-860c-27916b22c6ba +0 -0
  208. package/emoji/198a9432-0e41-4866-994a-448d4775afcb +0 -0
  209. package/emoji/1be886b3-c9c5-4593-b516-f357ed931f96 +0 -0
  210. package/emoji/1c2b3d1d-637f-4103-b066-4bc4511a3ad7 +0 -0
  211. package/emoji/1efd27e7-b15f-475c-8b32-9159d26b169d +0 -0
  212. package/emoji/270b9409-0ea5-4be2-a239-a8dce13f9c31 +0 -0
  213. package/emoji/27812f76-fee2-49dd-a217-363de6d159dc +0 -0
  214. package/emoji/297ec202-8c24-44c6-aead-689d6d461883 +0 -0
  215. package/emoji/2bf06d86-17cb-4f40-a5ef-bd75d239a1a3 +0 -0
  216. package/emoji/31a75163-1cce-4dc1-b0a2-ecad6a4c500b +0 -0
  217. package/emoji/35235635-fdbd-4273-8428-f3cb3e1e8fd3 +0 -0
  218. package/emoji/3690fff2-6824-4403-a6e3-16a6a54979a9 +0 -0
  219. package/emoji/391014c2-59e0-46a8-85ec-7a7fdaca1d2d +0 -0
  220. package/emoji/3b383dcb-6e76-4e85-8e16-7c68040c06c2 +0 -0
  221. package/emoji/42d617a7-b104-42f5-9618-473181f752cf +0 -0
  222. package/emoji/482495d3-cce9-4f88-bf2a-f6003f03a9b5 +0 -0
  223. package/emoji/48390e06-0efb-404c-89bd-5f2be241bd50 +0 -0
  224. package/emoji/4b808d8d-3248-4149-b919-71b108391bcf +0 -0
  225. package/emoji/4bc13544-d82a-4e32-bd17-a70592274314 +0 -0
  226. package/emoji/4fcebf70-8623-4343-8243-67c8547b2edd +0 -0
  227. package/emoji/509d09aa-1214-459c-8081-50918a17b9af +0 -0
  228. package/emoji/5272abd8-d4d7-4b90-acd2-bf30e6c27243 +0 -0
  229. package/emoji/53c272ce-48bd-4d7e-bfb8-a6482b88be54 +0 -0
  230. package/emoji/5b279e65-06f7-4b26-8b4c-d1b48fba728d +0 -0
  231. package/emoji/5bd141f9-4394-4108-9376-66ebbc2c2bc1 +0 -0
  232. package/emoji/5c769156-f9cb-40bd-ab89-4edeece613fd +0 -0
  233. package/emoji/5c85fba9-8ba7-4fc9-b1b2-48dc30d24a1b +0 -0
  234. package/emoji/61a5e565-d20b-40ba-a139-b0c73a6027f3 +0 -0
  235. package/emoji/6913f43d-dd45-456c-9641-a126104d9ae5 +0 -0
  236. package/emoji/6957e74e-9622-492d-a950-242db3752260 +0 -0
  237. package/emoji/6a14bab5-26af-4bfa-9c17-be7c2511976d +0 -0
  238. package/emoji/6be09439-509e-4095-a30a-b1c7c573895d +0 -0
  239. package/emoji/6cc435b1-fe53-433c-b5a9-2b2019053997 +0 -0
  240. package/emoji/74f1b2af-bc7e-4a0f-802a-64ded185d5e2 +0 -0
  241. package/emoji/7890ba09-f02f-428e-807d-006d03d51d4a +0 -0
  242. package/emoji/7dc69179-6b3c-4f40-b20b-0ff573deea2d +0 -0
  243. package/emoji/7dd1b6b1-439d-4279-916a-995408863172 +0 -0
  244. package/emoji/820498ad-a2c8-43a2-ab83-d26f9c2246d4 +0 -0
  245. package/emoji/8319469c-2787-44e5-91a6-c8c39810dd7c +0 -0
  246. package/emoji/86745d1d-9e59-4607-b2b0-46c741079be1 +0 -0
  247. package/emoji/887b3cff-ae9e-4b5f-ad00-3ca9fc72f689 +0 -0
  248. package/emoji/8c6cf621-71d6-4fca-abe6-e19f4dd7f883 +0 -0
  249. package/emoji/8ca6d32e-a1ef-4956-a416-d8d0d680f085 +0 -0
  250. package/emoji/8d979f5e-38a2-4dd1-b3e4-80938bbe499b +0 -0
  251. package/emoji/99f68ea0-e3fe-4f03-9cc3-5f7f5315404d +0 -0
  252. package/emoji/9ad3aedc-7f79-4d68-a144-82e5b5dc3033 +0 -0
  253. package/emoji/9e418c2f-1f0f-46c4-be39-3bda38a28545 +0 -0
  254. package/emoji/a1f616bf-7402-4e24-9111-18acaebabb48 +0 -0
  255. package/emoji/a25ed9c1-3f9c-4e5f-ade2-7b159fb9fbf4 +0 -0
  256. package/emoji/a5176bc2-39a8-467b-8c75-6fbbc81b59c7 +0 -0
  257. package/emoji/a584215c-6547-438b-8ae8-dd490b51890e +0 -0
  258. package/emoji/a739895f-cf61-4b7c-b350-8e8283aaf751 +0 -0
  259. package/emoji/aaa10dd2-02a2-499e-9e17-c83787436508 +0 -0
  260. package/emoji/ae90baf2-a0ef-4d4d-9cc4-94f8ddd60f45 +0 -0
  261. package/emoji/b0564c48-feae-431a-95f9-df597c6c124c +0 -0
  262. package/emoji/b218bb93-e69c-4793-a669-83316650c4e7 +0 -0
  263. package/emoji/b2998c27-85d5-4598-ab41-469aa8e0fcad +0 -0
  264. package/emoji/b3da08ba-4179-4b5d-826f-5fc15e1a3ad2 +0 -0
  265. package/emoji/b840eb6a-a917-4bb2-854b-8f1022e7904b +0 -0
  266. package/emoji/b84baa76-b4b0-4b83-bc83-78661cb4f1d4 +0 -0
  267. package/emoji/baf69d80-8b1d-4032-855f-605cf0d489c3 +0 -0
  268. package/emoji/bb4d372c-ccd0-4a47-b157-b6a3b9f763e2 +0 -0
  269. package/emoji/bdbd1627-c81d-42d9-b3f5-8979e2ab74dc +0 -0
  270. package/emoji/c257388f-8b85-450b-b168-ebdf8d8c3026 +0 -0
  271. package/emoji/c573fde1-faa9-4c1d-a172-e283645afcfd +0 -0
  272. package/emoji/c8e27810-e8ea-47ce-b7ec-cef0a6becb28 +0 -0
  273. package/emoji/cdeef182-b220-4850-9ecf-5d7c472fd754 +0 -0
  274. package/emoji/d59c1aa9-6f81-4c07-96dd-9953401ff211 +0 -0
  275. package/emoji/dd407dbf-a077-40ba-957f-337b3c5efdc7 +0 -0
  276. package/emoji/e01f6e06-5728-4e2c-90fb-314a5827b766 +0 -0
  277. package/emoji/e1f9ed12-a2ce-433d-b454-b833438a1f9c +0 -0
  278. package/emoji/e697e5c4-acd6-41cd-a43e-edee8da3ab7b +0 -0
  279. package/emoji/e8182220-3464-4e31-8c08-466baead7bfc +0 -0
  280. package/emoji/eb0f3fd5-abc9-4abf-b816-d8458aeb7ec8 +0 -0
  281. package/emoji/eb38ecf7-0d13-4c51-b96a-3777f79321c4 +0 -0
  282. package/emoji/ee515c85-b7ce-4493-a427-994cc0af0d59 +0 -0
  283. package/emoji/f485cef2-d3fa-4d59-88af-b79a3105cacf +0 -0
  284. package/emoji/ff0dab2a-7015-4e8c-b0d0-3569058359dc +0 -0
  285. package/files/01087968-07b6-4fdb-9aeb-fa9dc061be94 +0 -0
  286. package/files/030455b3-17cc-415f-b3b3-2bb56c92ee8b +0 -0
  287. package/files/06129f52-a858-4031-ad85-4d7f2bb793af +0 -0
  288. package/files/0a6155a9-069f-45e9-8a06-56992fe55187 +0 -0
  289. package/files/12b9dda5-feb1-4f20-a987-ad422db5ba73 +0 -0
  290. package/files/13c8fa1e-8821-4628-b607-9e4fa4510df7 +0 -0
  291. package/files/159b0ad3-1a30-419d-af22-28a1096ce825 +0 -0
  292. package/files/1699c563-6769-431b-8041-99a6a4386f25 +0 -0
  293. package/files/176b916c-0dd9-4b93-bfdc-b8ccabf15a96 +0 -0
  294. package/files/1a27dad9-8cc9-4a0c-9d4e-7bde63adda60 +0 -0
  295. package/files/1d29832c-059a-4190-bca6-b83ac77540d9 +0 -0
  296. package/files/1dcde013-8833-4369-8726-81236e4eb30e +0 -0
  297. package/files/2080fc9f-d2c4-4fbc-af84-232fe4900a4f +0 -0
  298. package/files/20889623-6869-46a2-999b-c07708c12521 +0 -0
  299. package/files/2107e243-c378-418d-a183-7df13873c65b +0 -0
  300. package/files/225ed61e-8f8c-4d17-b675-9a9f9918d5b4 +0 -0
  301. package/files/29ffba15-5acc-4ef8-b5a4-5ce61d3f6e85 +0 -0
  302. package/files/2a69434c-1d8a-4e9d-90d4-569aaeaae7e3 +0 -0
  303. package/files/2aec8fcf-25bf-478e-b2f6-fe67ad753071 +0 -0
  304. package/files/2c64490e-c7de-4cb2-b22e-70e2ac69d88f +0 -0
  305. package/files/2d80b4f4-6389-4f5d-8d32-f0ed93820907 +0 -0
  306. package/files/2f9dd26e-363f-4445-8ee5-28548007a33a +0 -0
  307. package/files/360dd8f5-76c4-4e86-ba22-9025dc7ca2a4 +0 -0
  308. package/files/3a0a7f5b-45a5-4340-ba7c-6a0fa2c63871 +0 -0
  309. package/files/3a8fa6be-8acf-4b7b-b653-25edc6b28cdc +0 -5
  310. package/files/3d9e191b-2c15-42aa-9928-c2cdbb5e14ca +0 -0
  311. package/files/3dd0f2ef-0d4e-4837-bffc-22aa645cbe85 +0 -0
  312. package/files/402c4b9b-adbf-4ab6-a21d-c17369b48abf +0 -0
  313. package/files/4685d988-33eb-4902-872f-3b824f497c8b +0 -0
  314. package/files/495f6c55-07f8-4713-a444-a2261a789b94 +0 -0
  315. package/files/4acba8e3-567b-4062-b81b-340e205a01de +0 -0
  316. package/files/4c0a10cc-395b-474a-8140-677ed607da89 +0 -0
  317. package/files/537584d9-25ad-4830-808a-a1e3d63e2a52 +0 -0
  318. package/files/5519f10d-745e-4474-8ca8-6c111693704c +0 -0
  319. package/files/5563cf92-a5e3-4be3-867d-9647a02298d4 +0 -0
  320. package/files/55b70d9b-fd58-4800-832c-d6b4521ba6d0 +0 -0
  321. package/files/5623cff3-ce9b-403b-9915-50d2bcbc981c +0 -0
  322. package/files/576314ba-2a1a-4753-8d77-2a9e04f509d1 +0 -0
  323. package/files/58ed97ae-0eac-4e04-add1-76646effa2d5 +0 -0
  324. package/files/68638efd-5389-481d-a841-36164c62c078 +0 -0
  325. package/files/6936d34b-a1f8-4a9d-b005-5544dbdcf5e5 +0 -0
  326. package/files/6bda9e88-3a28-47f7-994e-900ede6bf984 +0 -0
  327. package/files/7024a3b9-a863-4618-9dbd-fa6502017ae0 +0 -0
  328. package/files/707caccb-3780-456d-9056-c20bfdfc0e5b +0 -0
  329. package/files/74b8c73c-032b-4640-a5b3-d30dd270cbcb +0 -0
  330. package/files/767cb262-2974-40f8-8182-f7770b431923 +0 -0
  331. package/files/7a669935-cb48-4349-b26b-7705f8a04fbc +0 -0
  332. package/files/7bc8f678-6ee7-454c-a1f8-bb9358b89c95 +0 -0
  333. package/files/7ccc2699-07ec-4177-a9ce-ee7dc952fda1 +0 -0
  334. package/files/7e0eabf4-b334-4683-8156-ab8d949a0532 +0 -0
  335. package/files/7f0644ab-02a3-4121-adfc-29a7c55cf804 +0 -0
  336. package/files/7ff3266d-f103-48ce-90dd-95b9dfe5fcc9 +0 -0
  337. package/files/836e2e8e-aefd-4b4a-a9b9-bf7436158a8c +0 -0
  338. package/files/875f7ae5-fa23-4fc5-b04a-8433a7f7089c +0 -0
  339. package/files/8ca62da9-f204-49e3-b418-9451661b2904 +0 -0
  340. package/files/9283054c-107f-474d-b61e-f1d0061bcb86 +0 -0
  341. package/files/93b1ce7f-9566-452b-b4be-30f87d3de150 +0 -0
  342. package/files/93fb51c7-e19b-4ac1-9dc3-aeb8da0672ed +0 -0
  343. package/files/9b54a3a1-534a-4ed5-b016-3c74ed4c9edd +0 -0
  344. package/files/9b5beb6f-712d-4969-b127-fd66c9b2a9c6 +0 -0
  345. package/files/9e964fbf-d063-498e-b2e3-79f8d6afcf5f +0 -0
  346. package/files/a66b7a9f-58c2-47a8-a429-a6f0647c6fe9 +0 -0
  347. package/files/a7cbda7d-81ba-40f7-a997-51146af63e5f +0 -0
  348. package/files/ac01e83a-e572-41b6-81ab-c992cff7c170 +0 -0
  349. package/files/ad11a58f-f963-4233-bd29-1658b6b7e600 +0 -0
  350. package/files/ae60a4a8-08b9-4521-a0a3-d015a8b3ed08 +0 -0
  351. package/files/b1d3cf27-8d76-4cf9-aa51-d7c6bfd1b3bf +0 -0
  352. package/files/b2c68863-8554-4ac6-8e99-821f0267cf91 +0 -0
  353. package/files/b3043e01-a771-44af-bf19-5327646ff929 +0 -0
  354. package/files/b6d01b89-def5-4c7c-8e97-a3d88617f8f4 +0 -0
  355. package/files/b8760b32-bb1e-4cd7-a9d6-29c6e0b071bc +0 -0
  356. package/files/ba5e0470-44f7-47b3-bcb5-eeab3ca8c292 +0 -0
  357. package/files/c3969b5f-43ae-43f3-9bdd-d3959c79ca01 +0 -0
  358. package/files/caecd488-dbe6-4a30-a400-bced2ba8dae6 +0 -0
  359. package/files/d7d865b8-3a05-4ed1-b95d-b93dc1ebb9a9 +0 -0
  360. package/files/dbca5f31-cf38-4c1e-83b0-5ec8473196fc +0 -0
  361. package/files/dbf07e82-fff6-4985-bebe-d62c0458bfd0 +0 -0
  362. package/files/dd759c20-eead-4e57-9e74-4d3a2b978e91 +0 -5
  363. package/files/de0b2cf1-981b-4e4a-a04e-ac185d1620cd +0 -0
  364. package/files/de6837fe-5aa2-4ff9-a067-2646a008c780 +0 -0
  365. package/files/e2fde852-91cb-4e01-88a2-ee086b5f227c +0 -0
  366. package/files/e391d1ce-8d39-460c-a462-791730131f7f +0 -0
  367. package/files/e6d9b60f-2b1b-4c6f-ba6d-02f6de90d40f +0 -0
  368. package/files/f693c62d-2ac8-49fd-aa38-30904e013e3c +0 -0
  369. package/files/f749fdf5-193e-41f7-b643-5696f67c6402 +0 -0
  370. package/files/f8910384-e75c-4d65-825f-52a6748f6475 +0 -0
  371. package/files/fad8826b-e952-4acb-a509-3e6543b94d61 +0 -0
  372. package/jest.config.js +0 -13
  373. package/spire.sqlite +0 -0
@@ -0,0 +1,841 @@
1
+ import type { Database } from "../Database.ts";
2
+ import type { Emoji } from "@vex-chat/types";
3
+
4
+ import * as fs from "node:fs";
5
+ import * as fsp from "node:fs/promises";
6
+
7
+ import express from "express";
8
+
9
+ import { XUtils } from "@vex-chat/crypto";
10
+ import { type KeyPair, xSignOpen } from "@vex-chat/crypto";
11
+ import { PreKeysWSSchema, TokenScopes, UserSchema } from "@vex-chat/types";
12
+
13
+ import cors from "cors";
14
+ import { fileTypeFromBuffer, fileTypeFromFile } from "file-type";
15
+ import helmet from "helmet";
16
+ import jwt from "jsonwebtoken";
17
+ import multer from "multer";
18
+ import parseDuration from "parse-duration";
19
+ import { stringify as uuidStringify } from "uuid";
20
+ import { z } from "zod/v4";
21
+
22
+ import { POWER_LEVELS } from "../ClientManager.ts";
23
+ import { JWT_EXPIRY } from "../Spire.ts";
24
+ import { getJwtSecret } from "../utils/jwtSecret.ts";
25
+ import { msgpack } from "../utils/msgpack.ts";
26
+
27
+ import { getAvatarRouter } from "./avatar.ts";
28
+ import { errorHandler } from "./errors.ts";
29
+ import { getFileRouter } from "./file.ts";
30
+ import { getInviteRouter } from "./invite.ts";
31
+ import { setupDocs } from "./openapi.ts";
32
+ import {
33
+ hasAnyPermission,
34
+ hasPermission,
35
+ userHasPermission,
36
+ } from "./permissions.ts";
37
+ import { globalLimiter } from "./rateLimit.ts";
38
+ import { getUserRouter } from "./user.ts";
39
+ import { censorUser, getParam, getUser } from "./utils.ts";
40
+
41
+ // expiry of regkeys
42
+ export const EXPIRY_TIME = 1000 * 60 * 5;
43
+
44
+ export const ALLOWED_IMAGE_TYPES = [
45
+ "image/jpeg",
46
+ "image/png",
47
+ "image/gif",
48
+ "image/apng",
49
+ "image/avif",
50
+ ];
51
+
52
+ // ── Zod schemas for trust-boundary validation ──────────────────────────
53
+ const invitePayload = z.object({
54
+ duration: z.string().min(1),
55
+ serverID: z.string().min(1),
56
+ });
57
+
58
+ const channelPayload = z.object({
59
+ name: z.string().min(1).max(255),
60
+ });
61
+
62
+ const deviceListPayload = z.array(z.string());
63
+
64
+ const connectPayload = z.object({
65
+ signed: z.custom<Uint8Array>((val) => val instanceof Uint8Array),
66
+ });
67
+
68
+ const safePathParam = z.string().regex(/^[a-zA-Z0-9._-]+$/);
69
+
70
+ const emojiPayload = z.object({
71
+ file: z.string().optional(),
72
+ name: z.string().min(1),
73
+ signed: z.string().optional(),
74
+ });
75
+
76
+ const jwtUserPayload = z.object({
77
+ exp: z.number().optional(),
78
+ user: UserSchema,
79
+ });
80
+
81
+ const jwtDevicePayload = z.object({
82
+ device: z.object({
83
+ deleted: z.boolean(),
84
+ deviceID: z.string(),
85
+ lastLogin: z.string(),
86
+ name: z.string(),
87
+ owner: z.string(),
88
+ signKey: z.string(),
89
+ }),
90
+ });
91
+
92
+ /** Extract Bearer token from Authorization header. */
93
+ function extractBearer(req: express.Request): null | string {
94
+ const header = req.headers.authorization;
95
+ if (!header || !header.startsWith("Bearer ")) return null;
96
+ return header.slice(7);
97
+ }
98
+
99
+ const checkAuth: express.RequestHandler = (req, _res, next) => {
100
+ const token = extractBearer(req);
101
+ if (token) {
102
+ try {
103
+ const result = jwt.verify(token, getJwtSecret());
104
+ const parsed = jwtUserPayload.safeParse(result);
105
+ if (parsed.success) {
106
+ req.user = parsed.data.user;
107
+ if (parsed.data.exp !== undefined) {
108
+ req.exp = parsed.data.exp;
109
+ }
110
+ req.bearerToken = token;
111
+ }
112
+ } catch {
113
+ // Token verification failed — continue without auth
114
+ }
115
+ }
116
+ next();
117
+ };
118
+
119
+ const checkDevice: express.RequestHandler = (req, _res, next) => {
120
+ const token = req.headers["x-device-token"];
121
+ if (typeof token === "string" && token) {
122
+ try {
123
+ const result = jwt.verify(token, getJwtSecret());
124
+ const parsed = jwtDevicePayload.safeParse(result);
125
+ if (parsed.success) {
126
+ req.device = parsed.data.device;
127
+ }
128
+ } catch {
129
+ // Device token verification failed — continue without device
130
+ }
131
+ }
132
+ next();
133
+ };
134
+
135
+ export const protect: express.RequestHandler = (req, res, next) => {
136
+ if (!req.user) {
137
+ res.sendStatus(401);
138
+ return;
139
+ }
140
+
141
+ next();
142
+ };
143
+
144
+ export const msgpackParser: express.RequestHandler = (req, res, next) => {
145
+ if (req.is("application/msgpack")) {
146
+ try {
147
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-argument -- Express req.body is any; decoded body is validated by route-level Zod schemas
148
+ req.body = msgpack.decode(req.body);
149
+ } catch {
150
+ res.sendStatus(400);
151
+ return;
152
+ }
153
+ }
154
+ next();
155
+ };
156
+
157
+ const directories = ["files", "avatars"];
158
+ for (const dir of directories) {
159
+ if (!fs.existsSync(dir)) {
160
+ fs.mkdirSync(dir);
161
+ }
162
+ }
163
+
164
+ export const initApp = (
165
+ api: express.Application,
166
+ db: Database,
167
+ tokenValidator: (key: string, scope: TokenScopes) => boolean,
168
+ signKeys: KeyPair,
169
+ notify: (
170
+ userID: string,
171
+ event: string,
172
+ transmissionID: string,
173
+ data?: unknown,
174
+ deviceID?: string,
175
+ ) => void,
176
+ ) => {
177
+ // INIT ROUTERS
178
+ const userRouter = getUserRouter(db, tokenValidator);
179
+ const fileRouter = getFileRouter(db);
180
+ const avatarRouter = getAvatarRouter();
181
+ const inviteRouter = getInviteRouter(db, tokenValidator, notify);
182
+
183
+ // MIDDLEWARE
184
+ // Global per-IP rate limit is the FIRST middleware so a flooded
185
+ // source hits the limiter before Express spends any cycles on
186
+ // body parsing, helmet, or auth. See src/server/rateLimit.ts.
187
+ api.use(globalLimiter);
188
+ api.use(express.json({ limit: "20mb" }));
189
+ api.use(
190
+ express.raw({
191
+ limit: "20mb",
192
+ type: "application/msgpack",
193
+ }),
194
+ );
195
+ api.use(helmet());
196
+ api.use(msgpackParser);
197
+ api.use(checkAuth);
198
+ api.use(checkDevice);
199
+
200
+ const allowedOrigins = process.env["CORS_ORIGINS"]?.split(",") ?? [];
201
+ api.use(
202
+ cors({
203
+ credentials: true,
204
+ origin: allowedOrigins.length > 0 ? allowedOrigins : false,
205
+ }),
206
+ );
207
+
208
+ api.get("/server/:id", protect, async (req, res) => {
209
+ const server = await db.retrieveServer(getParam(req, "id"));
210
+
211
+ if (server) {
212
+ return res.send(msgpack.encode(server));
213
+ } else {
214
+ return res.sendStatus(404);
215
+ }
216
+ });
217
+
218
+ api.post("/server/:name", protect, async (req, res) => {
219
+ const userDetails = getUser(req);
220
+ const serverName = atob(getParam(req, "name"));
221
+
222
+ const server = await db.createServer(serverName, userDetails.userID);
223
+ res.send(msgpack.encode(server));
224
+ });
225
+
226
+ api.post("/server/:serverID/invites", protect, async (req, res) => {
227
+ const userDetails = getUser(req);
228
+
229
+ const parsedPayload = invitePayload.safeParse(req.body);
230
+ if (!parsedPayload.success) {
231
+ res.status(400).json({
232
+ error: "Invalid invite payload",
233
+ issues: parsedPayload.error.issues,
234
+ });
235
+ return;
236
+ }
237
+ const payload = parsedPayload.data;
238
+ const serverEntry = await db.retrieveServer(getParam(req, "serverID"));
239
+
240
+ if (!serverEntry) {
241
+ res.sendStatus(404);
242
+ return;
243
+ }
244
+
245
+ const permissions = await db.retrievePermissions(
246
+ userDetails.userID,
247
+ "server",
248
+ );
249
+
250
+ if (
251
+ !hasPermission(
252
+ permissions,
253
+ getParam(req, "serverID"),
254
+ POWER_LEVELS.INVITE,
255
+ )
256
+ ) {
257
+ res.sendStatus(401);
258
+ return;
259
+ }
260
+
261
+ const duration = parseDuration(payload.duration, "ms");
262
+
263
+ if (!duration) {
264
+ res.sendStatus(400);
265
+ return;
266
+ }
267
+
268
+ const expires = new Date(Date.now() + duration);
269
+
270
+ const invite = await db.createInvite(
271
+ crypto.randomUUID(),
272
+ serverEntry.serverID,
273
+ userDetails.userID,
274
+ expires.toString(),
275
+ );
276
+ res.send(msgpack.encode(invite));
277
+ });
278
+
279
+ api.get("/server/:serverID/invites", protect, async (req, res) => {
280
+ const userDetails = getUser(req);
281
+
282
+ const permissions = await db.retrievePermissions(
283
+ userDetails.userID,
284
+ "server",
285
+ );
286
+
287
+ if (
288
+ !hasPermission(
289
+ permissions,
290
+ getParam(req, "serverID"),
291
+ POWER_LEVELS.INVITE,
292
+ )
293
+ ) {
294
+ res.sendStatus(401);
295
+ return;
296
+ }
297
+
298
+ const inviteList = await db.retrieveServerInvites(
299
+ getParam(req, "serverID"),
300
+ );
301
+ res.send(msgpack.encode(inviteList));
302
+ });
303
+
304
+ api.delete("/server/:id", protect, async (req, res) => {
305
+ const userDetails = getUser(req);
306
+ const serverID = getParam(req, "id");
307
+ const permissions = await db.retrievePermissions(
308
+ userDetails.userID,
309
+ "server",
310
+ );
311
+ if (hasPermission(permissions, serverID, POWER_LEVELS.DELETE)) {
312
+ await db.deleteServer(serverID);
313
+ res.sendStatus(200);
314
+ return;
315
+ }
316
+ res.sendStatus(401);
317
+ });
318
+
319
+ api.post("/server/:id/channels", protect, async (req, res) => {
320
+ const userDetails = getUser(req);
321
+ const serverID = getParam(req, "id");
322
+ // resourceID is serverID
323
+ const parsedBody = channelPayload.safeParse(req.body);
324
+ if (!parsedBody.success) {
325
+ res.status(400).json({
326
+ error: "Invalid channel payload",
327
+ issues: parsedBody.error.issues,
328
+ });
329
+ return;
330
+ }
331
+ const { name } = parsedBody.data;
332
+ const permissions = await db.retrievePermissions(
333
+ userDetails.userID,
334
+ "server",
335
+ );
336
+ if (hasPermission(permissions, serverID, POWER_LEVELS.CREATE)) {
337
+ const channel = await db.createChannel(name, serverID);
338
+ res.send(msgpack.encode(channel));
339
+
340
+ const affectedUsers = await db.retrieveAffectedUsers(serverID);
341
+ // tell everyone about server change
342
+ for (const user of affectedUsers) {
343
+ notify(
344
+ user.userID,
345
+ "serverChange",
346
+ crypto.randomUUID(),
347
+ serverID,
348
+ );
349
+ }
350
+ return;
351
+ }
352
+ res.sendStatus(401);
353
+ });
354
+
355
+ api.get("/server/:id/channels", protect, async (req, res) => {
356
+ const serverID = getParam(req, "id");
357
+ const userDetails = getUser(req);
358
+ const permissions = await db.retrievePermissions(
359
+ userDetails.userID,
360
+ "server",
361
+ );
362
+ if (hasAnyPermission(permissions, serverID)) {
363
+ const channels = await db.retrieveChannels(serverID);
364
+ res.send(msgpack.encode(channels));
365
+ return;
366
+ }
367
+ res.sendStatus(401);
368
+ });
369
+
370
+ api.get("/server/:serverID/emoji", protect, async (req, res) => {
371
+ const rows = await db.retrieveEmojiList(getParam(req, "serverID"));
372
+ res.send(msgpack.encode(rows));
373
+ });
374
+
375
+ api.get("/server/:serverID/permissions", protect, async (req, res) => {
376
+ const userDetails = getUser(req);
377
+ const serverID = getParam(req, "serverID");
378
+ const permissions = await db.retrievePermissionsByResourceID(serverID);
379
+ const canSee = permissions.some(
380
+ (perm) => perm.userID === userDetails.userID,
381
+ );
382
+ if (!canSee) {
383
+ res.sendStatus(401);
384
+ return;
385
+ }
386
+ res.send(msgpack.encode(permissions));
387
+ });
388
+
389
+ api.delete("/channel/:id", protect, async (req, res) => {
390
+ const channelID = getParam(req, "id");
391
+ const userDetails = getUser(req);
392
+
393
+ const channel = await db.retrieveChannel(channelID);
394
+
395
+ if (!channel) {
396
+ res.sendStatus(401);
397
+ return;
398
+ }
399
+
400
+ const permissions = await db.retrievePermissions(
401
+ userDetails.userID,
402
+ "server",
403
+ );
404
+ for (const permission of permissions) {
405
+ if (
406
+ permission.resourceID === channel.serverID &&
407
+ permission.powerLevel > 50
408
+ ) {
409
+ await db.deleteChannel(channelID);
410
+
411
+ res.sendStatus(200);
412
+
413
+ const affectedUsers = await db.retrieveAffectedUsers(
414
+ channel.serverID,
415
+ );
416
+ // tell everyone about server change
417
+ for (const user of affectedUsers) {
418
+ notify(
419
+ user.userID,
420
+ "serverChange",
421
+ crypto.randomUUID(),
422
+ channel.serverID,
423
+ );
424
+ }
425
+ return;
426
+ }
427
+ }
428
+ res.sendStatus(401);
429
+ });
430
+
431
+ api.get("/channel/:id", protect, async (req, res) => {
432
+ const channel = await db.retrieveChannel(getParam(req, "id"));
433
+
434
+ if (channel) {
435
+ return res.send(msgpack.encode(channel));
436
+ } else {
437
+ return res.sendStatus(404);
438
+ }
439
+ });
440
+
441
+ api.delete("/permission/:permissionID", protect, async (req, res) => {
442
+ const permissionID = getParam(req, "permissionID");
443
+ const userDetails = getUser(req);
444
+ const permToDelete = await db.retrievePermission(permissionID);
445
+ if (!permToDelete) {
446
+ res.sendStatus(404);
447
+ return;
448
+ }
449
+
450
+ const permissions = await db.retrievePermissions(
451
+ userDetails.userID,
452
+ permToDelete.resourceType,
453
+ );
454
+
455
+ for (const perm of permissions) {
456
+ if (
457
+ perm.resourceID === permToDelete.resourceID &&
458
+ (perm.userID === userDetails.userID ||
459
+ (perm.powerLevel > POWER_LEVELS.DELETE &&
460
+ perm.powerLevel > permToDelete.powerLevel))
461
+ ) {
462
+ await db.deletePermission(permToDelete.permissionID);
463
+ res.sendStatus(200);
464
+ return;
465
+ }
466
+ }
467
+ res.sendStatus(401);
468
+ });
469
+
470
+ api.post("/userList/:channelID", protect, async (req, res) => {
471
+ const userDetails = getUser(req);
472
+ const channelID = getParam(req, "channelID");
473
+
474
+ const channel = await db.retrieveChannel(channelID);
475
+ if (!channel) {
476
+ res.sendStatus(404);
477
+ return;
478
+ }
479
+ const permissions = await db.retrievePermissions(
480
+ userDetails.userID,
481
+ "server",
482
+ );
483
+ for (const permission of permissions) {
484
+ if (permission.resourceID === channel.serverID) {
485
+ const groupMembers = await db.retrieveGroupMembers(channelID);
486
+ res.send(
487
+ msgpack.encode(
488
+ groupMembers.map((user) => censorUser(user)),
489
+ ),
490
+ );
491
+ return;
492
+ }
493
+ }
494
+ res.sendStatus(401);
495
+ });
496
+
497
+ api.post("/deviceList", protect, async (req, res) => {
498
+ const parsed = deviceListPayload.safeParse(req.body);
499
+ if (!parsed.success) {
500
+ res.status(400).json({
501
+ error: "Expected array of user ID strings",
502
+ issues: parsed.error.issues,
503
+ });
504
+ return;
505
+ }
506
+ const devices = await db.retrieveUserDeviceList(parsed.data);
507
+ res.send(msgpack.encode(devices));
508
+ });
509
+
510
+ api.get("/device/:id", protect, async (req, res) => {
511
+ const device = await db.retrieveDevice(getParam(req, "id"));
512
+
513
+ if (device) {
514
+ return res.send(msgpack.encode(device));
515
+ } else {
516
+ return res.sendStatus(404);
517
+ }
518
+ });
519
+
520
+ api.post("/device/:id/keyBundle", protect, async (req, res) => {
521
+ try {
522
+ const keyBundle = await db.getKeyBundle(getParam(req, "id"));
523
+ if (keyBundle) {
524
+ res.send(msgpack.encode(keyBundle));
525
+ } else {
526
+ res.sendStatus(404);
527
+ }
528
+ } catch {
529
+ res.sendStatus(500);
530
+ }
531
+ });
532
+
533
+ api.post("/device/:id/mail", protect, async (req, res) => {
534
+ const deviceDetails = req.device;
535
+ if (!deviceDetails) {
536
+ res.sendStatus(401);
537
+ return;
538
+ }
539
+ const inbox = await db.retrieveMail(deviceDetails.deviceID);
540
+ res.send(msgpack.encode(inbox));
541
+ });
542
+
543
+ api.post("/device/:id/connect", protect, async (req, res) => {
544
+ const parsedBody = connectPayload.safeParse(req.body);
545
+ if (!parsedBody.success) {
546
+ res.status(400).json({
547
+ error: "Invalid connect payload",
548
+ issues: parsedBody.error.issues,
549
+ });
550
+ return;
551
+ }
552
+ const { signed } = parsedBody.data;
553
+ const device = await db.retrieveDevice(getParam(req, "id"));
554
+ if (!device) {
555
+ res.sendStatus(404);
556
+ return;
557
+ }
558
+
559
+ const regKey = xSignOpen(signed, XUtils.decodeHex(device.signKey));
560
+ if (
561
+ regKey &&
562
+ tokenValidator(uuidStringify(regKey), TokenScopes.Connect)
563
+ ) {
564
+ const token = jwt.sign({ device }, getJwtSecret(), {
565
+ expiresIn: JWT_EXPIRY,
566
+ });
567
+ jwt.verify(token, getJwtSecret());
568
+
569
+ res.send(msgpack.encode({ deviceToken: token }));
570
+ } else {
571
+ res.sendStatus(401);
572
+ }
573
+ });
574
+
575
+ api.get("/device/:id/otk/count", protect, async (req, res) => {
576
+ const deviceDetails = req.device;
577
+ if (!deviceDetails) {
578
+ res.sendStatus(401);
579
+ return;
580
+ }
581
+ const count = await db.getOTKCount(deviceDetails.deviceID);
582
+ res.send(msgpack.encode({ count }));
583
+ });
584
+
585
+ api.post("/device/:id/otk", protect, async (req, res) => {
586
+ const parsedOTKs = z.array(PreKeysWSSchema).safeParse(req.body);
587
+ if (!parsedOTKs.success) {
588
+ res.status(400).json({
589
+ error: "Invalid OTK payload",
590
+ issues: parsedOTKs.error.issues,
591
+ });
592
+ return;
593
+ }
594
+ const submittedOTKs = parsedOTKs.data;
595
+ if (submittedOTKs.length === 0) {
596
+ res.sendStatus(200);
597
+ return;
598
+ }
599
+
600
+ const userDetails = getUser(req);
601
+
602
+ const deviceID = getParam(req, "id");
603
+ const otk = submittedOTKs[0];
604
+
605
+ const device = await db.retrieveDevice(deviceID);
606
+ if (!device || !otk) {
607
+ res.sendStatus(404);
608
+ return;
609
+ }
610
+
611
+ const message = xSignOpen(
612
+ otk.signature,
613
+ XUtils.decodeHex(device.signKey),
614
+ );
615
+
616
+ if (!message) {
617
+ res.sendStatus(401);
618
+ return;
619
+ }
620
+
621
+ await db.saveOTK(userDetails.userID, deviceID, submittedOTKs);
622
+ res.sendStatus(200);
623
+ });
624
+
625
+ api.get("/emoji/:emojiID/details", protect, async (req, res) => {
626
+ const emoji = await db.retrieveEmoji(getParam(req, "emojiID"));
627
+ res.send(msgpack.encode(emoji));
628
+ });
629
+
630
+ api.get("/emoji/:emojiID", protect, async (req, res) => {
631
+ const safeId = safePathParam.safeParse(getParam(req, "emojiID"));
632
+ if (!safeId.success) {
633
+ res.sendStatus(400);
634
+ return;
635
+ }
636
+ const filePath = "./emoji/" + safeId.data;
637
+ const typeDetails = await fileTypeFromFile(filePath).catch(() => null);
638
+ if (!typeDetails) {
639
+ res.sendStatus(404);
640
+ return;
641
+ }
642
+ res.set("Content-type", typeDetails.mime);
643
+ res.set("Cache-control", "public, max-age=31536000");
644
+
645
+ const stream = fs.createReadStream(filePath);
646
+ stream.on("error", (_err) => {
647
+ // debugger: emoji stream read error
648
+ res.sendStatus(500);
649
+ });
650
+ stream.pipe(res);
651
+ });
652
+
653
+ api.post("/emoji/:serverID/json", protect, async (req, res) => {
654
+ const parsedPayload = emojiPayload.safeParse(req.body);
655
+ if (!parsedPayload.success) {
656
+ res.status(400).json({
657
+ error: "Invalid emoji payload",
658
+ issues: parsedPayload.error.issues,
659
+ });
660
+ return;
661
+ }
662
+ const payload = parsedPayload.data;
663
+
664
+ const userDetails = getUser(req);
665
+ const device = req.device;
666
+
667
+ if (!device) {
668
+ res.sendStatus(401);
669
+ return;
670
+ }
671
+
672
+ if (!payload.file) {
673
+ res.sendStatus(400);
674
+ return;
675
+ }
676
+
677
+ const buf = Buffer.from(XUtils.decodeBase64(payload.file));
678
+ const serverEntry = await db.retrieveServer(getParam(req, "serverID"));
679
+
680
+ const permissionList = await db.retrievePermissionsByResourceID(
681
+ getParam(req, "serverID"),
682
+ );
683
+
684
+ if (
685
+ !userHasPermission(
686
+ permissionList,
687
+ userDetails.userID,
688
+ POWER_LEVELS.EMOJI,
689
+ )
690
+ ) {
691
+ res.sendStatus(401);
692
+ return;
693
+ }
694
+ if (!serverEntry) {
695
+ res.sendStatus(404);
696
+ return;
697
+ }
698
+ if (!payload.name) {
699
+ res.sendStatus(400);
700
+ return;
701
+ }
702
+ if (Buffer.byteLength(buf) > 256000) {
703
+ res.sendStatus(413);
704
+ return;
705
+ }
706
+
707
+ const mimeType = await fileTypeFromBuffer(buf);
708
+ if (!ALLOWED_IMAGE_TYPES.includes(mimeType?.mime || "no/type")) {
709
+ res.status(400).send({
710
+ error:
711
+ "Unsupported file type. Expected jpeg, png, gif, apng, or avif but received " +
712
+ String(mimeType?.ext),
713
+ });
714
+ return;
715
+ }
716
+
717
+ const emoji: Emoji = {
718
+ emojiID: crypto.randomUUID(),
719
+ name: payload.name,
720
+ owner: getParam(req, "serverID"),
721
+ };
722
+
723
+ await db.createEmoji(emoji);
724
+
725
+ try {
726
+ // write the file to disk
727
+ await fsp.writeFile("emoji/" + emoji.emojiID, buf);
728
+ res.send(msgpack.encode(emoji));
729
+ } catch (_err: unknown) {
730
+ // debugger: emoji write failed
731
+ res.sendStatus(500);
732
+ }
733
+ });
734
+
735
+ api.post(
736
+ "/emoji/:serverID",
737
+ protect,
738
+ multer().single("emoji"),
739
+ async (req, res) => {
740
+ const parsedPayload = emojiPayload.safeParse(req.body);
741
+ if (!parsedPayload.success) {
742
+ res.status(400).json({
743
+ error: "Invalid emoji payload",
744
+ issues: parsedPayload.error.issues,
745
+ });
746
+ return;
747
+ }
748
+ const payload = parsedPayload.data;
749
+ const serverID = getParam(req, "serverID");
750
+ if (typeof serverID !== "string") {
751
+ res.sendStatus(400);
752
+ return;
753
+ }
754
+ const serverEntry = await db.retrieveServer(serverID);
755
+ const userDetails = getUser(req);
756
+ const deviceDetails = req.device;
757
+ if (!deviceDetails) {
758
+ res.sendStatus(401);
759
+ return;
760
+ }
761
+
762
+ const permissionList =
763
+ await db.retrievePermissionsByResourceID(serverID);
764
+
765
+ if (
766
+ !userHasPermission(
767
+ permissionList,
768
+ userDetails.userID,
769
+ POWER_LEVELS.EMOJI,
770
+ )
771
+ ) {
772
+ res.sendStatus(401);
773
+ return;
774
+ }
775
+
776
+ if (!serverEntry) {
777
+ res.sendStatus(404);
778
+ return;
779
+ }
780
+
781
+ if (!payload.name) {
782
+ res.sendStatus(400);
783
+ return;
784
+ }
785
+
786
+ if (!req.file) {
787
+ res.sendStatus(400);
788
+ return;
789
+ }
790
+
791
+ if (Buffer.byteLength(req.file.buffer) > 256000) {
792
+ res.sendStatus(413);
793
+ return;
794
+ }
795
+
796
+ const mimeType = await fileTypeFromBuffer(req.file.buffer);
797
+ if (!ALLOWED_IMAGE_TYPES.includes(mimeType?.mime || "no/type")) {
798
+ res.status(400).send({
799
+ error:
800
+ "Unsupported file type. Expected jpeg, png, gif, apng, or avif but received " +
801
+ String(mimeType?.ext),
802
+ });
803
+ return;
804
+ }
805
+
806
+ const emoji: Emoji = {
807
+ emojiID: crypto.randomUUID(),
808
+ name: payload.name,
809
+ owner: serverID,
810
+ };
811
+
812
+ await db.createEmoji(emoji);
813
+
814
+ try {
815
+ // write the file to disk
816
+ await fsp.writeFile("emoji/" + emoji.emojiID, req.file.buffer);
817
+ res.send(msgpack.encode(emoji));
818
+ } catch (_err: unknown) {
819
+ // debugger: emoji write failed
820
+ res.sendStatus(500);
821
+ }
822
+ },
823
+ );
824
+
825
+ // COMPLEX RESOURCES
826
+ api.use("/user", userRouter);
827
+
828
+ api.use("/file", fileRouter);
829
+
830
+ api.use("/avatar", avatarRouter);
831
+
832
+ api.use("/invite", inviteRouter);
833
+
834
+ setupDocs(api);
835
+
836
+ // Central error handler MUST be last. Handles both thrown AppErrors
837
+ // (client-safe status + message) and programmer errors (generic 500
838
+ // with full details logged server-side). See src/server/errors.ts
839
+ // for the CWE mapping.
840
+ api.use(errorHandler());
841
+ };