@vex-chat/spire 0.8.0 → 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 (370) hide show
  1. package/README.md +82 -26
  2. package/dist/ClientManager.d.ts +23 -25
  3. package/dist/ClientManager.js +230 -249
  4. package/dist/ClientManager.js.map +1 -0
  5. package/dist/Database.d.ts +49 -47
  6. package/dist/Database.js +698 -773
  7. package/dist/Database.js.map +1 -0
  8. package/dist/Spire.d.ts +22 -14
  9. package/dist/Spire.js +496 -236
  10. package/dist/Spire.js.map +1 -0
  11. package/dist/__tests__/Database.spec.js +116 -75
  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 +64 -64
  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 +81 -62
  35. package/dist/server/file.js.map +1 -0
  36. package/dist/server/index.d.ts +8 -10
  37. package/dist/server/index.js +414 -405
  38. package/dist/server/index.js.map +1 -0
  39. package/dist/server/invite.d.ts +4 -5
  40. package/dist/server/invite.js +18 -52
  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 +55 -66
  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 -65
  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/052242d0-4129-4a6e-8076-22709c157549 +0 -0
  100. package/avatars/0a677c9a-4986-4b2c-ae2e-12faf22f55db +0 -0
  101. package/avatars/0ba21b91-decb-4a3e-ac86-4dd54d805a9a +0 -0
  102. package/avatars/0c48d8b6-1d1b-4297-a6c6-2fe50af6fc35 +0 -0
  103. package/avatars/0d993cdf-19a6-4299-a4ee-a06579d106cf +0 -0
  104. package/avatars/17b000e4-ac38-46ec-9dec-d2568086129a +0 -0
  105. package/avatars/19dc5594-0f06-4ac6-af18-8740dd39ef6b +0 -0
  106. package/avatars/20444fa3-6d5e-429e-b55f-b81c3d2c61ee +0 -0
  107. package/avatars/21c0512a-5630-4931-9442-d66db66737be +0 -0
  108. package/avatars/22830a60-0b6f-4912-83a5-72245465f332 +0 -0
  109. package/avatars/243639ce-f59f-4404-a1f1-4ec0eb5d2af3 +0 -0
  110. package/avatars/30d2c01d-7b7f-4ea9-9859-1c90837a23f7 +0 -0
  111. package/avatars/315a04f0-9a6f-4b0f-bb9f-5fa774c4752b +0 -0
  112. package/avatars/3563d333-53fe-4885-ac2d-9a4f761db85e +0 -0
  113. package/avatars/36a10c00-3b4c-437f-8e1f-4428ecde0003 +0 -0
  114. package/avatars/40b83eeb-c6e8-4268-82ab-69799a796405 +0 -0
  115. package/avatars/45b5ddb9-ad2c-4404-8ab3-cb4699e6d61c +0 -0
  116. package/avatars/4e4f0ffb-9a75-479a-bccb-446d0bf85020 +0 -0
  117. package/avatars/4e62c3bd-08c6-4fdd-bd65-f01c7322ed64 +0 -0
  118. package/avatars/5004d2e7-51af-44af-8776-6c71f7019843 +0 -0
  119. package/avatars/5041eb29-5c4b-4dea-8c1b-31ba4473161a +0 -0
  120. package/avatars/5065cf78-31c5-46cb-8d5c-c0b6be2d994e +0 -0
  121. package/avatars/51b91d2c-8956-4d73-b4ad-ca6a8d9da9a8 +0 -0
  122. package/avatars/58264a2c-5651-4a42-8ca2-a9907b311e48 +0 -0
  123. package/avatars/58c2357c-8080-4725-a0ce-182c96b037c4 +0 -0
  124. package/avatars/59b5f6dd-8e04-4d15-b4dc-c1c652558a74 +0 -0
  125. package/avatars/5b417a78-b274-48bc-98a4-6e54b74ee62d +0 -0
  126. package/avatars/611f5a93-1ed4-45a1-bc8e-e8e413f9b171 +0 -0
  127. package/avatars/65abe919-9921-46f6-9bc9-183e9cc53c8a +0 -0
  128. package/avatars/6934202d-1546-4270-8a15-97ba8b8c6fa9 +0 -0
  129. package/avatars/6acd24be-f4b7-4399-9e7c-807821828d29 +0 -0
  130. package/avatars/6b2d6ac5-e35c-4297-994e-f0eb6fe56740 +0 -0
  131. package/avatars/6cae9ddd-f163-44e7-a632-30425716a159 +0 -0
  132. package/avatars/6d90e79e-b9c1-4b89-843b-96636de8d26b +0 -0
  133. package/avatars/6f82c7bb-a974-4372-8a64-ce287e668c8c +0 -0
  134. package/avatars/74a45091-5a76-4bb7-ae8a-cd7373adc128 +0 -0
  135. package/avatars/7d071ab2-e0b5-4dbb-8bf5-1fae50c3663f +0 -0
  136. package/avatars/7de818c8-bda1-4f51-976e-160fc087184b +0 -0
  137. package/avatars/873937df-8cb1-427a-92bf-f829f4259624 +0 -0
  138. package/avatars/8b45ffa5-3322-4109-bfa0-1be088336135 +0 -0
  139. package/avatars/8efaa426-22a9-42a2-b4ac-a275717b812f +0 -0
  140. package/avatars/903fd1a6-d6ea-431c-b98f-f21e424a2852 +0 -0
  141. package/avatars/943d8533-5174-4199-990f-1ec69e5d60c4 +0 -0
  142. package/avatars/952d014e-3804-4cd2-a4a0-ffe40a11e4ac +0 -0
  143. package/avatars/95d3acb0-724d-4413-b20f-edad55812d5d +0 -0
  144. package/avatars/9641a946-f613-471b-bedd-c1730b96b51e +0 -0
  145. package/avatars/9b01cbbf-f6b2-43fb-b569-589b6f2a8134 +0 -0
  146. package/avatars/9cd4424d-a34f-4467-acc0-93cf82703e0d +0 -0
  147. package/avatars/9d9ad3b0-e5a6-420a-a6c5-fb9085b70376 +0 -0
  148. package/avatars/9e9e34b5-4e63-4c4b-9722-c7f5674b47aa +0 -0
  149. package/avatars/a387d5c1-59eb-4a6b-80c1-a8982ed12c33 +0 -0
  150. package/avatars/a3e86d21-d881-4824-8ebf-45e3bf0f9186 +0 -0
  151. package/avatars/a8d5cc1c-3f42-4b7b-8d33-f9a9ef77f96b +0 -0
  152. package/avatars/a91d815f-badc-4604-a7be-6c7a44e6101d +0 -0
  153. package/avatars/aa8d0324-bcec-4737-a8c4-bdbff914148d +0 -0
  154. package/avatars/abb8a941-8b6b-47d7-a2f9-8b153ba44aa2 +0 -0
  155. package/avatars/b011bb38-1ef3-4d22-82fa-8bf60faf7b5d +0 -0
  156. package/avatars/b24bcbc1-11f0-473d-a8b9-ba8ce4ca127d +0 -0
  157. package/avatars/b2607346-af1c-4e98-b725-7650a766db2a +0 -0
  158. package/avatars/b6300f7c-cb37-459b-b1bf-8a0a0e797a52 +0 -0
  159. package/avatars/b7d3cff3-84dd-4547-93a1-de1aaa8aa34c +0 -0
  160. package/avatars/baa4b51c-e97f-4f51-bcb3-f27bc506cfaf +0 -0
  161. package/avatars/be7022e4-e292-4515-80d5-f9b61ebeb4ce +0 -0
  162. package/avatars/bed596a3-7569-4854-9e76-f52d33c0a541 +0 -0
  163. package/avatars/bf69992f-3f72-4930-99bb-0ffe17f3aebf +0 -0
  164. package/avatars/ca00c250-c6d4-464d-a6de-1c8467a18fe8 +0 -0
  165. package/avatars/ca19d78f-c0bc-4bd5-b26f-6923cb19996d +0 -0
  166. package/avatars/cda4d6e1-e0a4-4024-ac95-6de98e713b98 +0 -0
  167. package/avatars/cf72c30d-2da8-4e81-aa71-735b9e714274 +0 -0
  168. package/avatars/d5a35b78-99b3-4564-b6b9-b2ccab28c470 +0 -0
  169. package/avatars/dadb38c1-2a9d-47a3-8d92-b56b6166973c +0 -0
  170. package/avatars/e68705c7-375d-4423-9a86-29a16bd3ee0e +0 -0
  171. package/avatars/e9af3e4c-1f62-4302-8b99-b68ce93b7a86 +0 -0
  172. package/avatars/ea7e7331-e845-4189-8248-5f5b1d63f5e3 +0 -0
  173. package/avatars/ef4f8dcb-ef6c-4e7a-9be1-0476161bfce5 +0 -0
  174. package/avatars/ef7d0917-a206-4f88-8b60-93f8253774dc +0 -0
  175. package/avatars/f1a554d6-1db3-4ff5-b0dc-b607d6c3b4ff +0 -0
  176. package/avatars/f1fecc21-c81f-49a8-88f3-f942a0a679f6 +0 -0
  177. package/avatars/f30a2427-1755-4053-813e-129a179e1dd3 +0 -0
  178. package/avatars/f5370717-5109-46a5-a8d7-e1dd996d0615 +0 -0
  179. package/avatars/f6dd7126-1144-4998-bbd3-d4e0fbee2e95 +0 -0
  180. package/avatars/f83413d5-0003-4756-9ece-745fd61cc468 +0 -0
  181. package/avatars/f9b3149e-7ec8-4bb3-a9b9-dcbe66dac197 +0 -0
  182. package/avatars/fa41d70b-857e-4423-bd7d-26ddcddc13b9 +0 -0
  183. package/avatars/fb551ee8-99c7-400b-8e1d-322ce4619998 +0 -0
  184. package/avatars/fe83a6d4-abb0-4ab0-b61d-76a7cc08be84 +0 -0
  185. package/dist/migrations/20210103192527_users.d.ts +0 -3
  186. package/dist/migrations/20210103192527_users.js +0 -30
  187. package/dist/migrations/20210103193502_mail.d.ts +0 -3
  188. package/dist/migrations/20210103193502_mail.js +0 -35
  189. package/dist/migrations/20210103193525_preKeys.d.ts +0 -3
  190. package/dist/migrations/20210103193525_preKeys.js +0 -30
  191. package/dist/migrations/20210103193553_oneTimeKeys.d.ts +0 -3
  192. package/dist/migrations/20210103193553_oneTimeKeys.js +0 -30
  193. package/dist/migrations/20210103193615_servers.d.ts +0 -3
  194. package/dist/migrations/20210103193615_servers.js +0 -28
  195. package/dist/migrations/20210103193729_channels.d.ts +0 -3
  196. package/dist/migrations/20210103193729_channels.js +0 -28
  197. package/dist/migrations/20210103193749_permissions.d.ts +0 -3
  198. package/dist/migrations/20210103193749_permissions.js +0 -30
  199. package/dist/migrations/20210103193801_files.d.ts +0 -3
  200. package/dist/migrations/20210103193801_files.js +0 -28
  201. package/emoji/04d98632-2c86-421b-a407-17f14fe86f8f +0 -0
  202. package/emoji/1160ed6e-1163-4043-9808-4029e863ed30 +0 -0
  203. package/emoji/1547ab18-1635-4a80-a82d-ebbb767b9932 +0 -0
  204. package/emoji/16922521-f6cb-4de4-860c-27916b22c6ba +0 -0
  205. package/emoji/198a9432-0e41-4866-994a-448d4775afcb +0 -0
  206. package/emoji/1be886b3-c9c5-4593-b516-f357ed931f96 +0 -0
  207. package/emoji/1c2b3d1d-637f-4103-b066-4bc4511a3ad7 +0 -0
  208. package/emoji/1efd27e7-b15f-475c-8b32-9159d26b169d +0 -0
  209. package/emoji/270b9409-0ea5-4be2-a239-a8dce13f9c31 +0 -0
  210. package/emoji/27812f76-fee2-49dd-a217-363de6d159dc +0 -0
  211. package/emoji/297ec202-8c24-44c6-aead-689d6d461883 +0 -0
  212. package/emoji/2bf06d86-17cb-4f40-a5ef-bd75d239a1a3 +0 -0
  213. package/emoji/31a75163-1cce-4dc1-b0a2-ecad6a4c500b +0 -0
  214. package/emoji/35235635-fdbd-4273-8428-f3cb3e1e8fd3 +0 -0
  215. package/emoji/3690fff2-6824-4403-a6e3-16a6a54979a9 +0 -0
  216. package/emoji/391014c2-59e0-46a8-85ec-7a7fdaca1d2d +0 -0
  217. package/emoji/3b383dcb-6e76-4e85-8e16-7c68040c06c2 +0 -0
  218. package/emoji/42d617a7-b104-42f5-9618-473181f752cf +0 -0
  219. package/emoji/482495d3-cce9-4f88-bf2a-f6003f03a9b5 +0 -0
  220. package/emoji/48390e06-0efb-404c-89bd-5f2be241bd50 +0 -0
  221. package/emoji/4b808d8d-3248-4149-b919-71b108391bcf +0 -0
  222. package/emoji/4bc13544-d82a-4e32-bd17-a70592274314 +0 -0
  223. package/emoji/4fcebf70-8623-4343-8243-67c8547b2edd +0 -0
  224. package/emoji/509d09aa-1214-459c-8081-50918a17b9af +0 -0
  225. package/emoji/5272abd8-d4d7-4b90-acd2-bf30e6c27243 +0 -0
  226. package/emoji/53c272ce-48bd-4d7e-bfb8-a6482b88be54 +0 -0
  227. package/emoji/5b279e65-06f7-4b26-8b4c-d1b48fba728d +0 -0
  228. package/emoji/5bd141f9-4394-4108-9376-66ebbc2c2bc1 +0 -0
  229. package/emoji/5c769156-f9cb-40bd-ab89-4edeece613fd +0 -0
  230. package/emoji/5c85fba9-8ba7-4fc9-b1b2-48dc30d24a1b +0 -0
  231. package/emoji/61a5e565-d20b-40ba-a139-b0c73a6027f3 +0 -0
  232. package/emoji/6913f43d-dd45-456c-9641-a126104d9ae5 +0 -0
  233. package/emoji/6957e74e-9622-492d-a950-242db3752260 +0 -0
  234. package/emoji/6a14bab5-26af-4bfa-9c17-be7c2511976d +0 -0
  235. package/emoji/6be09439-509e-4095-a30a-b1c7c573895d +0 -0
  236. package/emoji/6cc435b1-fe53-433c-b5a9-2b2019053997 +0 -0
  237. package/emoji/74f1b2af-bc7e-4a0f-802a-64ded185d5e2 +0 -0
  238. package/emoji/7890ba09-f02f-428e-807d-006d03d51d4a +0 -0
  239. package/emoji/7dc69179-6b3c-4f40-b20b-0ff573deea2d +0 -0
  240. package/emoji/7dd1b6b1-439d-4279-916a-995408863172 +0 -0
  241. package/emoji/820498ad-a2c8-43a2-ab83-d26f9c2246d4 +0 -0
  242. package/emoji/8319469c-2787-44e5-91a6-c8c39810dd7c +0 -0
  243. package/emoji/86745d1d-9e59-4607-b2b0-46c741079be1 +0 -0
  244. package/emoji/887b3cff-ae9e-4b5f-ad00-3ca9fc72f689 +0 -0
  245. package/emoji/8c6cf621-71d6-4fca-abe6-e19f4dd7f883 +0 -0
  246. package/emoji/8ca6d32e-a1ef-4956-a416-d8d0d680f085 +0 -0
  247. package/emoji/8d979f5e-38a2-4dd1-b3e4-80938bbe499b +0 -0
  248. package/emoji/99f68ea0-e3fe-4f03-9cc3-5f7f5315404d +0 -0
  249. package/emoji/9ad3aedc-7f79-4d68-a144-82e5b5dc3033 +0 -0
  250. package/emoji/9e418c2f-1f0f-46c4-be39-3bda38a28545 +0 -0
  251. package/emoji/a1f616bf-7402-4e24-9111-18acaebabb48 +0 -0
  252. package/emoji/a25ed9c1-3f9c-4e5f-ade2-7b159fb9fbf4 +0 -0
  253. package/emoji/a5176bc2-39a8-467b-8c75-6fbbc81b59c7 +0 -0
  254. package/emoji/a584215c-6547-438b-8ae8-dd490b51890e +0 -0
  255. package/emoji/a739895f-cf61-4b7c-b350-8e8283aaf751 +0 -0
  256. package/emoji/aaa10dd2-02a2-499e-9e17-c83787436508 +0 -0
  257. package/emoji/ae90baf2-a0ef-4d4d-9cc4-94f8ddd60f45 +0 -0
  258. package/emoji/b0564c48-feae-431a-95f9-df597c6c124c +0 -0
  259. package/emoji/b218bb93-e69c-4793-a669-83316650c4e7 +0 -0
  260. package/emoji/b2998c27-85d5-4598-ab41-469aa8e0fcad +0 -0
  261. package/emoji/b3da08ba-4179-4b5d-826f-5fc15e1a3ad2 +0 -0
  262. package/emoji/b840eb6a-a917-4bb2-854b-8f1022e7904b +0 -0
  263. package/emoji/b84baa76-b4b0-4b83-bc83-78661cb4f1d4 +0 -0
  264. package/emoji/baf69d80-8b1d-4032-855f-605cf0d489c3 +0 -0
  265. package/emoji/bb4d372c-ccd0-4a47-b157-b6a3b9f763e2 +0 -0
  266. package/emoji/bdbd1627-c81d-42d9-b3f5-8979e2ab74dc +0 -0
  267. package/emoji/c257388f-8b85-450b-b168-ebdf8d8c3026 +0 -0
  268. package/emoji/c573fde1-faa9-4c1d-a172-e283645afcfd +0 -0
  269. package/emoji/c8e27810-e8ea-47ce-b7ec-cef0a6becb28 +0 -0
  270. package/emoji/cdeef182-b220-4850-9ecf-5d7c472fd754 +0 -0
  271. package/emoji/d59c1aa9-6f81-4c07-96dd-9953401ff211 +0 -0
  272. package/emoji/dd407dbf-a077-40ba-957f-337b3c5efdc7 +0 -0
  273. package/emoji/e01f6e06-5728-4e2c-90fb-314a5827b766 +0 -0
  274. package/emoji/e1f9ed12-a2ce-433d-b454-b833438a1f9c +0 -0
  275. package/emoji/e697e5c4-acd6-41cd-a43e-edee8da3ab7b +0 -0
  276. package/emoji/e8182220-3464-4e31-8c08-466baead7bfc +0 -0
  277. package/emoji/eb0f3fd5-abc9-4abf-b816-d8458aeb7ec8 +0 -0
  278. package/emoji/eb38ecf7-0d13-4c51-b96a-3777f79321c4 +0 -0
  279. package/emoji/ee515c85-b7ce-4493-a427-994cc0af0d59 +0 -0
  280. package/emoji/f485cef2-d3fa-4d59-88af-b79a3105cacf +0 -0
  281. package/emoji/ff0dab2a-7015-4e8c-b0d0-3569058359dc +0 -0
  282. package/files/01087968-07b6-4fdb-9aeb-fa9dc061be94 +0 -0
  283. package/files/030455b3-17cc-415f-b3b3-2bb56c92ee8b +0 -0
  284. package/files/06129f52-a858-4031-ad85-4d7f2bb793af +0 -0
  285. package/files/0a6155a9-069f-45e9-8a06-56992fe55187 +0 -0
  286. package/files/12b9dda5-feb1-4f20-a987-ad422db5ba73 +0 -0
  287. package/files/13c8fa1e-8821-4628-b607-9e4fa4510df7 +0 -0
  288. package/files/159b0ad3-1a30-419d-af22-28a1096ce825 +0 -0
  289. package/files/1699c563-6769-431b-8041-99a6a4386f25 +0 -0
  290. package/files/176b916c-0dd9-4b93-bfdc-b8ccabf15a96 +0 -0
  291. package/files/1a27dad9-8cc9-4a0c-9d4e-7bde63adda60 +0 -0
  292. package/files/1d29832c-059a-4190-bca6-b83ac77540d9 +0 -0
  293. package/files/1dcde013-8833-4369-8726-81236e4eb30e +0 -0
  294. package/files/2080fc9f-d2c4-4fbc-af84-232fe4900a4f +0 -0
  295. package/files/20889623-6869-46a2-999b-c07708c12521 +0 -0
  296. package/files/2107e243-c378-418d-a183-7df13873c65b +0 -0
  297. package/files/225ed61e-8f8c-4d17-b675-9a9f9918d5b4 +0 -0
  298. package/files/29ffba15-5acc-4ef8-b5a4-5ce61d3f6e85 +0 -0
  299. package/files/2a69434c-1d8a-4e9d-90d4-569aaeaae7e3 +0 -0
  300. package/files/2aec8fcf-25bf-478e-b2f6-fe67ad753071 +0 -0
  301. package/files/2c64490e-c7de-4cb2-b22e-70e2ac69d88f +0 -0
  302. package/files/2d80b4f4-6389-4f5d-8d32-f0ed93820907 +0 -0
  303. package/files/2f9dd26e-363f-4445-8ee5-28548007a33a +0 -0
  304. package/files/360dd8f5-76c4-4e86-ba22-9025dc7ca2a4 +0 -0
  305. package/files/3a0a7f5b-45a5-4340-ba7c-6a0fa2c63871 +0 -0
  306. package/files/3a8fa6be-8acf-4b7b-b653-25edc6b28cdc +0 -5
  307. package/files/3d9e191b-2c15-42aa-9928-c2cdbb5e14ca +0 -0
  308. package/files/3dd0f2ef-0d4e-4837-bffc-22aa645cbe85 +0 -0
  309. package/files/402c4b9b-adbf-4ab6-a21d-c17369b48abf +0 -0
  310. package/files/4685d988-33eb-4902-872f-3b824f497c8b +0 -0
  311. package/files/495f6c55-07f8-4713-a444-a2261a789b94 +0 -0
  312. package/files/4acba8e3-567b-4062-b81b-340e205a01de +0 -0
  313. package/files/4c0a10cc-395b-474a-8140-677ed607da89 +0 -0
  314. package/files/537584d9-25ad-4830-808a-a1e3d63e2a52 +0 -0
  315. package/files/5519f10d-745e-4474-8ca8-6c111693704c +0 -0
  316. package/files/5563cf92-a5e3-4be3-867d-9647a02298d4 +0 -0
  317. package/files/55b70d9b-fd58-4800-832c-d6b4521ba6d0 +0 -0
  318. package/files/5623cff3-ce9b-403b-9915-50d2bcbc981c +0 -0
  319. package/files/576314ba-2a1a-4753-8d77-2a9e04f509d1 +0 -0
  320. package/files/58ed97ae-0eac-4e04-add1-76646effa2d5 +0 -0
  321. package/files/68638efd-5389-481d-a841-36164c62c078 +0 -0
  322. package/files/6936d34b-a1f8-4a9d-b005-5544dbdcf5e5 +0 -0
  323. package/files/6bda9e88-3a28-47f7-994e-900ede6bf984 +0 -0
  324. package/files/7024a3b9-a863-4618-9dbd-fa6502017ae0 +0 -0
  325. package/files/707caccb-3780-456d-9056-c20bfdfc0e5b +0 -0
  326. package/files/74b8c73c-032b-4640-a5b3-d30dd270cbcb +0 -0
  327. package/files/767cb262-2974-40f8-8182-f7770b431923 +0 -0
  328. package/files/7a669935-cb48-4349-b26b-7705f8a04fbc +0 -0
  329. package/files/7bc8f678-6ee7-454c-a1f8-bb9358b89c95 +0 -0
  330. package/files/7ccc2699-07ec-4177-a9ce-ee7dc952fda1 +0 -0
  331. package/files/7e0eabf4-b334-4683-8156-ab8d949a0532 +0 -0
  332. package/files/7f0644ab-02a3-4121-adfc-29a7c55cf804 +0 -0
  333. package/files/7ff3266d-f103-48ce-90dd-95b9dfe5fcc9 +0 -0
  334. package/files/836e2e8e-aefd-4b4a-a9b9-bf7436158a8c +0 -0
  335. package/files/875f7ae5-fa23-4fc5-b04a-8433a7f7089c +0 -0
  336. package/files/8ca62da9-f204-49e3-b418-9451661b2904 +0 -0
  337. package/files/9283054c-107f-474d-b61e-f1d0061bcb86 +0 -0
  338. package/files/93b1ce7f-9566-452b-b4be-30f87d3de150 +0 -0
  339. package/files/93fb51c7-e19b-4ac1-9dc3-aeb8da0672ed +0 -0
  340. package/files/9b54a3a1-534a-4ed5-b016-3c74ed4c9edd +0 -0
  341. package/files/9b5beb6f-712d-4969-b127-fd66c9b2a9c6 +0 -0
  342. package/files/9e964fbf-d063-498e-b2e3-79f8d6afcf5f +0 -0
  343. package/files/a66b7a9f-58c2-47a8-a429-a6f0647c6fe9 +0 -0
  344. package/files/a7cbda7d-81ba-40f7-a997-51146af63e5f +0 -0
  345. package/files/ac01e83a-e572-41b6-81ab-c992cff7c170 +0 -0
  346. package/files/ad11a58f-f963-4233-bd29-1658b6b7e600 +0 -0
  347. package/files/ae60a4a8-08b9-4521-a0a3-d015a8b3ed08 +0 -0
  348. package/files/b1d3cf27-8d76-4cf9-aa51-d7c6bfd1b3bf +0 -0
  349. package/files/b2c68863-8554-4ac6-8e99-821f0267cf91 +0 -0
  350. package/files/b3043e01-a771-44af-bf19-5327646ff929 +0 -0
  351. package/files/b6d01b89-def5-4c7c-8e97-a3d88617f8f4 +0 -0
  352. package/files/b8760b32-bb1e-4cd7-a9d6-29c6e0b071bc +0 -0
  353. package/files/ba5e0470-44f7-47b3-bcb5-eeab3ca8c292 +0 -0
  354. package/files/c3969b5f-43ae-43f3-9bdd-d3959c79ca01 +0 -0
  355. package/files/caecd488-dbe6-4a30-a400-bced2ba8dae6 +0 -0
  356. package/files/d7d865b8-3a05-4ed1-b95d-b93dc1ebb9a9 +0 -0
  357. package/files/dbca5f31-cf38-4c1e-83b0-5ec8473196fc +0 -0
  358. package/files/dbf07e82-fff6-4985-bebe-d62c0458bfd0 +0 -0
  359. package/files/dd759c20-eead-4e57-9e74-4d3a2b978e91 +0 -5
  360. package/files/de0b2cf1-981b-4e4a-a04e-ac185d1620cd +0 -0
  361. package/files/de6837fe-5aa2-4ff9-a067-2646a008c780 +0 -0
  362. package/files/e2fde852-91cb-4e01-88a2-ee086b5f227c +0 -0
  363. package/files/e391d1ce-8d39-460c-a462-791730131f7f +0 -0
  364. package/files/e6d9b60f-2b1b-4c6f-ba6d-02f6de90d40f +0 -0
  365. package/files/f693c62d-2ac8-49fd-aa38-30904e013e3c +0 -0
  366. package/files/f749fdf5-193e-41f7-b643-5696f67c6402 +0 -0
  367. package/files/f8910384-e75c-4d65-825f-52a6748f6475 +0 -0
  368. package/files/fad8826b-e952-4acb-a509-3e6543b94d61 +0 -0
  369. package/jest.config.js +0 -13
  370. package/spire.sqlite +0 -0
package/dist/Spire.js CHANGED
@@ -1,128 +1,166 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
5
- }) : (function(o, m, k, k2) {
6
- if (k2 === undefined) k2 = k;
7
- o[k2] = m[k];
8
- }));
9
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
10
- Object.defineProperty(o, "default", { enumerable: true, value: v });
11
- }) : function(o, v) {
12
- o["default"] = v;
13
- });
14
- var __importStar = (this && this.__importStar) || function (mod) {
15
- if (mod && mod.__esModule) return mod;
16
- var result = {};
17
- if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
18
- __setModuleDefault(result, mod);
19
- return result;
20
- };
21
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
22
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
23
- return new (P || (P = Promise))(function (resolve, reject) {
24
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
25
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
26
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
27
- step((generator = generator.apply(thisArg, _arguments || [])).next());
28
- });
29
- };
30
- var __importDefault = (this && this.__importDefault) || function (mod) {
31
- return (mod && mod.__esModule) ? mod : { "default": mod };
32
- };
33
- Object.defineProperty(exports, "__esModule", { value: true });
34
- exports.Spire = exports.JWT_EXPIRY = exports.TOKEN_EXPIRY = void 0;
35
- const fs_1 = __importDefault(require("fs"));
36
- const crypto_1 = require("@vex-chat/crypto");
37
- const types_1 = require("@vex-chat/types");
38
- const events_1 = require("events");
39
- const express_1 = __importDefault(require("express"));
40
- const express_ws_1 = __importDefault(require("express-ws"));
41
- const tweetnacl_1 = __importDefault(require("tweetnacl"));
42
- const uuid = __importStar(require("uuid"));
43
- const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
44
- const msgpack_lite_1 = __importDefault(require("msgpack-lite"));
45
- const ClientManager_1 = require("./ClientManager");
46
- const Database_1 = require("./Database");
47
- const server_1 = require("./server");
48
- const utils_1 = require("./server/utils");
49
- const createLogger_1 = require("./utils/createLogger");
1
+ import { EventEmitter } from "events";
2
+ import { execSync } from "node:child_process";
3
+ import * as fs from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import express from "express";
7
+ import { XUtils } from "@vex-chat/crypto";
8
+ import { xRandomBytes, xSignKeyPairFromSecret, xSignOpen, } from "@vex-chat/crypto";
9
+ import { MailWSSchema, RegistrationPayloadSchema, TokenScopes, UserSchema, } from "@vex-chat/types";
10
+ import jwt from "jsonwebtoken";
11
+ import { stringify as uuidStringify } from "uuid";
12
+ import { WebSocketServer } from "ws";
13
+ import { z } from "zod/v4";
14
+ import { ClientManager } from "./ClientManager.js";
15
+ import { Database, hashPassword } from "./Database.js";
16
+ import { initApp, protect } from "./server/index.js";
17
+ import { authLimiter } from "./server/rateLimit.js";
18
+ import { censorUser, getParam, getUser } from "./server/utils.js";
19
+ import { createLogger } from "./utils/createLogger.js";
20
+ import { getJwtSecret } from "./utils/jwtSecret.js";
21
+ import { msgpack } from "./utils/msgpack.js";
50
22
  // expiry of regkeys = 24hr
51
- exports.TOKEN_EXPIRY = 1000 * 60 * 10;
52
- exports.JWT_EXPIRY = "7d";
23
+ export const TOKEN_EXPIRY = 1000 * 60 * 10;
24
+ export const JWT_EXPIRY = "7d";
25
+ export const DEVICE_AUTH_JWT_EXPIRY = "1h";
26
+ const DEVICE_CHALLENGE_EXPIRY = 1000 * 60; // 60 seconds
27
+ const STATUS_LATENCY_BUDGET_MS = 250;
53
28
  // 3-19 chars long
54
29
  const usernameRegex = /^(\w{3,19})$/;
30
+ // ── Zod schemas for trust-boundary validation ──────────────────────────
31
+ const wsAuthMsg = z.object({
32
+ token: z.string().min(1),
33
+ type: z.literal("auth"),
34
+ });
35
+ const jwtPayload = z.object({
36
+ bearerToken: z.string().optional(),
37
+ exp: z.number().optional(),
38
+ user: UserSchema,
39
+ });
40
+ const authPayload = z.object({
41
+ password: z.string().min(1),
42
+ username: z.string().min(1),
43
+ });
44
+ const deviceAuthPayload = z.object({
45
+ deviceID: z.string().min(1),
46
+ signKey: z.string().min(1),
47
+ });
48
+ const deviceVerifyPayload = z.object({
49
+ challengeID: z.string().min(1),
50
+ signed: z.string().min(1),
51
+ });
52
+ const mailPostPayload = z.object({
53
+ header: z.custom((val) => val instanceof Uint8Array),
54
+ mail: MailWSSchema,
55
+ });
55
56
  const directories = ["files", "avatars", "emoji"];
56
57
  for (const dir of directories) {
57
- if (!fs_1.default.existsSync(dir)) {
58
- fs_1.default.mkdirSync(dir);
58
+ if (!fs.existsSync(dir)) {
59
+ fs.mkdirSync(dir);
59
60
  }
60
61
  }
61
- const TokenScopes = types_1.XTypes.HTTP.TokenScopes;
62
- class Spire extends events_1.EventEmitter {
62
+ const getAppVersion = () => {
63
+ try {
64
+ const raw = fs.readFileSync(new URL("../package.json", import.meta.url), {
65
+ encoding: "utf8",
66
+ });
67
+ const pkg = JSON.parse(raw);
68
+ if (typeof pkg === "object" &&
69
+ pkg !== null &&
70
+ "version" in pkg &&
71
+ typeof pkg.version === "string") {
72
+ return pkg.version;
73
+ }
74
+ return "unknown";
75
+ }
76
+ catch {
77
+ return "unknown";
78
+ }
79
+ };
80
+ const getCommitSha = () => {
81
+ const sourceDir = path.dirname(fileURLToPath(import.meta.url));
82
+ const repoRoot = path.resolve(sourceDir, "..");
83
+ try {
84
+ const sha = execSync("git rev-parse --verify --short=12 HEAD", {
85
+ cwd: repoRoot,
86
+ encoding: "utf8",
87
+ stdio: ["ignore", "pipe", "ignore"],
88
+ timeout: 1500,
89
+ }).trim();
90
+ return sha || "unknown";
91
+ }
92
+ catch {
93
+ return "unknown";
94
+ }
95
+ };
96
+ export class Spire extends EventEmitter {
97
+ actionTokens = [];
98
+ api = express();
99
+ clients = [];
100
+ commitSha = getCommitSha();
101
+ db;
102
+ dbReady = false;
103
+ deviceChallenges = new Map();
104
+ log;
105
+ options;
106
+ queuedRequestIncrements = 0;
107
+ requestsTotal = 0;
108
+ requestsTotalLoaded = false;
109
+ server = null;
110
+ signKeys;
111
+ startedAt = new Date();
112
+ version = getAppVersion();
113
+ wss = new WebSocketServer({ noServer: true });
63
114
  constructor(SK, options) {
64
115
  super();
65
- this.clients = [];
66
- this.expWs = express_ws_1.default(express_1.default());
67
- this.api = this.expWs.app;
68
- this.wss = this.expWs.getWss();
69
- this.actionTokens = [];
70
- this.server = null;
71
- this.signKeys = tweetnacl_1.default.sign.keyPair.fromSecretKey(crypto_1.XUtils.decodeHex(SK));
72
- this.db = new Database_1.Database(options);
73
- this.log = createLogger_1.createLogger("spire", (options === null || options === void 0 ? void 0 : options.logLevel) || "error");
74
- this.init((options === null || options === void 0 ? void 0 : options.apiPort) || 16777);
116
+ this.signKeys = xSignKeyPairFromSecret(XUtils.decodeHex(SK));
117
+ // Trust a single proxy hop (nginx / cloudflare / load balancer).
118
+ // Required so `req.ip` and `express-rate-limit`'s keyGenerator see
119
+ // the real client address via X-Forwarded-For. Never use `true` —
120
+ // that lets attackers spoof the header and bypass rate limiting.
121
+ // If spire is deployed without a proxy, set this to 0 instead.
122
+ this.api.set("trust proxy", 1);
123
+ this.db = new Database(options);
124
+ this.db.on("ready", () => {
125
+ this.dbReady = true;
126
+ this.bootstrapRequestCounter().catch((err) => {
127
+ this.log.error("Failed to load persisted request counter: " + String(err));
128
+ });
129
+ });
130
+ this.log = createLogger("spire", options?.logLevel || "error");
131
+ this.init(options?.apiPort || 16777);
75
132
  this.options = options;
76
133
  }
77
- close() {
78
- var _a, _b;
79
- return __awaiter(this, void 0, void 0, function* () {
80
- this.wss.clients.forEach((ws) => {
81
- ws.terminate();
82
- });
83
- this.wss.on("close", () => {
84
- this.log.info("ws: closed.");
85
- });
86
- (_a = this.server) === null || _a === void 0 ? void 0 : _a.on("close", () => {
87
- this.log.info("http: closed.");
88
- });
89
- (_b = this.server) === null || _b === void 0 ? void 0 : _b.close();
90
- this.wss.close();
91
- yield this.db.close();
92
- return;
134
+ async close() {
135
+ this.wss.clients.forEach((ws) => {
136
+ ws.terminate();
93
137
  });
138
+ this.wss.on("close", () => {
139
+ this.log.info("ws: closed.");
140
+ });
141
+ this.server?.on("close", () => {
142
+ this.log.info("http: closed.");
143
+ });
144
+ this.server?.close();
145
+ this.wss.close();
146
+ await this.db.close();
147
+ return;
94
148
  }
95
- notify(userID, event, transmissionID, data, deviceID) {
96
- for (const client of this.clients) {
97
- if (deviceID) {
98
- if (client.getDevice().deviceID === deviceID) {
99
- const msg = {
100
- transmissionID,
101
- type: "notify",
102
- event,
103
- data,
104
- };
105
- client.send(msg);
106
- }
107
- }
108
- else {
109
- if (client.getUser().userID === userID) {
110
- const msg = {
111
- transmissionID,
112
- type: "notify",
113
- event,
114
- data,
115
- };
116
- client.send(msg);
117
- }
118
- }
149
+ async bootstrapRequestCounter() {
150
+ const persistedTotal = await this.db.getRequestsTotal();
151
+ const startupIncrements = this.queuedRequestIncrements;
152
+ this.queuedRequestIncrements = 0;
153
+ this.requestsTotal = persistedTotal + startupIncrements;
154
+ if (startupIncrements > 0) {
155
+ await this.db.incrementRequestsTotal(startupIncrements);
119
156
  }
157
+ this.requestsTotalLoaded = true;
120
158
  }
121
159
  createActionToken(scope) {
122
160
  const token = {
123
- key: uuid.v4(),
124
- time: new Date(Date.now()),
161
+ key: crypto.randomUUID(),
125
162
  scope,
163
+ time: new Date().toISOString(),
126
164
  };
127
165
  this.actionTokens.push(token);
128
166
  return token;
@@ -132,69 +170,93 @@ class Spire extends events_1.EventEmitter {
132
170
  this.actionTokens.splice(this.actionTokens.indexOf(key), 1);
133
171
  }
134
172
  }
135
- validateToken(key, scope) {
136
- this.log.info("Validating token: " + key);
137
- for (const rKey of this.actionTokens) {
138
- if (rKey.key === key) {
139
- if (rKey.scope !== scope) {
140
- continue;
141
- }
142
- const age = new Date(Date.now()).getTime() - rKey.time.getTime();
143
- this.log.info("Token found, " + age + " ms old.");
144
- if (age < exports.TOKEN_EXPIRY) {
145
- this.log.info("Token is valid.");
146
- this.deleteActionToken(rKey);
147
- return true;
148
- }
149
- else {
150
- this.log.info("Token is expired.");
151
- }
152
- }
153
- }
154
- this.log.info("Token not found.");
155
- return false;
156
- }
157
173
  init(apiPort) {
174
+ this.api.use((_req, _res, next) => {
175
+ this.requestsTotal += 1;
176
+ if (!this.requestsTotalLoaded) {
177
+ this.queuedRequestIncrements += 1;
178
+ }
179
+ else {
180
+ this.db.incrementRequestsTotal(1).catch((err) => {
181
+ this.log.warn("Failed to persist request counter increment: " +
182
+ String(err));
183
+ });
184
+ }
185
+ next();
186
+ });
158
187
  // initialize the expression app configuration with loose routes/handlers
159
- server_1.initApp(this.api, this.db, this.log, this.validateToken.bind(this), this.signKeys, this.notify.bind(this));
160
- // All the app logic strongly coupled to spire class :/
161
- this.api.ws("/socket", (ws, req) => {
162
- const userDetails = req.user;
163
- if (!userDetails) {
164
- this.log.warn("User attempted to open socket with no jwt.");
165
- const err = {
166
- type: "unauthorized",
167
- transmissionID: uuid.v4(),
168
- };
169
- const msg = crypto_1.XUtils.packMessage(err);
170
- ws.send(msg);
188
+ initApp(this.api, this.db, this.log, this.validateToken.bind(this), this.signKeys, this.notify.bind(this));
189
+ // WS auth: client sends { type: "auth", token } as first message
190
+ this.wss.on("connection", (ws) => {
191
+ this.log.info("WS connection established, waiting for auth...");
192
+ const AUTH_TIMEOUT = 10_000;
193
+ const timer = setTimeout(() => {
194
+ this.log.warn("WS auth timeout — closing.");
171
195
  ws.close();
172
- return;
173
- }
174
- this.log.info("New client initiated.");
175
- this.log.info(JSON.stringify(userDetails));
176
- const client = new ClientManager_1.ClientManager(ws, this.db, this.notify.bind(this), userDetails, this.options);
177
- client.on("fail", () => {
178
- this.log.info("Client connection is down, removing: " + client.toString());
179
- if (this.clients.includes(client)) {
180
- this.clients.splice(this.clients.indexOf(client), 1);
196
+ }, AUTH_TIMEOUT);
197
+ const onFirstMessage = (data) => {
198
+ const str = Buffer.isBuffer(data)
199
+ ? data.toString()
200
+ : data instanceof ArrayBuffer
201
+ ? Buffer.from(data).toString()
202
+ : Buffer.concat(data).toString();
203
+ clearTimeout(timer);
204
+ ws.off("message", onFirstMessage);
205
+ try {
206
+ const rawParsed = JSON.parse(str);
207
+ const authResult = wsAuthMsg.safeParse(rawParsed);
208
+ if (!authResult.success) {
209
+ throw new Error("Expected { type: 'auth', token }, got: " +
210
+ JSON.stringify(authResult.error.issues));
211
+ }
212
+ const result = jwt.verify(authResult.data.token, getJwtSecret());
213
+ const jwtResult = jwtPayload.safeParse(result);
214
+ if (!jwtResult.success) {
215
+ throw new Error("Invalid JWT payload: " +
216
+ JSON.stringify(jwtResult.error.issues));
217
+ }
218
+ const userDetails = jwtResult.data.user;
219
+ this.log.info("WS auth succeeded for " + userDetails.username);
220
+ const client = new ClientManager(ws, this.db, this.notify.bind(this), userDetails, this.options);
221
+ client.on("fail", () => {
222
+ this.log.info("Client connection is down, removing: " +
223
+ client.toString());
224
+ if (this.clients.includes(client)) {
225
+ this.clients.splice(this.clients.indexOf(client), 1);
226
+ }
227
+ this.log.info("Current authorized clients: " +
228
+ String(this.clients.length));
229
+ });
230
+ client.on("authed", () => {
231
+ this.log.info("New client authorized: " + client.toString());
232
+ this.clients.push(client);
233
+ this.log.info("Current authorized clients: " +
234
+ String(this.clients.length));
235
+ });
181
236
  }
182
- this.log.info("Current authorized clients: " + this.clients.length);
183
- });
184
- client.on("authed", () => {
185
- this.log.info("New client authorized: " + client.toString());
186
- this.clients.push(client);
187
- this.log.info("Current authorized clients: " + this.clients.length);
237
+ catch (err) {
238
+ this.log.warn("WS auth failed: " + String(err));
239
+ const errMsg = {
240
+ transmissionID: crypto.randomUUID(),
241
+ type: "unauthorized",
242
+ };
243
+ ws.send(XUtils.packMessage(errMsg));
244
+ ws.close();
245
+ }
246
+ };
247
+ ws.on("message", onFirstMessage);
248
+ ws.on("close", () => {
249
+ clearTimeout(timer);
188
250
  });
189
251
  });
190
252
  this.api.get("/token/:tokenType", (req, res, next) => {
191
- if (req.params.tokenType !== "register") {
192
- server_1.protect(req, res, next);
253
+ if (getParam(req, "tokenType") !== "register") {
254
+ protect(req, res, next);
193
255
  }
194
256
  else {
195
257
  next();
196
258
  }
197
- }, (req, res) => __awaiter(this, void 0, void 0, function* () {
259
+ }, (req, res) => {
198
260
  const allowedTokens = [
199
261
  "file",
200
262
  "register",
@@ -204,33 +266,33 @@ class Spire extends events_1.EventEmitter {
204
266
  "emoji",
205
267
  "connect",
206
268
  ];
207
- const { tokenType } = req.params;
269
+ const tokenType = getParam(req, "tokenType");
208
270
  if (!allowedTokens.includes(tokenType)) {
209
271
  res.sendStatus(400);
210
272
  return;
211
273
  }
212
274
  let scope;
213
275
  switch (tokenType) {
214
- case "file":
215
- scope = TokenScopes.File;
216
- break;
217
- case "register":
218
- scope = TokenScopes.Register;
219
- break;
220
276
  case "avatar":
221
277
  scope = TokenScopes.Avatar;
222
278
  break;
279
+ case "connect":
280
+ scope = TokenScopes.Connect;
281
+ break;
223
282
  case "device":
224
283
  scope = TokenScopes.Device;
225
284
  break;
226
- case "invite":
227
- scope = TokenScopes.Invite;
228
- break;
229
285
  case "emoji":
230
286
  scope = TokenScopes.Emoji;
231
287
  break;
232
- case "connect":
233
- scope = TokenScopes.Connect;
288
+ case "file":
289
+ scope = TokenScopes.File;
290
+ break;
291
+ case "invite":
292
+ scope = TokenScopes.Invite;
293
+ break;
294
+ case "register":
295
+ scope = TokenScopes.Register;
234
296
  break;
235
297
  default:
236
298
  res.sendStatus(400);
@@ -242,110 +304,250 @@ class Spire extends events_1.EventEmitter {
242
304
  this.log.info("New token created: " + token.key);
243
305
  setTimeout(() => {
244
306
  this.deleteActionToken(token);
245
- }, exports.TOKEN_EXPIRY);
246
- return res.send(msgpack_lite_1.default.encode(token));
307
+ }, TOKEN_EXPIRY);
308
+ const acceptHeader = req.get("accept")?.toLowerCase() || "";
309
+ const wantsJson = acceptHeader.includes("application/json") &&
310
+ !acceptHeader.includes("application/msgpack") &&
311
+ !acceptHeader.includes("*/*");
312
+ if (wantsJson) {
313
+ return res.json(token);
314
+ }
315
+ res.set("Content-Type", "application/msgpack");
316
+ return res.send(msgpack.encode(token));
247
317
  }
248
318
  catch (err) {
249
- console.error(err.toString());
319
+ this.log.error(String(err));
250
320
  return res.sendStatus(500);
251
321
  }
252
- }));
253
- this.api.post("/whoami", (req, res) => __awaiter(this, void 0, void 0, function* () {
322
+ });
323
+ this.api.post("/whoami", (req, res) => {
254
324
  if (!req.user) {
255
325
  res.sendStatus(401);
256
326
  return;
257
327
  }
258
- res.send(msgpack_lite_1.default.encode({
259
- user: req.user,
328
+ res.send(msgpack.encode({
260
329
  exp: req.exp,
261
- token: req.cookies.auth,
330
+ token: req.bearerToken,
331
+ user: req.user,
262
332
  }));
263
- }));
264
- this.api.post("/goodbye", server_1.protect, (req, res) => __awaiter(this, void 0, void 0, function* () {
265
- const token = jsonwebtoken_1.default.sign({ user: utils_1.censorUser(req.user) }, process.env.SPK, { expiresIn: -1 });
266
- res.cookie("auth", token, { path: "/" });
267
- res.sendStatus(200);
268
- }));
269
- this.api.post("/mail", server_1.protect, (req, res) => __awaiter(this, void 0, void 0, function* () {
270
- const senderDeviceDetails = req.device;
271
- if (!senderDeviceDetails) {
272
- res.sendStatus(401);
333
+ });
334
+ this.api.get("/healthz", (_req, res) => {
335
+ if (!this.dbReady) {
336
+ res.status(503).json({ dbReady: false, ok: false });
273
337
  return;
274
338
  }
275
- const authorUserDetails = req.user;
276
- const { header, mail, } = req.body;
339
+ res.json({ dbReady: true, ok: true });
340
+ });
341
+ this.api.get("/status", async (_req, res) => {
342
+ const started = Date.now();
343
+ const dbHealthy = this.dbReady ? await this.db.isHealthy() : false;
344
+ const checkDurationMs = Date.now() - started;
345
+ const ok = dbHealthy;
346
+ res.json({
347
+ checkDurationMs,
348
+ commitSha: this.commitSha,
349
+ dbHealthy,
350
+ dbReady: this.dbReady,
351
+ latencyBudgetMs: STATUS_LATENCY_BUDGET_MS,
352
+ metrics: {
353
+ requestsTotal: this.requestsTotal,
354
+ },
355
+ now: new Date(),
356
+ ok,
357
+ startedAt: this.startedAt.toISOString(),
358
+ uptimeSeconds: Math.floor(process.uptime()),
359
+ version: this.version,
360
+ withinLatencyBudget: checkDurationMs <= STATUS_LATENCY_BUDGET_MS,
361
+ });
362
+ });
363
+ this.api.post("/goodbye", protect, (req, res) => {
364
+ jwt.sign({ user: req.user }, getJwtSecret(), { expiresIn: -1 });
365
+ res.sendStatus(200);
366
+ });
367
+ // ── Device-key auth ──────────────────────────────────────────
368
+ this.api.post("/auth/device", authLimiter, async (req, res) => {
277
369
  try {
278
- yield this.db.saveMail(mail, header, senderDeviceDetails.deviceID, authorUserDetails.userID);
279
- this.log.info("Received mail for " + mail.recipient);
280
- const recipientDeviceDetails = yield this.db.retrieveDevice(mail.recipient);
281
- if (!recipientDeviceDetails) {
282
- res.sendStatus(400);
283
- return;
370
+ const parsed = deviceAuthPayload.safeParse(req.body);
371
+ if (!parsed.success) {
372
+ return res
373
+ .status(400)
374
+ .send({ error: "deviceID and signKey required." });
375
+ }
376
+ const { deviceID, signKey } = parsed.data;
377
+ const device = await this.db.retrieveDevice(deviceID);
378
+ if (!device || device.signKey !== signKey) {
379
+ return res.status(404).send({ error: "Device not found." });
284
380
  }
285
- res.sendStatus(200);
286
- this.notify(recipientDeviceDetails.owner, "mail", uuid.v4(), null, mail.recipient);
381
+ // Generate challenge nonce (32 bytes)
382
+ const nonce = XUtils.encodeHex(xRandomBytes(32));
383
+ const challengeID = crypto.randomUUID();
384
+ this.deviceChallenges.set(challengeID, {
385
+ deviceID,
386
+ nonce,
387
+ time: Date.now(),
388
+ });
389
+ // Clean up expired challenges
390
+ setTimeout(() => {
391
+ this.deviceChallenges.delete(challengeID);
392
+ }, DEVICE_CHALLENGE_EXPIRY);
393
+ this.log.info("Device challenge issued for " + deviceID);
394
+ return res.send(msgpack.encode({ challenge: nonce, challengeID }));
287
395
  }
288
396
  catch (err) {
289
- this.log.error(err);
290
- res.status(500).send(err.toString());
397
+ this.log.error("Device challenge error: " + String(err));
398
+ return res.sendStatus(500);
291
399
  }
292
- }));
293
- this.api.post("/auth", (req, res) => __awaiter(this, void 0, void 0, function* () {
294
- const credentials = req.body;
295
- if (typeof credentials.password !== "string") {
296
- res.status(400).send("Password is required and must be a string.");
400
+ });
401
+ this.api.post("/auth/device/verify", authLimiter, async (req, res) => {
402
+ try {
403
+ const parsed = deviceVerifyPayload.safeParse(req.body);
404
+ if (!parsed.success) {
405
+ return res.status(400).send({
406
+ error: "challengeID and signed required.",
407
+ });
408
+ }
409
+ const { challengeID, signed } = parsed.data;
410
+ const challenge = this.deviceChallenges.get(challengeID);
411
+ if (!challenge) {
412
+ return res.status(401).send({
413
+ error: "Challenge expired or not found.",
414
+ });
415
+ }
416
+ // Consume the challenge (single-use)
417
+ this.deviceChallenges.delete(challengeID);
418
+ // Check expiry
419
+ if (Date.now() - challenge.time > DEVICE_CHALLENGE_EXPIRY) {
420
+ return res
421
+ .status(401)
422
+ .send({ error: "Challenge expired." });
423
+ }
424
+ // Look up the device to get its public signKey
425
+ const device = await this.db.retrieveDevice(challenge.deviceID);
426
+ if (!device) {
427
+ return res.status(404).send({ error: "Device not found." });
428
+ }
429
+ // Verify the Ed25519 signature
430
+ const opened = xSignOpen(XUtils.decodeHex(signed), XUtils.decodeHex(device.signKey));
431
+ if (!opened) {
432
+ return res
433
+ .status(401)
434
+ .send({ error: "Signature verification failed." });
435
+ }
436
+ // Verify the signed content matches the challenge nonce
437
+ const signedNonce = XUtils.encodeHex(opened);
438
+ if (signedNonce !== challenge.nonce) {
439
+ return res
440
+ .status(401)
441
+ .send({ error: "Challenge mismatch." });
442
+ }
443
+ // Look up device owner
444
+ const user = await this.db.retrieveUser(device.owner);
445
+ if (!user) {
446
+ return res
447
+ .status(404)
448
+ .send({ error: "Device owner not found." });
449
+ }
450
+ // Issue short-lived JWT (1 hour, not 7 days)
451
+ const token = jwt.sign({ user: censorUser(user) }, getJwtSecret(), { expiresIn: DEVICE_AUTH_JWT_EXPIRY });
452
+ this.log.info("Device-key auth succeeded for " +
453
+ user.username +
454
+ " (device " +
455
+ device.deviceID +
456
+ ")");
457
+ return res.send(msgpack.encode({ token, user: censorUser(user) }));
458
+ }
459
+ catch (err) {
460
+ this.log.error("Device verify error: " + String(err));
461
+ return res.sendStatus(500);
462
+ }
463
+ });
464
+ this.api.post("/mail", protect, async (req, res) => {
465
+ const senderDeviceDetails = req.device;
466
+ if (!senderDeviceDetails) {
467
+ res.sendStatus(401);
468
+ return;
469
+ }
470
+ const authorUserDetails = getUser(req);
471
+ const parsed = mailPostPayload.safeParse(req.body);
472
+ if (!parsed.success) {
473
+ res.status(400).json({
474
+ error: "Invalid mail payload",
475
+ issues: parsed.error.issues,
476
+ });
477
+ return;
478
+ }
479
+ const { header, mail } = parsed.data;
480
+ await this.db.saveMail(mail, header, senderDeviceDetails.deviceID, authorUserDetails.userID);
481
+ this.log.info("Received mail for " + mail.recipient);
482
+ const recipientDeviceDetails = await this.db.retrieveDevice(mail.recipient);
483
+ if (!recipientDeviceDetails) {
484
+ res.sendStatus(400);
297
485
  return;
298
486
  }
299
- if (typeof credentials.username !== "string") {
300
- res.status(400).send("Username is required and must be a string.");
487
+ res.sendStatus(200);
488
+ this.notify(recipientDeviceDetails.owner, "mail", crypto.randomUUID(), null, mail.recipient);
489
+ });
490
+ this.api.post("/auth", authLimiter, async (req, res) => {
491
+ const parsed = authPayload.safeParse(req.body);
492
+ if (!parsed.success) {
493
+ res.status(400).json({
494
+ error: "Invalid credentials format",
495
+ });
301
496
  return;
302
497
  }
498
+ const { password, username } = parsed.data;
303
499
  try {
304
- const userEntry = yield this.db.retrieveUser(credentials.username);
500
+ const userEntry = await this.db.retrieveUser(username);
305
501
  if (!userEntry) {
306
502
  res.sendStatus(404);
307
503
  this.log.warn("User does not exist.");
308
504
  return;
309
505
  }
310
- const salt = crypto_1.XUtils.decodeHex(userEntry.passwordSalt);
311
- const payloadHash = crypto_1.XUtils.encodeHex(Database_1.hashPassword(credentials.password, salt));
506
+ const salt = XUtils.decodeHex(userEntry.passwordSalt);
507
+ const payloadHash = XUtils.encodeHex(hashPassword(password, salt));
312
508
  if (payloadHash !== userEntry.passwordHash) {
313
509
  res.sendStatus(401);
314
510
  return;
315
511
  }
316
- const token = jsonwebtoken_1.default.sign({ user: utils_1.censorUser(userEntry) }, process.env.SPK, { expiresIn: exports.JWT_EXPIRY });
512
+ const token = jwt.sign({ user: censorUser(userEntry) }, getJwtSecret(), { expiresIn: JWT_EXPIRY });
317
513
  // just to make sure
318
- jsonwebtoken_1.default.verify(token, process.env.SPK);
319
- res.cookie("auth", token, { path: "/" });
320
- res.send(msgpack_lite_1.default.encode({ user: utils_1.censorUser(userEntry), token }));
514
+ jwt.verify(token, getJwtSecret());
515
+ res.send(msgpack.encode({ token, user: censorUser(userEntry) }));
321
516
  }
322
517
  catch (err) {
323
- this.log.error(err.toString());
518
+ this.log.error(String(err));
324
519
  res.sendStatus(500);
325
520
  }
326
- }));
327
- this.api.post("/register", (req, res) => __awaiter(this, void 0, void 0, function* () {
521
+ });
522
+ this.api.post("/register", authLimiter, async (req, res) => {
328
523
  try {
329
- const regPayload = req.body;
524
+ const regParsed = RegistrationPayloadSchema.safeParse(req.body);
525
+ if (!regParsed.success) {
526
+ res.status(400).json({
527
+ error: "Invalid registration payload",
528
+ issues: regParsed.error.issues,
529
+ });
530
+ return;
531
+ }
532
+ const regPayload = regParsed.data;
330
533
  if (!usernameRegex.test(regPayload.username)) {
331
534
  res.status(400).send({
332
535
  error: "Username must be between three and nineteen letters, digits, or underscores.",
333
536
  });
334
537
  return;
335
538
  }
336
- const regKey = tweetnacl_1.default.sign.open(crypto_1.XUtils.decodeHex(regPayload.signed), crypto_1.XUtils.decodeHex(regPayload.signKey));
539
+ const regKey = xSignOpen(XUtils.decodeHex(regPayload.signed), XUtils.decodeHex(regPayload.signKey));
337
540
  if (regKey &&
338
- this.validateToken(uuid.stringify(regKey), TokenScopes.Register)) {
339
- const [user, err] = yield this.db.createUser(regKey, regPayload);
541
+ this.validateToken(uuidStringify(regKey), TokenScopes.Register)) {
542
+ const [user, err] = await this.db.createUser(regKey, regPayload);
340
543
  if (err !== null) {
341
- switch (err.code) {
544
+ const errCode = "code" in err && typeof err.code === "string"
545
+ ? err.code
546
+ : undefined;
547
+ switch (errCode) {
342
548
  case "ER_DUP_ENTRY":
343
- const usernameConflict = err
344
- .toString()
345
- .includes("users_username_unique");
346
- const signKeyConflict = err
347
- .toString()
348
- .includes("users_signkey_unique");
549
+ const usernameConflict = String(err).includes("users_username_unique");
550
+ const signKeyConflict = String(err).includes("users_signkey_unique");
349
551
  this.log.warn("User attempted to register duplicate account.");
350
552
  if (usernameConflict) {
351
553
  res.status(400).send({
@@ -365,15 +567,19 @@ class Spire extends events_1.EventEmitter {
365
567
  break;
366
568
  default:
367
569
  this.log.info("Unsupported sql error type: " +
368
- err.code);
369
- this.log.error(err);
570
+ String(errCode));
571
+ this.log.error(String(err));
370
572
  res.sendStatus(500);
371
573
  break;
372
574
  }
373
575
  }
374
576
  else {
375
577
  this.log.info("Registration success.");
376
- res.send(msgpack_lite_1.default.encode(utils_1.censorUser(user)));
578
+ if (!user) {
579
+ res.sendStatus(500);
580
+ return;
581
+ }
582
+ res.send(msgpack.encode(censorUser(user)));
377
583
  }
378
584
  }
379
585
  else {
@@ -383,13 +589,67 @@ class Spire extends events_1.EventEmitter {
383
589
  }
384
590
  }
385
591
  catch (err) {
386
- this.log.error("error registering user: " + err.toString());
592
+ this.log.error("error registering user: " + String(err));
387
593
  res.sendStatus(500);
388
594
  }
389
- }));
595
+ });
390
596
  this.server = this.api.listen(apiPort, () => {
391
- this.log.info("API started on port " + apiPort.toString());
597
+ this.log.info("API started on port " + String(apiPort));
392
598
  });
599
+ // Accept all WS upgrades — auth happens post-connection.
600
+ this.server.on("upgrade", (req, socket, head) => {
601
+ this.wss.handleUpgrade(req, socket, head, (ws) => {
602
+ this.wss.emit("connection", ws);
603
+ });
604
+ });
605
+ }
606
+ notify(userID, event, transmissionID, data, deviceID) {
607
+ for (const client of this.clients) {
608
+ if (deviceID) {
609
+ if (client.getDevice().deviceID === deviceID) {
610
+ const msg = {
611
+ data,
612
+ event,
613
+ transmissionID,
614
+ type: "notify",
615
+ };
616
+ client.send(msg);
617
+ }
618
+ }
619
+ else {
620
+ if (client.getUser().userID === userID) {
621
+ const msg = {
622
+ data,
623
+ event,
624
+ transmissionID,
625
+ type: "notify",
626
+ };
627
+ client.send(msg);
628
+ }
629
+ }
630
+ }
631
+ }
632
+ validateToken(key, scope) {
633
+ this.log.info("Validating token: " + key);
634
+ for (const rKey of this.actionTokens) {
635
+ if (rKey.key === key) {
636
+ if (rKey.scope !== scope) {
637
+ continue;
638
+ }
639
+ const age = Date.now() - new Date(rKey.time).getTime();
640
+ this.log.info("Token found, " + String(age) + " ms old.");
641
+ if (age < TOKEN_EXPIRY) {
642
+ this.log.info("Token is valid.");
643
+ this.deleteActionToken(rKey);
644
+ return true;
645
+ }
646
+ else {
647
+ this.log.info("Token is expired.");
648
+ }
649
+ }
650
+ }
651
+ this.log.info("Token not found.");
652
+ return false;
393
653
  }
394
654
  }
395
- exports.Spire = Spire;
655
+ //# sourceMappingURL=Spire.js.map