@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.
- package/README.md +82 -26
- package/dist/ClientManager.d.ts +23 -25
- package/dist/ClientManager.js +230 -249
- package/dist/ClientManager.js.map +1 -0
- package/dist/Database.d.ts +49 -47
- package/dist/Database.js +698 -773
- package/dist/Database.js.map +1 -0
- package/dist/Spire.d.ts +22 -14
- package/dist/Spire.js +496 -236
- package/dist/Spire.js.map +1 -0
- package/dist/__tests__/Database.spec.js +116 -75
- package/dist/__tests__/Database.spec.js.map +1 -0
- package/dist/db/schema.d.ts +134 -0
- package/dist/db/schema.js +2 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -5
- package/dist/index.js.map +1 -0
- package/dist/middleware/validate.d.ts +12 -0
- package/dist/middleware/validate.js +35 -0
- package/dist/middleware/validate.js.map +1 -0
- package/dist/migrations/2026-04-06_initial-schema.d.ts +3 -0
- package/dist/migrations/2026-04-06_initial-schema.js +192 -0
- package/dist/migrations/2026-04-06_initial-schema.js.map +1 -0
- package/dist/run.js +26 -21
- package/dist/run.js.map +1 -0
- package/dist/server/avatar.d.ts +3 -4
- package/dist/server/avatar.js +64 -64
- package/dist/server/avatar.js.map +1 -0
- package/dist/server/errors.d.ts +59 -0
- package/dist/server/errors.js +94 -0
- package/dist/server/errors.js.map +1 -0
- package/dist/server/file.d.ts +3 -4
- package/dist/server/file.js +81 -62
- package/dist/server/file.js.map +1 -0
- package/dist/server/index.d.ts +8 -10
- package/dist/server/index.js +414 -405
- package/dist/server/index.js.map +1 -0
- package/dist/server/invite.d.ts +4 -5
- package/dist/server/invite.js +18 -52
- package/dist/server/invite.js.map +1 -0
- package/dist/server/openapi.d.ts +2 -0
- package/dist/server/openapi.js +40 -0
- package/dist/server/openapi.js.map +1 -0
- package/dist/server/permissions.d.ts +16 -0
- package/dist/server/permissions.js +22 -0
- package/dist/server/permissions.js.map +1 -0
- package/dist/server/rateLimit.d.ts +28 -0
- package/dist/server/rateLimit.js +58 -0
- package/dist/server/rateLimit.js.map +1 -0
- package/dist/server/user.d.ts +4 -7
- package/dist/server/user.js +55 -66
- package/dist/server/user.js.map +1 -0
- package/dist/server/utils.d.ts +35 -7
- package/dist/server/utils.js +50 -6
- package/dist/server/utils.js.map +1 -0
- package/dist/types/express.d.ts +20 -0
- package/dist/types/express.js +2 -0
- package/dist/types/express.js.map +1 -0
- package/dist/utils/createLogger.js +13 -19
- package/dist/utils/createLogger.js.map +1 -0
- package/dist/utils/createUint8UUID.js +6 -10
- package/dist/utils/createUint8UUID.js.map +1 -0
- package/dist/utils/jwtSecret.d.ts +7 -0
- package/dist/utils/jwtSecret.js +15 -0
- package/dist/utils/jwtSecret.js.map +1 -0
- package/dist/utils/loadEnv.js +7 -22
- package/dist/utils/loadEnv.js.map +1 -0
- package/dist/utils/msgpack.d.ts +2 -0
- package/dist/utils/msgpack.js +4 -0
- package/dist/utils/msgpack.js.map +1 -0
- package/package.json +91 -65
- package/src/ClientManager.ts +434 -0
- package/src/Database.ts +925 -0
- package/src/Spire.ts +878 -0
- package/src/__tests__/Database.spec.ts +167 -0
- package/src/ambient-modules.d.ts +1 -0
- package/src/db/schema.ts +165 -0
- package/src/index.ts +3 -0
- package/src/middleware/validate.ts +38 -0
- package/src/migrations/2026-04-06_initial-schema.ts +218 -0
- package/src/run.ts +37 -0
- package/src/server/avatar.ts +141 -0
- package/src/server/errors.ts +133 -0
- package/src/server/file.ts +172 -0
- package/src/server/index.ts +855 -0
- package/src/server/invite.ts +65 -0
- package/src/server/openapi.ts +51 -0
- package/src/server/permissions.ts +40 -0
- package/src/server/rateLimit.ts +86 -0
- package/src/server/user.ts +125 -0
- package/src/server/utils.ts +59 -0
- package/src/types/express.ts +23 -0
- package/src/utils/createLogger.ts +47 -0
- package/src/utils/createUint8UUID.ts +9 -0
- package/src/utils/jwtSecret.ts +16 -0
- package/src/utils/loadEnv.ts +15 -0
- package/src/utils/msgpack.ts +4 -0
- package/avatars/052242d0-4129-4a6e-8076-22709c157549 +0 -0
- package/avatars/0a677c9a-4986-4b2c-ae2e-12faf22f55db +0 -0
- package/avatars/0ba21b91-decb-4a3e-ac86-4dd54d805a9a +0 -0
- package/avatars/0c48d8b6-1d1b-4297-a6c6-2fe50af6fc35 +0 -0
- package/avatars/0d993cdf-19a6-4299-a4ee-a06579d106cf +0 -0
- package/avatars/17b000e4-ac38-46ec-9dec-d2568086129a +0 -0
- package/avatars/19dc5594-0f06-4ac6-af18-8740dd39ef6b +0 -0
- package/avatars/20444fa3-6d5e-429e-b55f-b81c3d2c61ee +0 -0
- package/avatars/21c0512a-5630-4931-9442-d66db66737be +0 -0
- package/avatars/22830a60-0b6f-4912-83a5-72245465f332 +0 -0
- package/avatars/243639ce-f59f-4404-a1f1-4ec0eb5d2af3 +0 -0
- package/avatars/30d2c01d-7b7f-4ea9-9859-1c90837a23f7 +0 -0
- package/avatars/315a04f0-9a6f-4b0f-bb9f-5fa774c4752b +0 -0
- package/avatars/3563d333-53fe-4885-ac2d-9a4f761db85e +0 -0
- package/avatars/36a10c00-3b4c-437f-8e1f-4428ecde0003 +0 -0
- package/avatars/40b83eeb-c6e8-4268-82ab-69799a796405 +0 -0
- package/avatars/45b5ddb9-ad2c-4404-8ab3-cb4699e6d61c +0 -0
- package/avatars/4e4f0ffb-9a75-479a-bccb-446d0bf85020 +0 -0
- package/avatars/4e62c3bd-08c6-4fdd-bd65-f01c7322ed64 +0 -0
- package/avatars/5004d2e7-51af-44af-8776-6c71f7019843 +0 -0
- package/avatars/5041eb29-5c4b-4dea-8c1b-31ba4473161a +0 -0
- package/avatars/5065cf78-31c5-46cb-8d5c-c0b6be2d994e +0 -0
- package/avatars/51b91d2c-8956-4d73-b4ad-ca6a8d9da9a8 +0 -0
- package/avatars/58264a2c-5651-4a42-8ca2-a9907b311e48 +0 -0
- package/avatars/58c2357c-8080-4725-a0ce-182c96b037c4 +0 -0
- package/avatars/59b5f6dd-8e04-4d15-b4dc-c1c652558a74 +0 -0
- package/avatars/5b417a78-b274-48bc-98a4-6e54b74ee62d +0 -0
- package/avatars/611f5a93-1ed4-45a1-bc8e-e8e413f9b171 +0 -0
- package/avatars/65abe919-9921-46f6-9bc9-183e9cc53c8a +0 -0
- package/avatars/6934202d-1546-4270-8a15-97ba8b8c6fa9 +0 -0
- package/avatars/6acd24be-f4b7-4399-9e7c-807821828d29 +0 -0
- package/avatars/6b2d6ac5-e35c-4297-994e-f0eb6fe56740 +0 -0
- package/avatars/6cae9ddd-f163-44e7-a632-30425716a159 +0 -0
- package/avatars/6d90e79e-b9c1-4b89-843b-96636de8d26b +0 -0
- package/avatars/6f82c7bb-a974-4372-8a64-ce287e668c8c +0 -0
- package/avatars/74a45091-5a76-4bb7-ae8a-cd7373adc128 +0 -0
- package/avatars/7d071ab2-e0b5-4dbb-8bf5-1fae50c3663f +0 -0
- package/avatars/7de818c8-bda1-4f51-976e-160fc087184b +0 -0
- package/avatars/873937df-8cb1-427a-92bf-f829f4259624 +0 -0
- package/avatars/8b45ffa5-3322-4109-bfa0-1be088336135 +0 -0
- package/avatars/8efaa426-22a9-42a2-b4ac-a275717b812f +0 -0
- package/avatars/903fd1a6-d6ea-431c-b98f-f21e424a2852 +0 -0
- package/avatars/943d8533-5174-4199-990f-1ec69e5d60c4 +0 -0
- package/avatars/952d014e-3804-4cd2-a4a0-ffe40a11e4ac +0 -0
- package/avatars/95d3acb0-724d-4413-b20f-edad55812d5d +0 -0
- package/avatars/9641a946-f613-471b-bedd-c1730b96b51e +0 -0
- package/avatars/9b01cbbf-f6b2-43fb-b569-589b6f2a8134 +0 -0
- package/avatars/9cd4424d-a34f-4467-acc0-93cf82703e0d +0 -0
- package/avatars/9d9ad3b0-e5a6-420a-a6c5-fb9085b70376 +0 -0
- package/avatars/9e9e34b5-4e63-4c4b-9722-c7f5674b47aa +0 -0
- package/avatars/a387d5c1-59eb-4a6b-80c1-a8982ed12c33 +0 -0
- package/avatars/a3e86d21-d881-4824-8ebf-45e3bf0f9186 +0 -0
- package/avatars/a8d5cc1c-3f42-4b7b-8d33-f9a9ef77f96b +0 -0
- package/avatars/a91d815f-badc-4604-a7be-6c7a44e6101d +0 -0
- package/avatars/aa8d0324-bcec-4737-a8c4-bdbff914148d +0 -0
- package/avatars/abb8a941-8b6b-47d7-a2f9-8b153ba44aa2 +0 -0
- package/avatars/b011bb38-1ef3-4d22-82fa-8bf60faf7b5d +0 -0
- package/avatars/b24bcbc1-11f0-473d-a8b9-ba8ce4ca127d +0 -0
- package/avatars/b2607346-af1c-4e98-b725-7650a766db2a +0 -0
- package/avatars/b6300f7c-cb37-459b-b1bf-8a0a0e797a52 +0 -0
- package/avatars/b7d3cff3-84dd-4547-93a1-de1aaa8aa34c +0 -0
- package/avatars/baa4b51c-e97f-4f51-bcb3-f27bc506cfaf +0 -0
- package/avatars/be7022e4-e292-4515-80d5-f9b61ebeb4ce +0 -0
- package/avatars/bed596a3-7569-4854-9e76-f52d33c0a541 +0 -0
- package/avatars/bf69992f-3f72-4930-99bb-0ffe17f3aebf +0 -0
- package/avatars/ca00c250-c6d4-464d-a6de-1c8467a18fe8 +0 -0
- package/avatars/ca19d78f-c0bc-4bd5-b26f-6923cb19996d +0 -0
- package/avatars/cda4d6e1-e0a4-4024-ac95-6de98e713b98 +0 -0
- package/avatars/cf72c30d-2da8-4e81-aa71-735b9e714274 +0 -0
- package/avatars/d5a35b78-99b3-4564-b6b9-b2ccab28c470 +0 -0
- package/avatars/dadb38c1-2a9d-47a3-8d92-b56b6166973c +0 -0
- package/avatars/e68705c7-375d-4423-9a86-29a16bd3ee0e +0 -0
- package/avatars/e9af3e4c-1f62-4302-8b99-b68ce93b7a86 +0 -0
- package/avatars/ea7e7331-e845-4189-8248-5f5b1d63f5e3 +0 -0
- package/avatars/ef4f8dcb-ef6c-4e7a-9be1-0476161bfce5 +0 -0
- package/avatars/ef7d0917-a206-4f88-8b60-93f8253774dc +0 -0
- package/avatars/f1a554d6-1db3-4ff5-b0dc-b607d6c3b4ff +0 -0
- package/avatars/f1fecc21-c81f-49a8-88f3-f942a0a679f6 +0 -0
- package/avatars/f30a2427-1755-4053-813e-129a179e1dd3 +0 -0
- package/avatars/f5370717-5109-46a5-a8d7-e1dd996d0615 +0 -0
- package/avatars/f6dd7126-1144-4998-bbd3-d4e0fbee2e95 +0 -0
- package/avatars/f83413d5-0003-4756-9ece-745fd61cc468 +0 -0
- package/avatars/f9b3149e-7ec8-4bb3-a9b9-dcbe66dac197 +0 -0
- package/avatars/fa41d70b-857e-4423-bd7d-26ddcddc13b9 +0 -0
- package/avatars/fb551ee8-99c7-400b-8e1d-322ce4619998 +0 -0
- package/avatars/fe83a6d4-abb0-4ab0-b61d-76a7cc08be84 +0 -0
- package/dist/migrations/20210103192527_users.d.ts +0 -3
- package/dist/migrations/20210103192527_users.js +0 -30
- package/dist/migrations/20210103193502_mail.d.ts +0 -3
- package/dist/migrations/20210103193502_mail.js +0 -35
- package/dist/migrations/20210103193525_preKeys.d.ts +0 -3
- package/dist/migrations/20210103193525_preKeys.js +0 -30
- package/dist/migrations/20210103193553_oneTimeKeys.d.ts +0 -3
- package/dist/migrations/20210103193553_oneTimeKeys.js +0 -30
- package/dist/migrations/20210103193615_servers.d.ts +0 -3
- package/dist/migrations/20210103193615_servers.js +0 -28
- package/dist/migrations/20210103193729_channels.d.ts +0 -3
- package/dist/migrations/20210103193729_channels.js +0 -28
- package/dist/migrations/20210103193749_permissions.d.ts +0 -3
- package/dist/migrations/20210103193749_permissions.js +0 -30
- package/dist/migrations/20210103193801_files.d.ts +0 -3
- package/dist/migrations/20210103193801_files.js +0 -28
- package/emoji/04d98632-2c86-421b-a407-17f14fe86f8f +0 -0
- package/emoji/1160ed6e-1163-4043-9808-4029e863ed30 +0 -0
- package/emoji/1547ab18-1635-4a80-a82d-ebbb767b9932 +0 -0
- package/emoji/16922521-f6cb-4de4-860c-27916b22c6ba +0 -0
- package/emoji/198a9432-0e41-4866-994a-448d4775afcb +0 -0
- package/emoji/1be886b3-c9c5-4593-b516-f357ed931f96 +0 -0
- package/emoji/1c2b3d1d-637f-4103-b066-4bc4511a3ad7 +0 -0
- package/emoji/1efd27e7-b15f-475c-8b32-9159d26b169d +0 -0
- package/emoji/270b9409-0ea5-4be2-a239-a8dce13f9c31 +0 -0
- package/emoji/27812f76-fee2-49dd-a217-363de6d159dc +0 -0
- package/emoji/297ec202-8c24-44c6-aead-689d6d461883 +0 -0
- package/emoji/2bf06d86-17cb-4f40-a5ef-bd75d239a1a3 +0 -0
- package/emoji/31a75163-1cce-4dc1-b0a2-ecad6a4c500b +0 -0
- package/emoji/35235635-fdbd-4273-8428-f3cb3e1e8fd3 +0 -0
- package/emoji/3690fff2-6824-4403-a6e3-16a6a54979a9 +0 -0
- package/emoji/391014c2-59e0-46a8-85ec-7a7fdaca1d2d +0 -0
- package/emoji/3b383dcb-6e76-4e85-8e16-7c68040c06c2 +0 -0
- package/emoji/42d617a7-b104-42f5-9618-473181f752cf +0 -0
- package/emoji/482495d3-cce9-4f88-bf2a-f6003f03a9b5 +0 -0
- package/emoji/48390e06-0efb-404c-89bd-5f2be241bd50 +0 -0
- package/emoji/4b808d8d-3248-4149-b919-71b108391bcf +0 -0
- package/emoji/4bc13544-d82a-4e32-bd17-a70592274314 +0 -0
- package/emoji/4fcebf70-8623-4343-8243-67c8547b2edd +0 -0
- package/emoji/509d09aa-1214-459c-8081-50918a17b9af +0 -0
- package/emoji/5272abd8-d4d7-4b90-acd2-bf30e6c27243 +0 -0
- package/emoji/53c272ce-48bd-4d7e-bfb8-a6482b88be54 +0 -0
- package/emoji/5b279e65-06f7-4b26-8b4c-d1b48fba728d +0 -0
- package/emoji/5bd141f9-4394-4108-9376-66ebbc2c2bc1 +0 -0
- package/emoji/5c769156-f9cb-40bd-ab89-4edeece613fd +0 -0
- package/emoji/5c85fba9-8ba7-4fc9-b1b2-48dc30d24a1b +0 -0
- package/emoji/61a5e565-d20b-40ba-a139-b0c73a6027f3 +0 -0
- package/emoji/6913f43d-dd45-456c-9641-a126104d9ae5 +0 -0
- package/emoji/6957e74e-9622-492d-a950-242db3752260 +0 -0
- package/emoji/6a14bab5-26af-4bfa-9c17-be7c2511976d +0 -0
- package/emoji/6be09439-509e-4095-a30a-b1c7c573895d +0 -0
- package/emoji/6cc435b1-fe53-433c-b5a9-2b2019053997 +0 -0
- package/emoji/74f1b2af-bc7e-4a0f-802a-64ded185d5e2 +0 -0
- package/emoji/7890ba09-f02f-428e-807d-006d03d51d4a +0 -0
- package/emoji/7dc69179-6b3c-4f40-b20b-0ff573deea2d +0 -0
- package/emoji/7dd1b6b1-439d-4279-916a-995408863172 +0 -0
- package/emoji/820498ad-a2c8-43a2-ab83-d26f9c2246d4 +0 -0
- package/emoji/8319469c-2787-44e5-91a6-c8c39810dd7c +0 -0
- package/emoji/86745d1d-9e59-4607-b2b0-46c741079be1 +0 -0
- package/emoji/887b3cff-ae9e-4b5f-ad00-3ca9fc72f689 +0 -0
- package/emoji/8c6cf621-71d6-4fca-abe6-e19f4dd7f883 +0 -0
- package/emoji/8ca6d32e-a1ef-4956-a416-d8d0d680f085 +0 -0
- package/emoji/8d979f5e-38a2-4dd1-b3e4-80938bbe499b +0 -0
- package/emoji/99f68ea0-e3fe-4f03-9cc3-5f7f5315404d +0 -0
- package/emoji/9ad3aedc-7f79-4d68-a144-82e5b5dc3033 +0 -0
- package/emoji/9e418c2f-1f0f-46c4-be39-3bda38a28545 +0 -0
- package/emoji/a1f616bf-7402-4e24-9111-18acaebabb48 +0 -0
- package/emoji/a25ed9c1-3f9c-4e5f-ade2-7b159fb9fbf4 +0 -0
- package/emoji/a5176bc2-39a8-467b-8c75-6fbbc81b59c7 +0 -0
- package/emoji/a584215c-6547-438b-8ae8-dd490b51890e +0 -0
- package/emoji/a739895f-cf61-4b7c-b350-8e8283aaf751 +0 -0
- package/emoji/aaa10dd2-02a2-499e-9e17-c83787436508 +0 -0
- package/emoji/ae90baf2-a0ef-4d4d-9cc4-94f8ddd60f45 +0 -0
- package/emoji/b0564c48-feae-431a-95f9-df597c6c124c +0 -0
- package/emoji/b218bb93-e69c-4793-a669-83316650c4e7 +0 -0
- package/emoji/b2998c27-85d5-4598-ab41-469aa8e0fcad +0 -0
- package/emoji/b3da08ba-4179-4b5d-826f-5fc15e1a3ad2 +0 -0
- package/emoji/b840eb6a-a917-4bb2-854b-8f1022e7904b +0 -0
- package/emoji/b84baa76-b4b0-4b83-bc83-78661cb4f1d4 +0 -0
- package/emoji/baf69d80-8b1d-4032-855f-605cf0d489c3 +0 -0
- package/emoji/bb4d372c-ccd0-4a47-b157-b6a3b9f763e2 +0 -0
- package/emoji/bdbd1627-c81d-42d9-b3f5-8979e2ab74dc +0 -0
- package/emoji/c257388f-8b85-450b-b168-ebdf8d8c3026 +0 -0
- package/emoji/c573fde1-faa9-4c1d-a172-e283645afcfd +0 -0
- package/emoji/c8e27810-e8ea-47ce-b7ec-cef0a6becb28 +0 -0
- package/emoji/cdeef182-b220-4850-9ecf-5d7c472fd754 +0 -0
- package/emoji/d59c1aa9-6f81-4c07-96dd-9953401ff211 +0 -0
- package/emoji/dd407dbf-a077-40ba-957f-337b3c5efdc7 +0 -0
- package/emoji/e01f6e06-5728-4e2c-90fb-314a5827b766 +0 -0
- package/emoji/e1f9ed12-a2ce-433d-b454-b833438a1f9c +0 -0
- package/emoji/e697e5c4-acd6-41cd-a43e-edee8da3ab7b +0 -0
- package/emoji/e8182220-3464-4e31-8c08-466baead7bfc +0 -0
- package/emoji/eb0f3fd5-abc9-4abf-b816-d8458aeb7ec8 +0 -0
- package/emoji/eb38ecf7-0d13-4c51-b96a-3777f79321c4 +0 -0
- package/emoji/ee515c85-b7ce-4493-a427-994cc0af0d59 +0 -0
- package/emoji/f485cef2-d3fa-4d59-88af-b79a3105cacf +0 -0
- package/emoji/ff0dab2a-7015-4e8c-b0d0-3569058359dc +0 -0
- package/files/01087968-07b6-4fdb-9aeb-fa9dc061be94 +0 -0
- package/files/030455b3-17cc-415f-b3b3-2bb56c92ee8b +0 -0
- package/files/06129f52-a858-4031-ad85-4d7f2bb793af +0 -0
- package/files/0a6155a9-069f-45e9-8a06-56992fe55187 +0 -0
- package/files/12b9dda5-feb1-4f20-a987-ad422db5ba73 +0 -0
- package/files/13c8fa1e-8821-4628-b607-9e4fa4510df7 +0 -0
- package/files/159b0ad3-1a30-419d-af22-28a1096ce825 +0 -0
- package/files/1699c563-6769-431b-8041-99a6a4386f25 +0 -0
- package/files/176b916c-0dd9-4b93-bfdc-b8ccabf15a96 +0 -0
- package/files/1a27dad9-8cc9-4a0c-9d4e-7bde63adda60 +0 -0
- package/files/1d29832c-059a-4190-bca6-b83ac77540d9 +0 -0
- package/files/1dcde013-8833-4369-8726-81236e4eb30e +0 -0
- package/files/2080fc9f-d2c4-4fbc-af84-232fe4900a4f +0 -0
- package/files/20889623-6869-46a2-999b-c07708c12521 +0 -0
- package/files/2107e243-c378-418d-a183-7df13873c65b +0 -0
- package/files/225ed61e-8f8c-4d17-b675-9a9f9918d5b4 +0 -0
- package/files/29ffba15-5acc-4ef8-b5a4-5ce61d3f6e85 +0 -0
- package/files/2a69434c-1d8a-4e9d-90d4-569aaeaae7e3 +0 -0
- package/files/2aec8fcf-25bf-478e-b2f6-fe67ad753071 +0 -0
- package/files/2c64490e-c7de-4cb2-b22e-70e2ac69d88f +0 -0
- package/files/2d80b4f4-6389-4f5d-8d32-f0ed93820907 +0 -0
- package/files/2f9dd26e-363f-4445-8ee5-28548007a33a +0 -0
- package/files/360dd8f5-76c4-4e86-ba22-9025dc7ca2a4 +0 -0
- package/files/3a0a7f5b-45a5-4340-ba7c-6a0fa2c63871 +0 -0
- package/files/3a8fa6be-8acf-4b7b-b653-25edc6b28cdc +0 -5
- package/files/3d9e191b-2c15-42aa-9928-c2cdbb5e14ca +0 -0
- package/files/3dd0f2ef-0d4e-4837-bffc-22aa645cbe85 +0 -0
- package/files/402c4b9b-adbf-4ab6-a21d-c17369b48abf +0 -0
- package/files/4685d988-33eb-4902-872f-3b824f497c8b +0 -0
- package/files/495f6c55-07f8-4713-a444-a2261a789b94 +0 -0
- package/files/4acba8e3-567b-4062-b81b-340e205a01de +0 -0
- package/files/4c0a10cc-395b-474a-8140-677ed607da89 +0 -0
- package/files/537584d9-25ad-4830-808a-a1e3d63e2a52 +0 -0
- package/files/5519f10d-745e-4474-8ca8-6c111693704c +0 -0
- package/files/5563cf92-a5e3-4be3-867d-9647a02298d4 +0 -0
- package/files/55b70d9b-fd58-4800-832c-d6b4521ba6d0 +0 -0
- package/files/5623cff3-ce9b-403b-9915-50d2bcbc981c +0 -0
- package/files/576314ba-2a1a-4753-8d77-2a9e04f509d1 +0 -0
- package/files/58ed97ae-0eac-4e04-add1-76646effa2d5 +0 -0
- package/files/68638efd-5389-481d-a841-36164c62c078 +0 -0
- package/files/6936d34b-a1f8-4a9d-b005-5544dbdcf5e5 +0 -0
- package/files/6bda9e88-3a28-47f7-994e-900ede6bf984 +0 -0
- package/files/7024a3b9-a863-4618-9dbd-fa6502017ae0 +0 -0
- package/files/707caccb-3780-456d-9056-c20bfdfc0e5b +0 -0
- package/files/74b8c73c-032b-4640-a5b3-d30dd270cbcb +0 -0
- package/files/767cb262-2974-40f8-8182-f7770b431923 +0 -0
- package/files/7a669935-cb48-4349-b26b-7705f8a04fbc +0 -0
- package/files/7bc8f678-6ee7-454c-a1f8-bb9358b89c95 +0 -0
- package/files/7ccc2699-07ec-4177-a9ce-ee7dc952fda1 +0 -0
- package/files/7e0eabf4-b334-4683-8156-ab8d949a0532 +0 -0
- package/files/7f0644ab-02a3-4121-adfc-29a7c55cf804 +0 -0
- package/files/7ff3266d-f103-48ce-90dd-95b9dfe5fcc9 +0 -0
- package/files/836e2e8e-aefd-4b4a-a9b9-bf7436158a8c +0 -0
- package/files/875f7ae5-fa23-4fc5-b04a-8433a7f7089c +0 -0
- package/files/8ca62da9-f204-49e3-b418-9451661b2904 +0 -0
- package/files/9283054c-107f-474d-b61e-f1d0061bcb86 +0 -0
- package/files/93b1ce7f-9566-452b-b4be-30f87d3de150 +0 -0
- package/files/93fb51c7-e19b-4ac1-9dc3-aeb8da0672ed +0 -0
- package/files/9b54a3a1-534a-4ed5-b016-3c74ed4c9edd +0 -0
- package/files/9b5beb6f-712d-4969-b127-fd66c9b2a9c6 +0 -0
- package/files/9e964fbf-d063-498e-b2e3-79f8d6afcf5f +0 -0
- package/files/a66b7a9f-58c2-47a8-a429-a6f0647c6fe9 +0 -0
- package/files/a7cbda7d-81ba-40f7-a997-51146af63e5f +0 -0
- package/files/ac01e83a-e572-41b6-81ab-c992cff7c170 +0 -0
- package/files/ad11a58f-f963-4233-bd29-1658b6b7e600 +0 -0
- package/files/ae60a4a8-08b9-4521-a0a3-d015a8b3ed08 +0 -0
- package/files/b1d3cf27-8d76-4cf9-aa51-d7c6bfd1b3bf +0 -0
- package/files/b2c68863-8554-4ac6-8e99-821f0267cf91 +0 -0
- package/files/b3043e01-a771-44af-bf19-5327646ff929 +0 -0
- package/files/b6d01b89-def5-4c7c-8e97-a3d88617f8f4 +0 -0
- package/files/b8760b32-bb1e-4cd7-a9d6-29c6e0b071bc +0 -0
- package/files/ba5e0470-44f7-47b3-bcb5-eeab3ca8c292 +0 -0
- package/files/c3969b5f-43ae-43f3-9bdd-d3959c79ca01 +0 -0
- package/files/caecd488-dbe6-4a30-a400-bced2ba8dae6 +0 -0
- package/files/d7d865b8-3a05-4ed1-b95d-b93dc1ebb9a9 +0 -0
- package/files/dbca5f31-cf38-4c1e-83b0-5ec8473196fc +0 -0
- package/files/dbf07e82-fff6-4985-bebe-d62c0458bfd0 +0 -0
- package/files/dd759c20-eead-4e57-9e74-4d3a2b978e91 +0 -5
- package/files/de0b2cf1-981b-4e4a-a04e-ac185d1620cd +0 -0
- package/files/de6837fe-5aa2-4ff9-a067-2646a008c780 +0 -0
- package/files/e2fde852-91cb-4e01-88a2-ee086b5f227c +0 -0
- package/files/e391d1ce-8d39-460c-a462-791730131f7f +0 -0
- package/files/e6d9b60f-2b1b-4c6f-ba6d-02f6de90d40f +0 -0
- package/files/f693c62d-2ac8-49fd-aa38-30904e013e3c +0 -0
- package/files/f749fdf5-193e-41f7-b643-5696f67c6402 +0 -0
- package/files/f8910384-e75c-4d65-825f-52a6748f6475 +0 -0
- package/files/fad8826b-e952-4acb-a509-3e6543b94d61 +0 -0
- package/jest.config.js +0 -13
- package/spire.sqlite +0 -0
package/dist/Spire.js
CHANGED
|
@@ -1,128 +1,166 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
};
|
|
21
|
-
|
|
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
|
-
|
|
52
|
-
|
|
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 (!
|
|
58
|
-
|
|
58
|
+
if (!fs.existsSync(dir)) {
|
|
59
|
+
fs.mkdirSync(dir);
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
|
-
const
|
|
62
|
-
|
|
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.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
this
|
|
71
|
-
this.
|
|
72
|
-
this.db = new
|
|
73
|
-
this.
|
|
74
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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:
|
|
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
|
-
|
|
160
|
-
//
|
|
161
|
-
this.
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
|
192
|
-
|
|
253
|
+
if (getParam(req, "tokenType") !== "register") {
|
|
254
|
+
protect(req, res, next);
|
|
193
255
|
}
|
|
194
256
|
else {
|
|
195
257
|
next();
|
|
196
258
|
}
|
|
197
|
-
}, (req, res) =>
|
|
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
|
|
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 "
|
|
233
|
-
scope = TokenScopes.
|
|
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
|
-
},
|
|
246
|
-
|
|
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
|
-
|
|
319
|
+
this.log.error(String(err));
|
|
250
320
|
return res.sendStatus(500);
|
|
251
321
|
}
|
|
252
|
-
})
|
|
253
|
-
this.api.post("/whoami", (req, res) =>
|
|
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(
|
|
259
|
-
user: req.user,
|
|
328
|
+
res.send(msgpack.encode({
|
|
260
329
|
exp: req.exp,
|
|
261
|
-
token: req.
|
|
330
|
+
token: req.bearerToken,
|
|
331
|
+
user: req.user,
|
|
262
332
|
}));
|
|
263
|
-
})
|
|
264
|
-
this.api.
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
276
|
-
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
286
|
-
|
|
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.
|
|
397
|
+
this.log.error("Device challenge error: " + String(err));
|
|
398
|
+
return res.sendStatus(500);
|
|
291
399
|
}
|
|
292
|
-
})
|
|
293
|
-
this.api.post("/auth", (req, res) =>
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
300
|
-
|
|
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 =
|
|
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 =
|
|
311
|
-
const payloadHash =
|
|
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 =
|
|
512
|
+
const token = jwt.sign({ user: censorUser(userEntry) }, getJwtSecret(), { expiresIn: JWT_EXPIRY });
|
|
317
513
|
// just to make sure
|
|
318
|
-
|
|
319
|
-
res.
|
|
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
|
|
518
|
+
this.log.error(String(err));
|
|
324
519
|
res.sendStatus(500);
|
|
325
520
|
}
|
|
326
|
-
})
|
|
327
|
-
this.api.post("/register", (req, res) =>
|
|
521
|
+
});
|
|
522
|
+
this.api.post("/register", authLimiter, async (req, res) => {
|
|
328
523
|
try {
|
|
329
|
-
const
|
|
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 =
|
|
539
|
+
const regKey = xSignOpen(XUtils.decodeHex(regPayload.signed), XUtils.decodeHex(regPayload.signKey));
|
|
337
540
|
if (regKey &&
|
|
338
|
-
this.validateToken(
|
|
339
|
-
const [user, err] =
|
|
541
|
+
this.validateToken(uuidStringify(regKey), TokenScopes.Register)) {
|
|
542
|
+
const [user, err] = await this.db.createUser(regKey, regPayload);
|
|
340
543
|
if (err !== null) {
|
|
341
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
655
|
+
//# sourceMappingURL=Spire.js.map
|