@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
package/dist/Spire.js CHANGED
@@ -1,128 +1,157 @@
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, hashPasswordArgon2, verifyPassword } 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 { getJwtSecret } from "./utils/jwtSecret.js";
20
+ import { msgpack } from "./utils/msgpack.js";
50
21
  // expiry of regkeys = 24hr
51
- exports.TOKEN_EXPIRY = 1000 * 60 * 10;
52
- exports.JWT_EXPIRY = "7d";
22
+ export const TOKEN_EXPIRY = 1000 * 60 * 10;
23
+ export const JWT_EXPIRY = "7d";
24
+ export const DEVICE_AUTH_JWT_EXPIRY = "1h";
25
+ const DEVICE_CHALLENGE_EXPIRY = 1000 * 60; // 60 seconds
26
+ const STATUS_LATENCY_BUDGET_MS = 250;
53
27
  // 3-19 chars long
54
28
  const usernameRegex = /^(\w{3,19})$/;
29
+ // ── Zod schemas for trust-boundary validation ──────────────────────────
30
+ const wsAuthMsg = z.object({
31
+ token: z.string().min(1),
32
+ type: z.literal("auth"),
33
+ });
34
+ const jwtPayload = z.object({
35
+ bearerToken: z.string().optional(),
36
+ exp: z.number().optional(),
37
+ user: UserSchema,
38
+ });
39
+ const authPayload = z.object({
40
+ password: z.string().min(1),
41
+ username: z.string().min(1),
42
+ });
43
+ const deviceAuthPayload = z.object({
44
+ deviceID: z.string().min(1),
45
+ signKey: z.string().min(1),
46
+ });
47
+ const deviceVerifyPayload = z.object({
48
+ challengeID: z.string().min(1),
49
+ signed: z.string().min(1),
50
+ });
51
+ const mailPostPayload = z.object({
52
+ header: z.custom((val) => val instanceof Uint8Array),
53
+ mail: MailWSSchema,
54
+ });
55
55
  const directories = ["files", "avatars", "emoji"];
56
56
  for (const dir of directories) {
57
- if (!fs_1.default.existsSync(dir)) {
58
- fs_1.default.mkdirSync(dir);
57
+ if (!fs.existsSync(dir)) {
58
+ fs.mkdirSync(dir);
59
59
  }
60
60
  }
61
- const TokenScopes = types_1.XTypes.HTTP.TokenScopes;
62
- class Spire extends events_1.EventEmitter {
61
+ const getAppVersion = () => {
62
+ try {
63
+ const raw = fs.readFileSync(new URL("../package.json", import.meta.url), {
64
+ encoding: "utf8",
65
+ });
66
+ const pkg = JSON.parse(raw);
67
+ if (typeof pkg === "object" &&
68
+ pkg !== null &&
69
+ "version" in pkg &&
70
+ typeof pkg.version === "string") {
71
+ return pkg.version;
72
+ }
73
+ return "unknown";
74
+ }
75
+ catch {
76
+ return "unknown";
77
+ }
78
+ };
79
+ const getCommitSha = () => {
80
+ const sourceDir = path.dirname(fileURLToPath(import.meta.url));
81
+ const repoRoot = path.resolve(sourceDir, "..");
82
+ try {
83
+ const sha = execSync("git rev-parse --verify --short=12 HEAD", {
84
+ cwd: repoRoot,
85
+ encoding: "utf8",
86
+ stdio: ["ignore", "pipe", "ignore"],
87
+ timeout: 1500,
88
+ }).trim();
89
+ return sha || "unknown";
90
+ }
91
+ catch {
92
+ return "unknown";
93
+ }
94
+ };
95
+ export class Spire extends EventEmitter {
96
+ actionTokens = [];
97
+ api = express();
98
+ clients = [];
99
+ commitSha = getCommitSha();
100
+ db;
101
+ dbReady = false;
102
+ deviceChallenges = new Map();
103
+ queuedRequestIncrements = 0;
104
+ requestsTotal = 0;
105
+ requestsTotalLoaded = false;
106
+ server = null;
107
+ signKeys;
108
+ startedAt = new Date();
109
+ version = getAppVersion();
110
+ wss = new WebSocketServer({
111
+ maxPayload: 4096,
112
+ noServer: true,
113
+ });
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);
75
- this.options = options;
76
- }
77
- close() {
78
- var _a, _b;
79
- return __awaiter(this, void 0, void 0, function* () {
80
- this.wss.clients.forEach((ws) => {
81
- ws.terminate();
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
+ // debugger: bootstrap request counter failed
82
128
  });
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;
93
129
  });
130
+ this.init(options?.apiPort || 16777);
94
131
  }
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
- }
132
+ async close() {
133
+ this.wss.clients.forEach((ws) => {
134
+ ws.terminate();
135
+ });
136
+ this.server?.close();
137
+ this.wss.close();
138
+ await this.db.close();
139
+ }
140
+ async bootstrapRequestCounter() {
141
+ const persistedTotal = await this.db.getRequestsTotal();
142
+ const startupIncrements = this.queuedRequestIncrements;
143
+ this.queuedRequestIncrements = 0;
144
+ this.requestsTotal = persistedTotal + startupIncrements;
145
+ if (startupIncrements > 0) {
146
+ await this.db.incrementRequestsTotal(startupIncrements);
119
147
  }
148
+ this.requestsTotalLoaded = true;
120
149
  }
121
150
  createActionToken(scope) {
122
151
  const token = {
123
- key: uuid.v4(),
124
- time: new Date(Date.now()),
152
+ key: crypto.randomUUID(),
125
153
  scope,
154
+ time: new Date().toISOString(),
126
155
  };
127
156
  this.actionTokens.push(token);
128
157
  return token;
@@ -132,69 +161,82 @@ class Spire extends events_1.EventEmitter {
132
161
  this.actionTokens.splice(this.actionTokens.indexOf(key), 1);
133
162
  }
134
163
  }
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
164
  init(apiPort) {
165
+ this.api.use((_req, _res, next) => {
166
+ this.requestsTotal += 1;
167
+ if (!this.requestsTotalLoaded) {
168
+ this.queuedRequestIncrements += 1;
169
+ }
170
+ else {
171
+ this.db.incrementRequestsTotal(1).catch((_err) => {
172
+ // debugger: failed to persist request counter
173
+ });
174
+ }
175
+ next();
176
+ });
158
177
  // 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);
178
+ initApp(this.api, this.db, this.validateToken.bind(this), this.signKeys, this.notify.bind(this));
179
+ // WS auth: client sends { type: "auth", token } as first message
180
+ this.wss.on("connection", (ws) => {
181
+ const AUTH_TIMEOUT = 10_000;
182
+ const timer = setTimeout(() => {
171
183
  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);
184
+ }, AUTH_TIMEOUT);
185
+ const onFirstMessage = (data) => {
186
+ const str = Buffer.isBuffer(data)
187
+ ? data.toString()
188
+ : data instanceof ArrayBuffer
189
+ ? Buffer.from(data).toString()
190
+ : Buffer.concat(data).toString();
191
+ clearTimeout(timer);
192
+ ws.off("message", onFirstMessage);
193
+ try {
194
+ const rawParsed = JSON.parse(str);
195
+ const authResult = wsAuthMsg.safeParse(rawParsed);
196
+ if (!authResult.success) {
197
+ throw new Error("Expected { type: 'auth', token }, got: " +
198
+ JSON.stringify(authResult.error.issues));
199
+ }
200
+ const result = jwt.verify(authResult.data.token, getJwtSecret());
201
+ const jwtResult = jwtPayload.safeParse(result);
202
+ if (!jwtResult.success) {
203
+ throw new Error("Invalid JWT payload: " +
204
+ JSON.stringify(jwtResult.error.issues));
205
+ }
206
+ const userDetails = jwtResult.data.user;
207
+ const client = new ClientManager(ws, this.db, this.notify.bind(this), userDetails);
208
+ client.on("fail", () => {
209
+ if (this.clients.includes(client)) {
210
+ this.clients.splice(this.clients.indexOf(client), 1);
211
+ }
212
+ });
213
+ client.on("authed", () => {
214
+ this.clients.push(client);
215
+ });
181
216
  }
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);
217
+ catch (_err) {
218
+ // debugger: WS auth failed
219
+ const errMsg = {
220
+ transmissionID: crypto.randomUUID(),
221
+ type: "unauthorized",
222
+ };
223
+ ws.send(XUtils.packMessage(errMsg));
224
+ ws.close();
225
+ }
226
+ };
227
+ ws.on("message", onFirstMessage);
228
+ ws.on("close", () => {
229
+ clearTimeout(timer);
188
230
  });
189
231
  });
190
232
  this.api.get("/token/:tokenType", (req, res, next) => {
191
- if (req.params.tokenType !== "register") {
192
- server_1.protect(req, res, next);
233
+ if (getParam(req, "tokenType") !== "register") {
234
+ protect(req, res, next);
193
235
  }
194
236
  else {
195
237
  next();
196
238
  }
197
- }, (req, res) => __awaiter(this, void 0, void 0, function* () {
239
+ }, (req, res) => {
198
240
  const allowedTokens = [
199
241
  "file",
200
242
  "register",
@@ -204,149 +246,284 @@ class Spire extends events_1.EventEmitter {
204
246
  "emoji",
205
247
  "connect",
206
248
  ];
207
- const { tokenType } = req.params;
249
+ const tokenType = getParam(req, "tokenType");
208
250
  if (!allowedTokens.includes(tokenType)) {
209
251
  res.sendStatus(400);
210
252
  return;
211
253
  }
212
254
  let scope;
213
255
  switch (tokenType) {
214
- case "file":
215
- scope = TokenScopes.File;
216
- break;
217
- case "register":
218
- scope = TokenScopes.Register;
219
- break;
220
256
  case "avatar":
221
257
  scope = TokenScopes.Avatar;
222
258
  break;
259
+ case "connect":
260
+ scope = TokenScopes.Connect;
261
+ break;
223
262
  case "device":
224
263
  scope = TokenScopes.Device;
225
264
  break;
226
- case "invite":
227
- scope = TokenScopes.Invite;
228
- break;
229
265
  case "emoji":
230
266
  scope = TokenScopes.Emoji;
231
267
  break;
232
- case "connect":
233
- scope = TokenScopes.Connect;
268
+ case "file":
269
+ scope = TokenScopes.File;
270
+ break;
271
+ case "invite":
272
+ scope = TokenScopes.Invite;
273
+ break;
274
+ case "register":
275
+ scope = TokenScopes.Register;
234
276
  break;
235
277
  default:
236
278
  res.sendStatus(400);
237
279
  return;
238
280
  }
239
281
  try {
240
- this.log.info("New token requested of type " + tokenType);
241
282
  const token = this.createActionToken(scope);
242
- this.log.info("New token created: " + token.key);
243
283
  setTimeout(() => {
244
284
  this.deleteActionToken(token);
245
- }, exports.TOKEN_EXPIRY);
246
- return res.send(msgpack_lite_1.default.encode(token));
285
+ }, TOKEN_EXPIRY);
286
+ const acceptHeader = req.get("accept")?.toLowerCase() || "";
287
+ const wantsJson = acceptHeader.includes("application/json") &&
288
+ !acceptHeader.includes("application/msgpack") &&
289
+ !acceptHeader.includes("*/*");
290
+ if (wantsJson) {
291
+ return res.json(token);
292
+ }
293
+ res.set("Content-Type", "application/msgpack");
294
+ return res.send(msgpack.encode(token));
247
295
  }
248
- catch (err) {
249
- console.error(err.toString());
296
+ catch (_err) {
297
+ // debugger: token creation failed
250
298
  return res.sendStatus(500);
251
299
  }
252
- }));
253
- this.api.post("/whoami", (req, res) => __awaiter(this, void 0, void 0, function* () {
300
+ });
301
+ this.api.post("/whoami", (req, res) => {
254
302
  if (!req.user) {
255
303
  res.sendStatus(401);
256
304
  return;
257
305
  }
258
- res.send(msgpack_lite_1.default.encode({
259
- user: req.user,
306
+ res.send(msgpack.encode({
260
307
  exp: req.exp,
261
- token: req.cookies.auth,
308
+ user: req.user,
262
309
  }));
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: "/" });
310
+ });
311
+ this.api.get("/healthz", (_req, res) => {
312
+ if (!this.dbReady) {
313
+ res.status(503).json({ dbReady: false, ok: false });
314
+ return;
315
+ }
316
+ res.json({ dbReady: true, ok: true });
317
+ });
318
+ this.api.get("/status", async (_req, res) => {
319
+ const started = Date.now();
320
+ const dbHealthy = this.dbReady ? await this.db.isHealthy() : false;
321
+ const checkDurationMs = Date.now() - started;
322
+ const ok = dbHealthy;
323
+ const canaryEnv = process.env["CANARY"]?.trim().toLowerCase();
324
+ res.json({
325
+ canary: canaryEnv === "1" ||
326
+ canaryEnv === "true" ||
327
+ canaryEnv === "yes",
328
+ checkDurationMs,
329
+ commitSha: this.commitSha,
330
+ dbHealthy,
331
+ dbReady: this.dbReady,
332
+ latencyBudgetMs: STATUS_LATENCY_BUDGET_MS,
333
+ metrics: {
334
+ requestsTotal: this.requestsTotal,
335
+ },
336
+ now: new Date(),
337
+ ok,
338
+ startedAt: this.startedAt.toISOString(),
339
+ uptimeSeconds: Math.floor(process.uptime()),
340
+ version: this.version,
341
+ withinLatencyBudget: checkDurationMs <= STATUS_LATENCY_BUDGET_MS,
342
+ });
343
+ });
344
+ this.api.post("/goodbye", protect, (req, res) => {
345
+ jwt.sign({ user: req.user }, getJwtSecret(), { expiresIn: -1 });
267
346
  res.sendStatus(200);
268
- }));
269
- this.api.post("/mail", server_1.protect, (req, res) => __awaiter(this, void 0, void 0, function* () {
347
+ });
348
+ // ── Device-key auth ──────────────────────────────────────────
349
+ this.api.post("/auth/device", authLimiter, async (req, res) => {
350
+ try {
351
+ const parsed = deviceAuthPayload.safeParse(req.body);
352
+ if (!parsed.success) {
353
+ return res
354
+ .status(400)
355
+ .send({ error: "deviceID and signKey required." });
356
+ }
357
+ const { deviceID, signKey } = parsed.data;
358
+ const device = await this.db.retrieveDevice(deviceID);
359
+ if (!device || device.signKey !== signKey) {
360
+ return res.status(404).send({ error: "Device not found." });
361
+ }
362
+ // Generate challenge nonce (32 bytes)
363
+ const nonce = XUtils.encodeHex(xRandomBytes(32));
364
+ const challengeID = crypto.randomUUID();
365
+ this.deviceChallenges.set(challengeID, {
366
+ deviceID,
367
+ nonce,
368
+ time: Date.now(),
369
+ });
370
+ // Clean up expired challenges
371
+ setTimeout(() => {
372
+ this.deviceChallenges.delete(challengeID);
373
+ }, DEVICE_CHALLENGE_EXPIRY);
374
+ return res.send(msgpack.encode({ challenge: nonce, challengeID }));
375
+ }
376
+ catch (_err) {
377
+ // debugger: device challenge error
378
+ return res.sendStatus(500);
379
+ }
380
+ });
381
+ this.api.post("/auth/device/verify", authLimiter, async (req, res) => {
382
+ try {
383
+ const parsed = deviceVerifyPayload.safeParse(req.body);
384
+ if (!parsed.success) {
385
+ return res.status(400).send({
386
+ error: "challengeID and signed required.",
387
+ });
388
+ }
389
+ const { challengeID, signed } = parsed.data;
390
+ const challenge = this.deviceChallenges.get(challengeID);
391
+ if (!challenge) {
392
+ return res.status(401).send({
393
+ error: "Challenge expired or not found.",
394
+ });
395
+ }
396
+ // Consume the challenge (single-use)
397
+ this.deviceChallenges.delete(challengeID);
398
+ // Check expiry
399
+ if (Date.now() - challenge.time > DEVICE_CHALLENGE_EXPIRY) {
400
+ return res
401
+ .status(401)
402
+ .send({ error: "Challenge expired." });
403
+ }
404
+ // Look up the device to get its public signKey
405
+ const device = await this.db.retrieveDevice(challenge.deviceID);
406
+ if (!device) {
407
+ return res.status(404).send({ error: "Device not found." });
408
+ }
409
+ // Verify the Ed25519 signature
410
+ const opened = xSignOpen(XUtils.decodeHex(signed), XUtils.decodeHex(device.signKey));
411
+ if (!opened) {
412
+ return res
413
+ .status(401)
414
+ .send({ error: "Signature verification failed." });
415
+ }
416
+ // Verify the signed content matches the challenge nonce
417
+ const signedNonce = XUtils.encodeHex(opened);
418
+ if (signedNonce !== challenge.nonce) {
419
+ return res
420
+ .status(401)
421
+ .send({ error: "Challenge mismatch." });
422
+ }
423
+ // Look up device owner
424
+ const user = await this.db.retrieveUser(device.owner);
425
+ if (!user) {
426
+ return res
427
+ .status(404)
428
+ .send({ error: "Device owner not found." });
429
+ }
430
+ // Issue short-lived JWT (1 hour, not 7 days)
431
+ const token = jwt.sign({ user: censorUser(user) }, getJwtSecret(), { expiresIn: DEVICE_AUTH_JWT_EXPIRY });
432
+ return res.send(msgpack.encode({ token, user: censorUser(user) }));
433
+ }
434
+ catch (_err) {
435
+ // debugger: device verify error
436
+ return res.sendStatus(500);
437
+ }
438
+ });
439
+ this.api.post("/mail", protect, async (req, res) => {
270
440
  const senderDeviceDetails = req.device;
271
441
  if (!senderDeviceDetails) {
272
442
  res.sendStatus(401);
273
443
  return;
274
444
  }
275
- const authorUserDetails = req.user;
276
- const { header, mail, } = req.body;
277
- 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;
284
- }
285
- res.sendStatus(200);
286
- this.notify(recipientDeviceDetails.owner, "mail", uuid.v4(), null, mail.recipient);
287
- }
288
- catch (err) {
289
- this.log.error(err);
290
- res.status(500).send(err.toString());
445
+ const authorUserDetails = getUser(req);
446
+ const parsed = mailPostPayload.safeParse(req.body);
447
+ if (!parsed.success) {
448
+ res.status(400).json({
449
+ error: "Invalid mail payload",
450
+ issues: parsed.error.issues,
451
+ });
452
+ return;
291
453
  }
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.");
454
+ const { header, mail } = parsed.data;
455
+ await this.db.saveMail(mail, header, senderDeviceDetails.deviceID, authorUserDetails.userID);
456
+ const recipientDeviceDetails = await this.db.retrieveDevice(mail.recipient);
457
+ if (!recipientDeviceDetails) {
458
+ res.sendStatus(400);
297
459
  return;
298
460
  }
299
- if (typeof credentials.username !== "string") {
300
- res.status(400).send("Username is required and must be a string.");
461
+ res.sendStatus(200);
462
+ this.notify(recipientDeviceDetails.owner, "mail", crypto.randomUUID(), null, mail.recipient);
463
+ });
464
+ this.api.post("/auth", authLimiter, async (req, res) => {
465
+ const parsed = authPayload.safeParse(req.body);
466
+ if (!parsed.success) {
467
+ res.status(400).json({
468
+ error: "Invalid credentials format",
469
+ });
301
470
  return;
302
471
  }
472
+ const { password, username } = parsed.data;
303
473
  try {
304
- const userEntry = yield this.db.retrieveUser(credentials.username);
474
+ const userEntry = await this.db.retrieveUser(username);
305
475
  if (!userEntry) {
306
- res.sendStatus(404);
307
- this.log.warn("User does not exist.");
476
+ res.sendStatus(401);
308
477
  return;
309
478
  }
310
- const salt = crypto_1.XUtils.decodeHex(userEntry.passwordSalt);
311
- const payloadHash = crypto_1.XUtils.encodeHex(Database_1.hashPassword(credentials.password, salt));
312
- if (payloadHash !== userEntry.passwordHash) {
479
+ const { needsRehash, valid } = await verifyPassword(password, userEntry);
480
+ if (!valid) {
313
481
  res.sendStatus(401);
314
482
  return;
315
483
  }
316
- const token = jsonwebtoken_1.default.sign({ user: utils_1.censorUser(userEntry) }, process.env.SPK, { expiresIn: exports.JWT_EXPIRY });
484
+ if (needsRehash) {
485
+ const newHash = await hashPasswordArgon2(password);
486
+ await this.db.rehashPassword(userEntry.userID, newHash);
487
+ }
488
+ const token = jwt.sign({ user: censorUser(userEntry) }, getJwtSecret(), { expiresIn: JWT_EXPIRY });
317
489
  // 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 }));
490
+ jwt.verify(token, getJwtSecret());
491
+ res.send(msgpack.encode({ token, user: censorUser(userEntry) }));
321
492
  }
322
- catch (err) {
323
- this.log.error(err.toString());
493
+ catch (_err) {
494
+ // debugger: auth error
324
495
  res.sendStatus(500);
325
496
  }
326
- }));
327
- this.api.post("/register", (req, res) => __awaiter(this, void 0, void 0, function* () {
497
+ });
498
+ this.api.post("/register", authLimiter, async (req, res) => {
328
499
  try {
329
- const regPayload = req.body;
500
+ const regParsed = RegistrationPayloadSchema.safeParse(req.body);
501
+ if (!regParsed.success) {
502
+ res.status(400).json({
503
+ error: "Invalid registration payload",
504
+ issues: regParsed.error.issues,
505
+ });
506
+ return;
507
+ }
508
+ const regPayload = regParsed.data;
330
509
  if (!usernameRegex.test(regPayload.username)) {
331
510
  res.status(400).send({
332
511
  error: "Username must be between three and nineteen letters, digits, or underscores.",
333
512
  });
334
513
  return;
335
514
  }
336
- const regKey = tweetnacl_1.default.sign.open(crypto_1.XUtils.decodeHex(regPayload.signed), crypto_1.XUtils.decodeHex(regPayload.signKey));
515
+ const regKey = xSignOpen(XUtils.decodeHex(regPayload.signed), XUtils.decodeHex(regPayload.signKey));
337
516
  if (regKey &&
338
- this.validateToken(uuid.stringify(regKey), TokenScopes.Register)) {
339
- const [user, err] = yield this.db.createUser(regKey, regPayload);
517
+ this.validateToken(uuidStringify(regKey), TokenScopes.Register)) {
518
+ const [user, err] = await this.db.createUser(regKey, regPayload);
340
519
  if (err !== null) {
341
- switch (err.code) {
520
+ const errCode = "code" in err && typeof err.code === "string"
521
+ ? err.code
522
+ : undefined;
523
+ switch (errCode) {
342
524
  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");
349
- this.log.warn("User attempted to register duplicate account.");
525
+ const usernameConflict = String(err).includes("users_username_unique");
526
+ const signKeyConflict = String(err).includes("users_signkey_unique");
350
527
  if (usernameConflict) {
351
528
  res.status(400).send({
352
529
  error: "Username is already registered.",
@@ -364,16 +541,16 @@ class Spire extends events_1.EventEmitter {
364
541
  });
365
542
  break;
366
543
  default:
367
- this.log.info("Unsupported sql error type: " +
368
- err.code);
369
- this.log.error(err);
370
544
  res.sendStatus(500);
371
545
  break;
372
546
  }
373
547
  }
374
548
  else {
375
- this.log.info("Registration success.");
376
- res.send(msgpack_lite_1.default.encode(utils_1.censorUser(user)));
549
+ if (!user) {
550
+ res.sendStatus(500);
551
+ return;
552
+ }
553
+ res.send(msgpack.encode(censorUser(user)));
377
554
  }
378
555
  }
379
556
  else {
@@ -382,14 +559,59 @@ class Spire extends events_1.EventEmitter {
382
559
  });
383
560
  }
384
561
  }
385
- catch (err) {
386
- this.log.error("error registering user: " + err.toString());
562
+ catch (_err) {
563
+ // debugger: registration error
387
564
  res.sendStatus(500);
388
565
  }
389
- }));
390
- this.server = this.api.listen(apiPort, () => {
391
- this.log.info("API started on port " + apiPort.toString());
392
566
  });
567
+ this.server = this.api.listen(apiPort);
568
+ // Accept all WS upgrades — auth happens post-connection.
569
+ this.server.on("upgrade", (req, socket, head) => {
570
+ this.wss.handleUpgrade(req, socket, head, (ws) => {
571
+ this.wss.emit("connection", ws);
572
+ });
573
+ });
574
+ }
575
+ notify(userID, event, transmissionID, data, deviceID) {
576
+ for (const client of this.clients) {
577
+ if (deviceID) {
578
+ if (client.getDevice().deviceID === deviceID) {
579
+ const msg = {
580
+ data,
581
+ event,
582
+ transmissionID,
583
+ type: "notify",
584
+ };
585
+ client.send(msg);
586
+ }
587
+ }
588
+ else {
589
+ if (client.getUser().userID === userID) {
590
+ const msg = {
591
+ data,
592
+ event,
593
+ transmissionID,
594
+ type: "notify",
595
+ };
596
+ client.send(msg);
597
+ }
598
+ }
599
+ }
600
+ }
601
+ validateToken(key, scope) {
602
+ for (const rKey of this.actionTokens) {
603
+ if (rKey.key === key) {
604
+ if (rKey.scope !== scope) {
605
+ continue;
606
+ }
607
+ const age = Date.now() - new Date(rKey.time).getTime();
608
+ if (age < TOKEN_EXPIRY) {
609
+ this.deleteActionToken(rKey);
610
+ return true;
611
+ }
612
+ }
613
+ }
614
+ return false;
393
615
  }
394
616
  }
395
- exports.Spire = Spire;
617
+ //# sourceMappingURL=Spire.js.map