@vex-chat/spire 0.7.4 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/README.md +82 -26
  2. package/dist/ClientManager.d.ts +24 -25
  3. package/dist/ClientManager.js +232 -509
  4. package/dist/ClientManager.js.map +1 -0
  5. package/dist/Database.d.ts +49 -41
  6. package/dist/Database.js +698 -716
  7. package/dist/Database.js.map +1 -0
  8. package/dist/Spire.d.ts +23 -15
  9. package/dist/Spire.js +518 -218
  10. package/dist/Spire.js.map +1 -0
  11. package/dist/__tests__/Database.spec.js +113 -73
  12. package/dist/__tests__/Database.spec.js.map +1 -0
  13. package/dist/db/schema.d.ts +134 -0
  14. package/dist/db/schema.js +2 -0
  15. package/dist/db/schema.js.map +1 -0
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.js +3 -5
  18. package/dist/index.js.map +1 -0
  19. package/dist/middleware/validate.d.ts +12 -0
  20. package/dist/middleware/validate.js +35 -0
  21. package/dist/middleware/validate.js.map +1 -0
  22. package/dist/migrations/2026-04-06_initial-schema.d.ts +3 -0
  23. package/dist/migrations/2026-04-06_initial-schema.js +192 -0
  24. package/dist/migrations/2026-04-06_initial-schema.js.map +1 -0
  25. package/dist/run.js +26 -21
  26. package/dist/run.js.map +1 -0
  27. package/dist/server/avatar.d.ts +3 -4
  28. package/dist/server/avatar.js +85 -67
  29. package/dist/server/avatar.js.map +1 -0
  30. package/dist/server/errors.d.ts +59 -0
  31. package/dist/server/errors.js +94 -0
  32. package/dist/server/errors.js.map +1 -0
  33. package/dist/server/file.d.ts +3 -4
  34. package/dist/server/file.js +101 -61
  35. package/dist/server/file.js.map +1 -0
  36. package/dist/server/index.d.ts +9 -6
  37. package/dist/server/index.js +595 -70
  38. package/dist/server/index.js.map +1 -0
  39. package/dist/server/invite.d.ts +4 -5
  40. package/dist/server/invite.js +21 -103
  41. package/dist/server/invite.js.map +1 -0
  42. package/dist/server/openapi.d.ts +2 -0
  43. package/dist/server/openapi.js +40 -0
  44. package/dist/server/openapi.js.map +1 -0
  45. package/dist/server/permissions.d.ts +16 -0
  46. package/dist/server/permissions.js +22 -0
  47. package/dist/server/permissions.js.map +1 -0
  48. package/dist/server/rateLimit.d.ts +28 -0
  49. package/dist/server/rateLimit.js +58 -0
  50. package/dist/server/rateLimit.js.map +1 -0
  51. package/dist/server/user.d.ts +4 -7
  52. package/dist/server/user.js +66 -76
  53. package/dist/server/user.js.map +1 -0
  54. package/dist/server/utils.d.ts +35 -7
  55. package/dist/server/utils.js +50 -6
  56. package/dist/server/utils.js.map +1 -0
  57. package/dist/types/express.d.ts +20 -0
  58. package/dist/types/express.js +2 -0
  59. package/dist/types/express.js.map +1 -0
  60. package/dist/utils/createLogger.js +13 -19
  61. package/dist/utils/createLogger.js.map +1 -0
  62. package/dist/utils/createUint8UUID.js +6 -10
  63. package/dist/utils/createUint8UUID.js.map +1 -0
  64. package/dist/utils/jwtSecret.d.ts +7 -0
  65. package/dist/utils/jwtSecret.js +15 -0
  66. package/dist/utils/jwtSecret.js.map +1 -0
  67. package/dist/utils/loadEnv.js +7 -22
  68. package/dist/utils/loadEnv.js.map +1 -0
  69. package/dist/utils/msgpack.d.ts +2 -0
  70. package/dist/utils/msgpack.js +4 -0
  71. package/dist/utils/msgpack.js.map +1 -0
  72. package/package.json +91 -63
  73. package/src/ClientManager.ts +434 -0
  74. package/src/Database.ts +925 -0
  75. package/src/Spire.ts +878 -0
  76. package/src/__tests__/Database.spec.ts +167 -0
  77. package/src/ambient-modules.d.ts +1 -0
  78. package/src/db/schema.ts +165 -0
  79. package/src/index.ts +3 -0
  80. package/src/middleware/validate.ts +38 -0
  81. package/src/migrations/2026-04-06_initial-schema.ts +218 -0
  82. package/src/run.ts +37 -0
  83. package/src/server/avatar.ts +141 -0
  84. package/src/server/errors.ts +133 -0
  85. package/src/server/file.ts +172 -0
  86. package/src/server/index.ts +855 -0
  87. package/src/server/invite.ts +65 -0
  88. package/src/server/openapi.ts +51 -0
  89. package/src/server/permissions.ts +40 -0
  90. package/src/server/rateLimit.ts +86 -0
  91. package/src/server/user.ts +125 -0
  92. package/src/server/utils.ts +59 -0
  93. package/src/types/express.ts +23 -0
  94. package/src/utils/createLogger.ts +47 -0
  95. package/src/utils/createUint8UUID.ts +9 -0
  96. package/src/utils/jwtSecret.ts +16 -0
  97. package/src/utils/loadEnv.ts +15 -0
  98. package/src/utils/msgpack.ts +4 -0
  99. package/avatars/169d76cb-6e7c-4e24-8224-017673eed8ff +0 -0
  100. package/avatars/1cae8d0b-0c6b-4c73-b25c-2d2349a57122 +0 -0
  101. package/avatars/1d87bc2b-71fc-4818-8004-40d04093e5fc +0 -0
  102. package/avatars/1f2c3d62-8b4d-465a-9895-51a24caef00d +0 -0
  103. package/avatars/245ee7fc-1004-41ab-adab-a04c9ceb9d7a +0 -0
  104. package/avatars/2465c28c-bdaf-4fa2-b42a-d054f04dc39b +0 -0
  105. package/avatars/3900a674-a2dd-4996-a61d-8c29b3270f41 +0 -0
  106. package/avatars/3c3b9c77-ea50-45e7-bb25-65d6f3d2a219 +0 -0
  107. package/avatars/414a2ff4-ad2f-4a7d-aa27-3b09ad3522b2 +0 -0
  108. package/avatars/522fe504-f0ad-4ed4-9dc6-e5dc2338e531 +0 -0
  109. package/avatars/53e5eb29-e7d1-4d58-add9-d44f39f0cfb7 +0 -0
  110. package/avatars/54d3f757-1038-41c8-bfb9-efd37b6e8ebe +0 -0
  111. package/avatars/623e86d7-c49c-46f6-9b76-ca70c9dbc43b +0 -0
  112. package/avatars/66e2abae-60f5-4dd1-b9fb-297a4bedfeb0 +0 -0
  113. package/avatars/6f37980e-f6fa-4d6d-9206-24050b403f45 +0 -0
  114. package/avatars/80138ece-eb5c-4b20-817b-903d6b0f54ae +0 -0
  115. package/avatars/841c77d3-37c4-431b-be22-672888062874 +0 -0
  116. package/avatars/88051a61-2bda-4750-95a3-5fb0b4918149 +0 -0
  117. package/avatars/89540973-c421-4bf1-8b89-9f1eaa51c1b5 +0 -0
  118. package/avatars/8a802fad-8c99-4942-8f80-47cec600149c +0 -0
  119. package/avatars/90531d8a-907a-4a1a-ac45-c85e4acb0df9 +0 -0
  120. package/avatars/9b7d0da9-b8d6-4801-b128-9993f79f464a +0 -0
  121. package/avatars/9bc456f1-c4c4-48a1-b9e6-fd44dd744a72 +0 -0
  122. package/avatars/9cf878bf-7430-49ec-a47a-78f3c93793dd +0 -0
  123. package/avatars/9ee82847-6ad3-45e5-92b9-f474a6c54d96 +0 -0
  124. package/avatars/ab44c857-d81d-4c88-85db-32f9532e5376 +0 -0
  125. package/avatars/b396a8d2-dc14-48d2-aac4-3755dc637051 +0 -0
  126. package/avatars/b6ac11c5-a8b2-4e0a-995a-9c87f1e58787 +0 -0
  127. package/avatars/b79d6855-b738-434c-be32-809637e62b9b +0 -0
  128. package/avatars/bbcd0188-d6a5-48ae-90fb-be5ff30599ab +0 -0
  129. package/avatars/bc7a9e0e-4720-4a6e-a90d-c11fec94d380 +0 -0
  130. package/avatars/c1c4889f-8383-4041-8bdd-9fded4046f37 +0 -0
  131. package/avatars/c4c7203c-d93a-4749-ade2-17053acf1d2a +0 -0
  132. package/avatars/ca974a70-0a23-4668-8b80-c4304dc7f793 +0 -0
  133. package/avatars/cf119a0d-eb3f-4bed-905b-f14a876c3535 +0 -0
  134. package/avatars/d464b03d-30c2-49e3-a666-80aefa8a1b35 +0 -0
  135. package/avatars/da0eee89-82f0-4d45-ab48-7d2786b634c5 +0 -0
  136. package/avatars/de4c77a5-68e9-4bb2-b40d-d964bf377d61 +0 -0
  137. package/avatars/dea95395-7d0b-42aa-a9ed-40c7d4fb4c48 +0 -0
  138. package/avatars/edb30749-59ba-4aa2-9c52-0fb22048f4cf +0 -0
  139. package/avatars/f17c245a-af7d-445b-9365-49f7f54b1eeb +0 -0
  140. package/avatars/f1ee6a35-b262-4dbf-99f5-3d011e3b98ec +0 -0
  141. package/avatars/f802bdd0-345d-41f6-9184-0f30e1258fb3 +0 -0
  142. package/dist/migrations/20210103192527_users.d.ts +0 -3
  143. package/dist/migrations/20210103192527_users.js +0 -30
  144. package/dist/migrations/20210103193502_mail.d.ts +0 -3
  145. package/dist/migrations/20210103193502_mail.js +0 -35
  146. package/dist/migrations/20210103193525_preKeys.d.ts +0 -3
  147. package/dist/migrations/20210103193525_preKeys.js +0 -30
  148. package/dist/migrations/20210103193553_oneTimeKeys.d.ts +0 -3
  149. package/dist/migrations/20210103193553_oneTimeKeys.js +0 -30
  150. package/dist/migrations/20210103193615_servers.d.ts +0 -3
  151. package/dist/migrations/20210103193615_servers.js +0 -28
  152. package/dist/migrations/20210103193729_channels.d.ts +0 -3
  153. package/dist/migrations/20210103193729_channels.js +0 -28
  154. package/dist/migrations/20210103193749_permissions.d.ts +0 -3
  155. package/dist/migrations/20210103193749_permissions.js +0 -30
  156. package/dist/migrations/20210103193801_files.d.ts +0 -3
  157. package/dist/migrations/20210103193801_files.js +0 -28
  158. package/files/00ea7368-45a7-4f6a-a199-974da14be1a0 +0 -0
  159. package/files/039503d2-a170-4962-b921-c97994ba64ff +0 -0
  160. package/files/14b6fa02-4cbb-40df-be4f-a07187cb619e +0 -0
  161. package/files/15c04cb1-dc6a-4f19-aa6f-4a3b92d05bf7 +0 -0
  162. package/files/2a8d411c-8b92-4532-b84d-d64c638d6293 +0 -0
  163. package/files/37de2cd3-08a8-4044-9c37-e13386765f3d +0 -0
  164. package/files/42452029-284e-4c81-9f18-feb6ce309eed +0 -0
  165. package/files/43d09f2f-29c8-415f-8c4a-23f2a32eb79e +0 -0
  166. package/files/52992923-33ab-44a1-8118-605e9b4856a7 +0 -0
  167. package/files/53180681-36e2-49c0-8382-94dca0da09bf +0 -0
  168. package/files/5a56cd7b-1d04-4619-b60f-1e5515b9164a +0 -0
  169. package/files/5ced7676-20c4-4219-a4f2-70a25eb7eea8 +0 -0
  170. package/files/60e787ff-8ec5-444d-b963-0aaf5313b53e +0 -0
  171. package/files/67a17729-fcb7-4339-9499-1fc08fea72ca +0 -0
  172. package/files/68d09565-908d-4a67-8f09-e183f8708eb4 +0 -0
  173. package/files/70e587c5-56e8-47f0-bd36-691efcb0cc2e +0 -4
  174. package/files/7a227619-b715-4e2c-a79d-b2158d56a799 +0 -0
  175. package/files/7e3cc3ea-b706-4835-994d-65d33acaf369 +0 -0
  176. package/files/816f72a0-65dc-40c1-8862-1d22065cca3c +0 -0
  177. package/files/8bf84972-5086-4631-a752-093d7b1a098b +0 -0
  178. package/files/8c46e3bc-3f2e-441d-b8cc-17428fc3d219 +0 -0
  179. package/files/8eb83364-8826-4eee-895a-ba5cd3ab85e9 +0 -0
  180. package/files/8faefaea-14e3-49e4-ac74-9710622bfae9 +0 -0
  181. package/files/91af08c2-9dca-41f4-b6c6-b7bf33505df9 +0 -0
  182. package/files/9349dffa-35dd-49a0-a651-10b438038f95 +0 -0
  183. package/files/b0a12a41-6283-4f27-b2c8-66774991d96c +0 -0
  184. package/files/b28afb08-d18b-48a8-93c3-6a59c66d8fee +0 -0
  185. package/files/be60fe01-1578-4363-a908-41500092b77d +0 -0
  186. package/files/cf7b90e3-734a-4453-9ff3-9a331404b5ef +0 -0
  187. package/files/d1be30aa-8ff4-41c1-b360-f65922029047 +0 -0
  188. package/files/d2d31a57-a413-443c-9b97-fa9ca394ef0b +0 -0
  189. package/files/d357f223-b786-478d-a113-b53cb62acdab +0 -0
  190. package/files/d4a69ee0-05c4-4ff0-8753-624cdd0f24e9 +0 -0
  191. package/files/d57f411b-1874-4f00-a6b0-2f86de6b229d +0 -0
  192. package/files/d85aaa33-7f70-4a70-b3d2-cad667beb38c +0 -0
  193. package/files/db2fc236-dbe0-4d24-bfaf-884829fd090a +0 -0
  194. package/files/df2e20ec-6dcc-4402-94ee-7d1163f84197 +0 -0
  195. package/files/e0880ab5-6d49-4dc0-a5ec-0d3793a8a7b8 +0 -0
  196. package/files/e59ee9c6-5aea-48ea-b3d9-94a324dc2260 +0 -0
  197. package/files/e6d7aad6-4220-4ce7-85f8-89d0b1f0cc89 +0 -0
  198. package/files/f0bfc98c-3534-459d-931a-7c6e77bde153 +0 -0
  199. package/files/f8443740-050c-4714-9bf6-334783ae6ffd +0 -0
  200. package/files/fc405ae7-f9fb-455d-ac8c-0ca0d88d4115 +0 -0
  201. package/jest.config.js +0 -13
  202. package/spire.sqlite +0 -0
@@ -1,117 +1,642 @@
1
- "use strict";
2
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
- return new (P || (P = Promise))(function (resolve, reject) {
5
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
- step((generator = generator.apply(thisArg, _arguments || [])).next());
9
- });
10
- };
11
- var __importDefault = (this && this.__importDefault) || function (mod) {
12
- return (mod && mod.__esModule) ? mod : { "default": mod };
13
- };
14
- Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.initApp = exports.protect = exports.EXPIRY_TIME = void 0;
16
- const fs_1 = __importDefault(require("fs"));
17
- const cookie_parser_1 = __importDefault(require("cookie-parser"));
18
- const cors_1 = __importDefault(require("cors"));
19
- const express_1 = __importDefault(require("express"));
20
- const helmet_1 = __importDefault(require("helmet"));
21
- const morgan_1 = __importDefault(require("morgan"));
22
- const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
23
- const avatar_1 = require("./avatar");
24
- const file_1 = require("./file");
25
- const invite_1 = require("./invite");
26
- const user_1 = require("./user");
1
+ import * as fs from "node:fs";
2
+ import * as fsp from "node:fs/promises";
3
+ import express from "express";
4
+ import { XUtils } from "@vex-chat/crypto";
5
+ import { xSignOpen } from "@vex-chat/crypto";
6
+ import { PreKeysWSSchema, TokenScopes, UserSchema } from "@vex-chat/types";
7
+ import cors from "cors";
8
+ import { fileTypeFromBuffer, fileTypeFromFile } from "file-type";
9
+ import helmet from "helmet";
10
+ import jwt from "jsonwebtoken";
11
+ import morgan from "morgan";
12
+ import multer from "multer";
13
+ import parseDuration from "parse-duration";
14
+ import { stringify as uuidStringify } from "uuid";
15
+ import { z } from "zod/v4";
16
+ import { POWER_LEVELS } from "../ClientManager.js";
17
+ import { JWT_EXPIRY } from "../Spire.js";
18
+ import { getJwtSecret } from "../utils/jwtSecret.js";
19
+ import { msgpack } from "../utils/msgpack.js";
20
+ import { getAvatarRouter } from "./avatar.js";
21
+ import { errorHandler } from "./errors.js";
22
+ import { getFileRouter } from "./file.js";
23
+ import { getInviteRouter } from "./invite.js";
24
+ import { setupDocs } from "./openapi.js";
25
+ import { hasAnyPermission, hasPermission, userHasPermission, } from "./permissions.js";
26
+ import { globalLimiter } from "./rateLimit.js";
27
+ import { getUserRouter } from "./user.js";
28
+ import { censorUser, getParam, getUser } from "./utils.js";
27
29
  // expiry of regkeys
28
- exports.EXPIRY_TIME = 1000 * 60 * 5;
29
- const checkJwt = (req, res, next) => {
30
- if (req.cookies.auth) {
30
+ export const EXPIRY_TIME = 1000 * 60 * 5;
31
+ export const ALLOWED_IMAGE_TYPES = [
32
+ "image/jpeg",
33
+ "image/png",
34
+ "image/gif",
35
+ "image/apng",
36
+ "image/avif",
37
+ ];
38
+ // ── Zod schemas for trust-boundary validation ──────────────────────────
39
+ const invitePayload = z.object({
40
+ duration: z.string().min(1),
41
+ serverID: z.string().min(1),
42
+ });
43
+ const channelPayload = z.object({
44
+ name: z.string().min(1).max(255),
45
+ });
46
+ const deviceListPayload = z.array(z.string());
47
+ const connectPayload = z.object({
48
+ signed: z.custom((val) => val instanceof Uint8Array),
49
+ });
50
+ const safePathParam = z.string().regex(/^[a-zA-Z0-9._-]+$/);
51
+ const emojiPayload = z.object({
52
+ file: z.string().optional(),
53
+ name: z.string().min(1),
54
+ signed: z.string().optional(),
55
+ });
56
+ const jwtUserPayload = z.object({
57
+ exp: z.number().optional(),
58
+ user: UserSchema,
59
+ });
60
+ const jwtDevicePayload = z.object({
61
+ device: z.object({
62
+ deleted: z.boolean(),
63
+ deviceID: z.string(),
64
+ lastLogin: z.string(),
65
+ name: z.string(),
66
+ owner: z.string(),
67
+ signKey: z.string(),
68
+ }),
69
+ });
70
+ /** Extract Bearer token from Authorization header. */
71
+ function extractBearer(req) {
72
+ const header = req.headers.authorization;
73
+ if (!header || !header.startsWith("Bearer "))
74
+ return null;
75
+ return header.slice(7);
76
+ }
77
+ const checkAuth = (req, _res, next) => {
78
+ const token = extractBearer(req);
79
+ if (token) {
31
80
  try {
32
- const result = jsonwebtoken_1.default.verify(req.cookies.auth, process.env.SPK);
33
- // lol glad this is a try/catch block
34
- req.user = result.user;
81
+ const result = jwt.verify(token, getJwtSecret());
82
+ const parsed = jwtUserPayload.safeParse(result);
83
+ if (parsed.success) {
84
+ req.user = parsed.data.user;
85
+ if (parsed.data.exp !== undefined) {
86
+ req.exp = parsed.data.exp;
87
+ }
88
+ req.bearerToken = token;
89
+ }
35
90
  }
36
- catch (err) {
37
- console.warn(err.toString());
91
+ catch {
92
+ // Token verification failed — continue without auth
38
93
  }
39
94
  }
40
95
  next();
41
96
  };
42
- const protect = (req, res, next) => {
97
+ const checkDevice = (req, _res, next) => {
98
+ const token = req.headers["x-device-token"];
99
+ if (typeof token === "string" && token) {
100
+ try {
101
+ const result = jwt.verify(token, getJwtSecret());
102
+ const parsed = jwtDevicePayload.safeParse(result);
103
+ if (parsed.success) {
104
+ req.device = parsed.data.device;
105
+ }
106
+ }
107
+ catch {
108
+ // Device token verification failed — continue without device
109
+ }
110
+ }
111
+ next();
112
+ };
113
+ export const protect = (req, res, next) => {
43
114
  if (!req.user) {
44
115
  res.sendStatus(401);
45
116
  return;
46
117
  }
47
118
  next();
48
119
  };
49
- exports.protect = protect;
50
- // 3-19 chars long
120
+ export const msgpackParser = (req, res, next) => {
121
+ if (req.is("application/msgpack")) {
122
+ try {
123
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-argument -- Express req.body is any; decoded body is validated by route-level Zod schemas
124
+ req.body = msgpack.decode(req.body);
125
+ }
126
+ catch {
127
+ res.sendStatus(400);
128
+ return;
129
+ }
130
+ }
131
+ next();
132
+ };
133
+ const isProduction = process.env["NODE_ENV"] === "production";
51
134
  const directories = ["files", "avatars"];
52
135
  for (const dir of directories) {
53
- if (!fs_1.default.existsSync(dir)) {
54
- fs_1.default.mkdirSync(dir);
136
+ if (!fs.existsSync(dir)) {
137
+ fs.mkdirSync(dir);
55
138
  }
56
139
  }
57
- const initApp = (api, db, log, tokenValidator, signKeys, notify) => {
140
+ export const initApp = (api, db, log, tokenValidator, signKeys, notify) => {
58
141
  // INIT ROUTERS
59
- const userRouter = user_1.getUserRouter(db, log, tokenValidator);
60
- const fileRouter = file_1.getFileRouter(db, log);
61
- const avatarRouter = avatar_1.getAvatarRouter(db, log);
62
- const inviteRouter = invite_1.getInviteRouter(db, log, tokenValidator, notify);
142
+ const userRouter = getUserRouter(db, log, tokenValidator);
143
+ const fileRouter = getFileRouter(db, log);
144
+ const avatarRouter = getAvatarRouter(db, log);
145
+ const inviteRouter = getInviteRouter(db, log, tokenValidator, notify);
63
146
  // MIDDLEWARE
64
- api.use(express_1.default.json({ limit: "20mb" }));
65
- api.use(helmet_1.default());
66
- api.use(cookie_parser_1.default());
67
- api.use(checkJwt);
147
+ // Global per-IP rate limit is the FIRST middleware so a flooded
148
+ // source hits the limiter before Express spends any cycles on
149
+ // body parsing, helmet, or auth. See src/server/rateLimit.ts.
150
+ api.use(globalLimiter);
151
+ api.use(express.json({ limit: "20mb" }));
152
+ api.use(express.raw({
153
+ limit: "20mb",
154
+ type: "application/msgpack",
155
+ }));
156
+ if (isProduction) {
157
+ api.use(helmet());
158
+ }
159
+ api.use(msgpackParser);
160
+ api.use(checkAuth);
161
+ api.use(checkDevice);
68
162
  if (!jestRun()) {
69
- api.use(morgan_1.default("dev", { stream: process.stdout }));
163
+ api.use(morgan("dev", { stream: process.stdout }));
70
164
  }
71
- api.use(cors_1.default({ credentials: true }));
72
- // SIMPLE RESOURCES
73
- api.get("/server/:id", (req, res) => __awaiter(void 0, void 0, void 0, function* () {
74
- const server = yield db.retrieveServer(req.params.id);
165
+ api.use(cors({ credentials: true }));
166
+ api.get("/server/:id", protect, async (req, res) => {
167
+ const server = await db.retrieveServer(getParam(req, "id"));
75
168
  if (server) {
76
- return res.send(server);
169
+ return res.send(msgpack.encode(server));
77
170
  }
78
171
  else {
172
+ return res.sendStatus(404);
173
+ }
174
+ });
175
+ api.post("/server/:name", protect, async (req, res) => {
176
+ const userDetails = getUser(req);
177
+ const serverName = atob(getParam(req, "name"));
178
+ const server = await db.createServer(serverName, userDetails.userID);
179
+ res.send(msgpack.encode(server));
180
+ });
181
+ api.post("/server/:serverID/invites", protect, async (req, res) => {
182
+ const userDetails = getUser(req);
183
+ const parsedPayload = invitePayload.safeParse(req.body);
184
+ if (!parsedPayload.success) {
185
+ res.status(400).json({
186
+ error: "Invalid invite payload",
187
+ issues: parsedPayload.error.issues,
188
+ });
189
+ return;
190
+ }
191
+ const payload = parsedPayload.data;
192
+ const serverEntry = await db.retrieveServer(getParam(req, "serverID"));
193
+ if (!serverEntry) {
79
194
  res.sendStatus(404);
195
+ return;
80
196
  }
81
- }));
82
- api.get("/channel/:id", (req, res) => __awaiter(void 0, void 0, void 0, function* () {
83
- const channel = yield db.retrieveChannel(req.params.id);
197
+ const permissions = await db.retrievePermissions(userDetails.userID, "server");
198
+ if (!hasPermission(permissions, getParam(req, "serverID"), POWER_LEVELS.INVITE)) {
199
+ log.warn("No permission!");
200
+ res.sendStatus(401);
201
+ return;
202
+ }
203
+ const duration = parseDuration(payload.duration, "ms");
204
+ if (!duration) {
205
+ res.sendStatus(400);
206
+ return;
207
+ }
208
+ const expires = new Date(Date.now() + duration);
209
+ const invite = await db.createInvite(crypto.randomUUID(), serverEntry.serverID, userDetails.userID, expires.toString());
210
+ res.send(msgpack.encode(invite));
211
+ });
212
+ api.get("/server/:serverID/invites", protect, async (req, res) => {
213
+ const userDetails = getUser(req);
214
+ const permissions = await db.retrievePermissions(userDetails.userID, "server");
215
+ if (!hasPermission(permissions, getParam(req, "serverID"), POWER_LEVELS.INVITE)) {
216
+ res.sendStatus(401);
217
+ return;
218
+ }
219
+ const inviteList = await db.retrieveServerInvites(getParam(req, "serverID"));
220
+ res.send(msgpack.encode(inviteList));
221
+ });
222
+ api.delete("/server/:id", protect, async (req, res) => {
223
+ const userDetails = getUser(req);
224
+ const serverID = getParam(req, "id");
225
+ const permissions = await db.retrievePermissions(userDetails.userID, "server");
226
+ if (hasPermission(permissions, serverID, POWER_LEVELS.DELETE)) {
227
+ await db.deleteServer(serverID);
228
+ res.sendStatus(200);
229
+ return;
230
+ }
231
+ res.sendStatus(401);
232
+ });
233
+ api.post("/server/:id/channels", protect, async (req, res) => {
234
+ const userDetails = getUser(req);
235
+ const serverID = getParam(req, "id");
236
+ // resourceID is serverID
237
+ const parsedBody = channelPayload.safeParse(req.body);
238
+ if (!parsedBody.success) {
239
+ res.status(400).json({
240
+ error: "Invalid channel payload",
241
+ issues: parsedBody.error.issues,
242
+ });
243
+ return;
244
+ }
245
+ const { name } = parsedBody.data;
246
+ const permissions = await db.retrievePermissions(userDetails.userID, "server");
247
+ if (hasPermission(permissions, serverID, POWER_LEVELS.CREATE)) {
248
+ const channel = await db.createChannel(name, serverID);
249
+ res.send(msgpack.encode(channel));
250
+ const affectedUsers = await db.retrieveAffectedUsers(serverID);
251
+ // tell everyone about server change
252
+ for (const user of affectedUsers) {
253
+ notify(user.userID, "serverChange", crypto.randomUUID(), serverID);
254
+ }
255
+ return;
256
+ }
257
+ res.sendStatus(401);
258
+ });
259
+ api.get("/server/:id/channels", protect, async (req, res) => {
260
+ const serverID = getParam(req, "id");
261
+ const userDetails = getUser(req);
262
+ const permissions = await db.retrievePermissions(userDetails.userID, "server");
263
+ if (hasAnyPermission(permissions, serverID)) {
264
+ const channels = await db.retrieveChannels(serverID);
265
+ res.send(msgpack.encode(channels));
266
+ return;
267
+ }
268
+ res.sendStatus(401);
269
+ });
270
+ api.get("/server/:serverID/emoji", protect, async (req, res) => {
271
+ const rows = await db.retrieveEmojiList(getParam(req, "serverID"));
272
+ res.send(msgpack.encode(rows));
273
+ });
274
+ api.get("/server/:serverID/permissions", protect, async (req, res) => {
275
+ const userDetails = getUser(req);
276
+ const serverID = getParam(req, "serverID");
277
+ const permissions = await db.retrievePermissionsByResourceID(serverID);
278
+ const canSee = permissions.some((perm) => perm.userID === userDetails.userID);
279
+ if (!canSee) {
280
+ res.sendStatus(401);
281
+ return;
282
+ }
283
+ res.send(msgpack.encode(permissions));
284
+ });
285
+ api.delete("/channel/:id", protect, async (req, res) => {
286
+ const channelID = getParam(req, "id");
287
+ const userDetails = getUser(req);
288
+ const channel = await db.retrieveChannel(channelID);
289
+ if (!channel) {
290
+ res.sendStatus(401);
291
+ return;
292
+ }
293
+ const permissions = await db.retrievePermissions(userDetails.userID, "server");
294
+ for (const permission of permissions) {
295
+ if (permission.resourceID === channel.serverID &&
296
+ permission.powerLevel > 50) {
297
+ await db.deleteChannel(channelID);
298
+ res.sendStatus(200);
299
+ const affectedUsers = await db.retrieveAffectedUsers(channel.serverID);
300
+ // tell everyone about server change
301
+ for (const user of affectedUsers) {
302
+ notify(user.userID, "serverChange", crypto.randomUUID(), channel.serverID);
303
+ }
304
+ return;
305
+ }
306
+ }
307
+ res.sendStatus(401);
308
+ });
309
+ api.get("/channel/:id", protect, async (req, res) => {
310
+ const channel = await db.retrieveChannel(getParam(req, "id"));
84
311
  if (channel) {
85
- return res.send(channel);
312
+ return res.send(msgpack.encode(channel));
86
313
  }
87
314
  else {
315
+ return res.sendStatus(404);
316
+ }
317
+ });
318
+ api.delete("/permission/:permissionID", protect, async (req, res) => {
319
+ const permissionID = getParam(req, "permissionID");
320
+ const userDetails = getUser(req);
321
+ const permToDelete = await db.retrievePermission(permissionID);
322
+ if (!permToDelete) {
88
323
  res.sendStatus(404);
324
+ return;
89
325
  }
90
- }));
91
- api.post("/deviceList", (req, res) => __awaiter(void 0, void 0, void 0, function* () {
92
- const userIDs = req.body;
93
- const devices = yield db.retrieveUserDeviceList(userIDs);
94
- res.send(devices);
95
- }));
96
- api.get("/device/:id", (req, res) => __awaiter(void 0, void 0, void 0, function* () {
97
- const device = yield db.retrieveDevice(req.params.id);
326
+ const permissions = await db.retrievePermissions(userDetails.userID, permToDelete.resourceType);
327
+ for (const perm of permissions) {
328
+ if (perm.resourceID === permToDelete.resourceID &&
329
+ (perm.userID === userDetails.userID ||
330
+ (perm.powerLevel > POWER_LEVELS.DELETE &&
331
+ perm.powerLevel > permToDelete.powerLevel))) {
332
+ await db.deletePermission(permToDelete.permissionID);
333
+ res.sendStatus(200);
334
+ return;
335
+ }
336
+ }
337
+ res.sendStatus(401);
338
+ });
339
+ api.post("/userList/:channelID", protect, async (req, res) => {
340
+ const userDetails = getUser(req);
341
+ const channelID = getParam(req, "channelID");
342
+ const channel = await db.retrieveChannel(channelID);
343
+ if (!channel) {
344
+ res.sendStatus(404);
345
+ return;
346
+ }
347
+ const permissions = await db.retrievePermissions(userDetails.userID, "server");
348
+ for (const permission of permissions) {
349
+ if (permission.resourceID === channel.serverID) {
350
+ const groupMembers = await db.retrieveGroupMembers(channelID);
351
+ res.send(msgpack.encode(groupMembers.map((user) => censorUser(user))));
352
+ return;
353
+ }
354
+ }
355
+ res.sendStatus(401);
356
+ });
357
+ api.post("/deviceList", protect, async (req, res) => {
358
+ const parsed = deviceListPayload.safeParse(req.body);
359
+ if (!parsed.success) {
360
+ res.status(400).json({
361
+ error: "Expected array of user ID strings",
362
+ issues: parsed.error.issues,
363
+ });
364
+ return;
365
+ }
366
+ const devices = await db.retrieveUserDeviceList(parsed.data);
367
+ res.send(msgpack.encode(devices));
368
+ });
369
+ api.get("/device/:id", protect, async (req, res) => {
370
+ const device = await db.retrieveDevice(getParam(req, "id"));
98
371
  if (device) {
99
- return res.send(device);
372
+ return res.send(msgpack.encode(device));
100
373
  }
101
374
  else {
375
+ return res.sendStatus(404);
376
+ }
377
+ });
378
+ api.post("/device/:id/keyBundle", protect, async (req, res) => {
379
+ try {
380
+ const keyBundle = await db.getKeyBundle(getParam(req, "id"));
381
+ if (keyBundle) {
382
+ res.send(msgpack.encode(keyBundle));
383
+ }
384
+ else {
385
+ res.sendStatus(404);
386
+ }
387
+ }
388
+ catch {
389
+ res.sendStatus(500);
390
+ }
391
+ });
392
+ api.post("/device/:id/mail", protect, async (req, res) => {
393
+ const deviceDetails = req.device;
394
+ if (!deviceDetails) {
395
+ res.sendStatus(401);
396
+ return;
397
+ }
398
+ const inbox = await db.retrieveMail(deviceDetails.deviceID);
399
+ res.send(msgpack.encode(inbox));
400
+ });
401
+ api.post("/device/:id/connect", protect, async (req, res) => {
402
+ const parsedBody = connectPayload.safeParse(req.body);
403
+ if (!parsedBody.success) {
404
+ res.status(400).json({
405
+ error: "Invalid connect payload",
406
+ issues: parsedBody.error.issues,
407
+ });
408
+ return;
409
+ }
410
+ const { signed } = parsedBody.data;
411
+ const device = await db.retrieveDevice(getParam(req, "id"));
412
+ if (!device) {
102
413
  res.sendStatus(404);
414
+ return;
103
415
  }
104
- }));
416
+ const regKey = xSignOpen(signed, XUtils.decodeHex(device.signKey));
417
+ if (regKey &&
418
+ tokenValidator(uuidStringify(regKey), TokenScopes.Connect)) {
419
+ const token = jwt.sign({ device }, getJwtSecret(), {
420
+ expiresIn: JWT_EXPIRY,
421
+ });
422
+ jwt.verify(token, getJwtSecret());
423
+ res.send(msgpack.encode({ deviceToken: token }));
424
+ }
425
+ else {
426
+ res.sendStatus(401);
427
+ }
428
+ });
429
+ api.get("/device/:id/otk/count", protect, async (req, res) => {
430
+ const deviceDetails = req.device;
431
+ if (!deviceDetails) {
432
+ res.sendStatus(401);
433
+ return;
434
+ }
435
+ const count = await db.getOTKCount(deviceDetails.deviceID);
436
+ res.send(msgpack.encode({ count }));
437
+ });
438
+ api.post("/device/:id/otk", protect, async (req, res) => {
439
+ const parsedOTKs = z.array(PreKeysWSSchema).safeParse(req.body);
440
+ if (!parsedOTKs.success) {
441
+ res.status(400).json({
442
+ error: "Invalid OTK payload",
443
+ issues: parsedOTKs.error.issues,
444
+ });
445
+ return;
446
+ }
447
+ const submittedOTKs = parsedOTKs.data;
448
+ if (submittedOTKs.length === 0) {
449
+ res.sendStatus(200);
450
+ return;
451
+ }
452
+ const userDetails = getUser(req);
453
+ const deviceID = getParam(req, "id");
454
+ const otk = submittedOTKs[0];
455
+ const device = await db.retrieveDevice(deviceID);
456
+ if (!device || !otk) {
457
+ res.sendStatus(404);
458
+ return;
459
+ }
460
+ const message = xSignOpen(otk.signature, XUtils.decodeHex(device.signKey));
461
+ if (!message) {
462
+ res.sendStatus(401);
463
+ return;
464
+ }
465
+ await db.saveOTK(userDetails.userID, deviceID, submittedOTKs);
466
+ res.sendStatus(200);
467
+ });
468
+ api.get("/emoji/:emojiID/details", protect, async (req, res) => {
469
+ const emoji = await db.retrieveEmoji(getParam(req, "emojiID"));
470
+ res.send(msgpack.encode(emoji));
471
+ });
472
+ api.get("/emoji/:emojiID", protect, async (req, res) => {
473
+ const safeId = safePathParam.safeParse(getParam(req, "emojiID"));
474
+ if (!safeId.success) {
475
+ res.sendStatus(400);
476
+ return;
477
+ }
478
+ const filePath = "./emoji/" + safeId.data;
479
+ const typeDetails = await fileTypeFromFile(filePath).catch(() => null);
480
+ if (!typeDetails) {
481
+ res.sendStatus(404);
482
+ return;
483
+ }
484
+ res.set("Content-type", typeDetails.mime);
485
+ res.set("Cache-control", "public, max-age=31536000");
486
+ const stream = fs.createReadStream(filePath);
487
+ stream.on("error", (err) => {
488
+ log.error(err.toString());
489
+ res.sendStatus(500);
490
+ });
491
+ stream.pipe(res);
492
+ });
493
+ api.post("/emoji/:serverID/json", protect, async (req, res) => {
494
+ const parsedPayload = emojiPayload.safeParse(req.body);
495
+ if (!parsedPayload.success) {
496
+ res.status(400).json({
497
+ error: "Invalid emoji payload",
498
+ issues: parsedPayload.error.issues,
499
+ });
500
+ return;
501
+ }
502
+ const payload = parsedPayload.data;
503
+ const userDetails = getUser(req);
504
+ const device = req.device;
505
+ if (!device) {
506
+ res.sendStatus(401);
507
+ return;
508
+ }
509
+ if (!payload.file) {
510
+ res.sendStatus(400);
511
+ return;
512
+ }
513
+ const buf = Buffer.from(XUtils.decodeBase64(payload.file));
514
+ const serverEntry = await db.retrieveServer(getParam(req, "serverID"));
515
+ const permissionList = await db.retrievePermissionsByResourceID(getParam(req, "serverID"));
516
+ if (!userHasPermission(permissionList, userDetails.userID, POWER_LEVELS.EMOJI)) {
517
+ res.sendStatus(401);
518
+ return;
519
+ }
520
+ if (!serverEntry) {
521
+ res.sendStatus(404);
522
+ return;
523
+ }
524
+ if (!payload.name) {
525
+ res.sendStatus(400);
526
+ }
527
+ if (Buffer.byteLength(buf) > 256000) {
528
+ log.warn("File too big.");
529
+ res.sendStatus(413);
530
+ }
531
+ const mimeType = await fileTypeFromBuffer(buf);
532
+ if (!ALLOWED_IMAGE_TYPES.includes(mimeType?.mime || "no/type")) {
533
+ res.status(400).send({
534
+ error: "Unsupported file type. Expected jpeg, png, gif, apng, or avif but received " +
535
+ String(mimeType?.ext),
536
+ });
537
+ return;
538
+ }
539
+ const emoji = {
540
+ emojiID: crypto.randomUUID(),
541
+ name: payload.name,
542
+ owner: getParam(req, "serverID"),
543
+ };
544
+ await db.createEmoji(emoji);
545
+ try {
546
+ // write the file to disk
547
+ await fsp.writeFile("emoji/" + emoji.emojiID, buf);
548
+ log.info("Wrote new emoji " + emoji.emojiID);
549
+ res.send(msgpack.encode(emoji));
550
+ }
551
+ catch (err) {
552
+ log.warn(String(err));
553
+ res.sendStatus(500);
554
+ }
555
+ });
556
+ api.post("/emoji/:serverID", protect, multer().single("emoji"), async (req, res) => {
557
+ const parsedPayload = emojiPayload.safeParse(req.body);
558
+ if (!parsedPayload.success) {
559
+ res.status(400).json({
560
+ error: "Invalid emoji payload",
561
+ issues: parsedPayload.error.issues,
562
+ });
563
+ return;
564
+ }
565
+ const payload = parsedPayload.data;
566
+ const serverID = getParam(req, "serverID");
567
+ if (typeof serverID !== "string") {
568
+ res.sendStatus(400);
569
+ return;
570
+ }
571
+ const serverEntry = await db.retrieveServer(serverID);
572
+ const userDetails = getUser(req);
573
+ const deviceDetails = req.device;
574
+ if (!deviceDetails) {
575
+ res.sendStatus(401);
576
+ return;
577
+ }
578
+ const permissionList = await db.retrievePermissionsByResourceID(serverID);
579
+ if (!userHasPermission(permissionList, userDetails.userID, POWER_LEVELS.EMOJI)) {
580
+ res.sendStatus(401);
581
+ return;
582
+ }
583
+ if (!serverEntry) {
584
+ res.sendStatus(404);
585
+ return;
586
+ }
587
+ if (!payload.name) {
588
+ res.sendStatus(400);
589
+ }
590
+ if (!req.file) {
591
+ log.warn("MISSING FILE");
592
+ res.sendStatus(400);
593
+ return;
594
+ }
595
+ if (Buffer.byteLength(req.file.buffer) > 256000) {
596
+ log.warn("File too big.");
597
+ res.sendStatus(413);
598
+ }
599
+ const mimeType = await fileTypeFromBuffer(req.file.buffer);
600
+ if (!ALLOWED_IMAGE_TYPES.includes(mimeType?.mime || "no/type")) {
601
+ res.status(400).send({
602
+ error: "Unsupported file type. Expected jpeg, png, gif, apng, or avif but received " +
603
+ String(mimeType?.ext),
604
+ });
605
+ return;
606
+ }
607
+ const emoji = {
608
+ emojiID: crypto.randomUUID(),
609
+ name: payload.name,
610
+ owner: serverID,
611
+ };
612
+ await db.createEmoji(emoji);
613
+ try {
614
+ // write the file to disk
615
+ await fsp.writeFile("emoji/" + emoji.emojiID, req.file.buffer);
616
+ log.info("Wrote new emoji " + emoji.emojiID);
617
+ res.send(msgpack.encode(emoji));
618
+ }
619
+ catch (err) {
620
+ log.warn(String(err));
621
+ res.sendStatus(500);
622
+ }
623
+ });
105
624
  // COMPLEX RESOURCES
106
625
  api.use("/user", userRouter);
107
626
  api.use("/file", fileRouter);
108
627
  api.use("/avatar", avatarRouter);
109
628
  api.use("/invite", inviteRouter);
629
+ setupDocs(api);
630
+ // Central error handler MUST be last. Handles both thrown AppErrors
631
+ // (client-safe status + message) and programmer errors (generic 500
632
+ // with full details logged server-side). See src/server/errors.ts
633
+ // for the CWE mapping.
634
+ api.use(errorHandler(log));
110
635
  };
111
- exports.initApp = initApp;
112
636
  /**
113
637
  * @ignore
114
638
  */
115
639
  const jestRun = () => {
116
- return process.env.JEST_WORKER_ID !== undefined;
640
+ return process.env["JEST_WORKER_ID"] !== undefined;
117
641
  };
642
+ //# sourceMappingURL=index.js.map