@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.
- package/README.md +83 -28
- package/dist/ClientManager.d.ts +22 -26
- package/dist/ClientManager.js +198 -259
- package/dist/ClientManager.js.map +1 -0
- package/dist/Database.d.ts +71 -49
- package/dist/Database.js +737 -774
- package/dist/Database.js.map +1 -0
- package/dist/Spire.d.ts +20 -15
- package/dist/Spire.js +471 -249
- package/dist/Spire.js.map +1 -0
- package/dist/__tests__/Database.spec.js +102 -75
- package/dist/__tests__/Database.spec.js.map +1 -0
- package/dist/db/schema.d.ts +135 -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/migrations/2026-04-14_argon2id-password-hashing.d.ts +3 -0
- package/dist/migrations/2026-04-14_argon2id-password-hashing.js +12 -0
- package/dist/migrations/2026-04-14_argon2id-password-hashing.js.map +1 -0
- package/dist/run.js +25 -21
- package/dist/run.js.map +1 -0
- package/dist/server/avatar.d.ts +1 -4
- package/dist/server/avatar.js +63 -67
- package/dist/server/avatar.js.map +1 -0
- package/dist/server/errors.d.ts +56 -0
- package/dist/server/errors.js +68 -0
- package/dist/server/errors.js.map +1 -0
- package/dist/server/file.d.ts +2 -4
- package/dist/server/file.js +81 -64
- package/dist/server/file.js.map +1 -0
- package/dist/server/index.d.ts +7 -10
- package/dist/server/index.js +414 -416
- package/dist/server/index.js.map +1 -0
- package/dist/server/invite.d.ts +3 -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 +3 -7
- package/dist/server/user.js +56 -69
- 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/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 +14 -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 +93 -65
- package/src/ClientManager.ts +382 -0
- package/src/Database.ts +987 -0
- package/src/Spire.ts +807 -0
- package/src/__tests__/Database.spec.ts +150 -0
- package/src/ambient-modules.d.ts +1 -0
- package/src/db/schema.ts +166 -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/migrations/2026-04-14_argon2id-password-hashing.ts +18 -0
- package/src/run.ts +36 -0
- package/src/server/avatar.ts +134 -0
- package/src/server/errors.ts +104 -0
- package/src/server/file.ts +167 -0
- package/src/server/index.ts +841 -0
- package/src/server/invite.ts +63 -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 +121 -0
- package/src/server/utils.ts +59 -0
- package/src/types/express.ts +23 -0
- package/src/utils/createUint8UUID.ts +9 -0
- package/src/utils/jwtSecret.ts +15 -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/dist/utils/createLogger.d.ts +0 -5
- package/dist/utils/createLogger.js +0 -41
- 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,157 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
52
|
-
|
|
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 (!
|
|
58
|
-
|
|
57
|
+
if (!fs.existsSync(dir)) {
|
|
58
|
+
fs.mkdirSync(dir);
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
|
-
const
|
|
62
|
-
|
|
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.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
this
|
|
71
|
-
this.
|
|
72
|
-
this.db = new
|
|
73
|
-
this.
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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:
|
|
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
|
-
|
|
160
|
-
//
|
|
161
|
-
this.
|
|
162
|
-
const
|
|
163
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
|
192
|
-
|
|
233
|
+
if (getParam(req, "tokenType") !== "register") {
|
|
234
|
+
protect(req, res, next);
|
|
193
235
|
}
|
|
194
236
|
else {
|
|
195
237
|
next();
|
|
196
238
|
}
|
|
197
|
-
}, (req, res) =>
|
|
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
|
|
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 "
|
|
233
|
-
scope = TokenScopes.
|
|
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
|
-
},
|
|
246
|
-
|
|
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 (
|
|
249
|
-
|
|
296
|
+
catch (_err) {
|
|
297
|
+
// debugger: token creation failed
|
|
250
298
|
return res.sendStatus(500);
|
|
251
299
|
}
|
|
252
|
-
})
|
|
253
|
-
this.api.post("/whoami", (req, res) =>
|
|
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(
|
|
259
|
-
user: req.user,
|
|
306
|
+
res.send(msgpack.encode({
|
|
260
307
|
exp: req.exp,
|
|
261
|
-
|
|
308
|
+
user: req.user,
|
|
262
309
|
}));
|
|
263
|
-
})
|
|
264
|
-
this.api.
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
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
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
294
|
-
const
|
|
295
|
-
if (
|
|
296
|
-
res.
|
|
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
|
-
|
|
300
|
-
|
|
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 =
|
|
474
|
+
const userEntry = await this.db.retrieveUser(username);
|
|
305
475
|
if (!userEntry) {
|
|
306
|
-
res.sendStatus(
|
|
307
|
-
this.log.warn("User does not exist.");
|
|
476
|
+
res.sendStatus(401);
|
|
308
477
|
return;
|
|
309
478
|
}
|
|
310
|
-
const
|
|
311
|
-
|
|
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
|
-
|
|
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
|
-
|
|
319
|
-
res.
|
|
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 (
|
|
323
|
-
|
|
493
|
+
catch (_err) {
|
|
494
|
+
// debugger: auth error
|
|
324
495
|
res.sendStatus(500);
|
|
325
496
|
}
|
|
326
|
-
})
|
|
327
|
-
this.api.post("/register", (req, res) =>
|
|
497
|
+
});
|
|
498
|
+
this.api.post("/register", authLimiter, async (req, res) => {
|
|
328
499
|
try {
|
|
329
|
-
const
|
|
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 =
|
|
515
|
+
const regKey = xSignOpen(XUtils.decodeHex(regPayload.signed), XUtils.decodeHex(regPayload.signKey));
|
|
337
516
|
if (regKey &&
|
|
338
|
-
this.validateToken(
|
|
339
|
-
const [user, err] =
|
|
517
|
+
this.validateToken(uuidStringify(regKey), TokenScopes.Register)) {
|
|
518
|
+
const [user, err] = await this.db.createUser(regKey, regPayload);
|
|
340
519
|
if (err !== null) {
|
|
341
|
-
|
|
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
|
-
|
|
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
|
-
|
|
376
|
-
|
|
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 (
|
|
386
|
-
|
|
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
|
-
|
|
617
|
+
//# sourceMappingURL=Spire.js.map
|