@vex-chat/spire 1.0.1 → 1.0.3

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 (106) hide show
  1. package/CLA.md +38 -0
  2. package/LICENSE-COMMERCIAL +10 -0
  3. package/LICENSING.md +15 -0
  4. package/README.md +15 -13
  5. package/dist/ClientManager.d.ts +5 -0
  6. package/dist/ClientManager.js +6 -2
  7. package/dist/ClientManager.js.map +1 -1
  8. package/dist/Database.d.ts +29 -1
  9. package/dist/Database.js +206 -64
  10. package/dist/Database.js.map +1 -1
  11. package/dist/Spire.d.ts +5 -0
  12. package/dist/Spire.js +86 -19
  13. package/dist/Spire.js.map +1 -1
  14. package/dist/db/schema.d.ts +5 -0
  15. package/dist/db/schema.js +5 -0
  16. package/dist/db/schema.js.map +1 -1
  17. package/dist/index.d.ts +5 -0
  18. package/dist/index.js +5 -0
  19. package/dist/index.js.map +1 -1
  20. package/dist/middleware/validate.d.ts +5 -0
  21. package/dist/middleware/validate.js +5 -0
  22. package/dist/middleware/validate.js.map +1 -1
  23. package/dist/migrations/2026-04-06_initial-schema.d.ts +5 -0
  24. package/dist/migrations/2026-04-06_initial-schema.js +5 -0
  25. package/dist/migrations/2026-04-06_initial-schema.js.map +1 -1
  26. package/dist/migrations/2026-04-14_argon2id-password-hashing.d.ts +5 -0
  27. package/dist/migrations/2026-04-14_argon2id-password-hashing.js +5 -0
  28. package/dist/migrations/2026-04-14_argon2id-password-hashing.js.map +1 -1
  29. package/dist/run.d.ts +5 -0
  30. package/dist/run.js +5 -0
  31. package/dist/run.js.map +1 -1
  32. package/dist/server/avatar.d.ts +5 -0
  33. package/dist/server/avatar.js +5 -0
  34. package/dist/server/avatar.js.map +1 -1
  35. package/dist/server/errors.d.ts +5 -0
  36. package/dist/server/errors.js +5 -0
  37. package/dist/server/errors.js.map +1 -1
  38. package/dist/server/file.d.ts +5 -0
  39. package/dist/server/file.js +5 -0
  40. package/dist/server/file.js.map +1 -1
  41. package/dist/server/index.d.ts +5 -0
  42. package/dist/server/index.js +20 -2
  43. package/dist/server/index.js.map +1 -1
  44. package/dist/server/invite.d.ts +5 -0
  45. package/dist/server/invite.js +5 -0
  46. package/dist/server/invite.js.map +1 -1
  47. package/dist/server/openapi.d.ts +5 -0
  48. package/dist/server/openapi.js +5 -0
  49. package/dist/server/openapi.js.map +1 -1
  50. package/dist/server/permissions.d.ts +5 -0
  51. package/dist/server/permissions.js +5 -0
  52. package/dist/server/permissions.js.map +1 -1
  53. package/dist/server/rateLimit.d.ts +15 -0
  54. package/dist/server/rateLimit.js +56 -0
  55. package/dist/server/rateLimit.js.map +1 -1
  56. package/dist/server/user.d.ts +5 -0
  57. package/dist/server/user.js +12 -3
  58. package/dist/server/user.js.map +1 -1
  59. package/dist/server/utils.d.ts +5 -0
  60. package/dist/server/utils.js +5 -0
  61. package/dist/server/utils.js.map +1 -1
  62. package/dist/types/express.d.ts +5 -0
  63. package/dist/types/express.js +5 -0
  64. package/dist/types/express.js.map +1 -1
  65. package/dist/utils/createUint8UUID.d.ts +5 -0
  66. package/dist/utils/createUint8UUID.js +5 -0
  67. package/dist/utils/createUint8UUID.js.map +1 -1
  68. package/dist/utils/jwtSecret.d.ts +5 -0
  69. package/dist/utils/jwtSecret.js +5 -0
  70. package/dist/utils/jwtSecret.js.map +1 -1
  71. package/dist/utils/loadEnv.d.ts +5 -0
  72. package/dist/utils/loadEnv.js +5 -0
  73. package/dist/utils/loadEnv.js.map +1 -1
  74. package/dist/utils/msgpack.d.ts +5 -0
  75. package/dist/utils/msgpack.js +5 -0
  76. package/dist/utils/msgpack.js.map +1 -1
  77. package/package.json +18 -8
  78. package/src/ClientManager.ts +7 -2
  79. package/src/Database.ts +231 -68
  80. package/src/Spire.ts +91 -23
  81. package/src/__tests__/Database.spec.ts +6 -0
  82. package/src/ambient-modules.d.ts +6 -0
  83. package/src/db/schema.ts +6 -0
  84. package/src/index.ts +6 -0
  85. package/src/middleware/validate.ts +6 -0
  86. package/src/migrations/2026-04-06_initial-schema.ts +6 -0
  87. package/src/migrations/2026-04-14_argon2id-password-hashing.ts +6 -0
  88. package/src/run.ts +6 -0
  89. package/src/server/avatar.ts +6 -0
  90. package/src/server/errors.ts +6 -0
  91. package/src/server/file.ts +6 -0
  92. package/src/server/index.ts +22 -2
  93. package/src/server/invite.ts +6 -0
  94. package/src/server/openapi.ts +6 -0
  95. package/src/server/permissions.ts +6 -0
  96. package/src/server/rateLimit.ts +43 -3
  97. package/src/server/user.ts +13 -3
  98. package/src/server/utils.ts +6 -0
  99. package/src/types/express.ts +6 -0
  100. package/src/utils/createUint8UUID.ts +6 -0
  101. package/src/utils/jwtSecret.ts +6 -0
  102. package/src/utils/loadEnv.ts +6 -0
  103. package/src/utils/msgpack.ts +6 -0
  104. package/dist/__tests__/Database.spec.d.ts +0 -1
  105. package/dist/__tests__/Database.spec.js +0 -122
  106. package/dist/__tests__/Database.spec.js.map +0 -1
package/src/Spire.ts CHANGED
@@ -1,9 +1,16 @@
1
+ /**
2
+ * Copyright (c) 2020-2026 Vex Heavy Industries LLC
3
+ * Licensed under AGPL-3.0. See LICENSE for details.
4
+ * Commercial licenses available at vex.wtf
5
+ */
6
+
1
7
  import type { ActionToken, BaseMsg, NotifyMsg, User } from "@vex-chat/types";
2
8
  import type { Server } from "http";
3
9
 
4
10
  import { EventEmitter } from "events";
5
11
  import { execSync } from "node:child_process";
6
12
  import * as fs from "node:fs";
13
+ import { freemem, loadavg, totalmem } from "node:os";
7
14
  import path from "node:path";
8
15
  import { fileURLToPath } from "node:url";
9
16
 
@@ -31,7 +38,7 @@ import { z } from "zod/v4";
31
38
  import { ClientManager } from "./ClientManager.ts";
32
39
  import { Database, hashPasswordArgon2, verifyPassword } from "./Database.ts";
33
40
  import { initApp, protect } from "./server/index.ts";
34
- import { authLimiter } from "./server/rateLimit.ts";
41
+ import { authLimiter, devApiKeySkipsRateLimits } from "./server/rateLimit.ts";
35
42
  import { censorUser, getParam, getUser } from "./server/utils.ts";
36
43
  import { getJwtSecret } from "./utils/jwtSecret.ts";
37
44
  import { msgpack } from "./utils/msgpack.ts";
@@ -41,7 +48,6 @@ export const TOKEN_EXPIRY = 1000 * 60 * 10;
41
48
  export const JWT_EXPIRY = "7d";
42
49
  export const DEVICE_AUTH_JWT_EXPIRY = "1h";
43
50
  const DEVICE_CHALLENGE_EXPIRY = 1000 * 60; // 60 seconds
44
- const STATUS_LATENCY_BUDGET_MS = 250;
45
51
 
46
52
  // 3-19 chars long
47
53
  const usernameRegex = /^(\w{3,19})$/;
@@ -190,13 +196,22 @@ export class Spire extends EventEmitter {
190
196
 
191
197
  private async bootstrapRequestCounter(): Promise<void> {
192
198
  const persistedTotal = await this.db.getRequestsTotal();
199
+
200
+ // Between the await above and this synchronous block, requests may
201
+ // have incremented both `requestsTotal` and `queuedRequestIncrements`.
202
+ // Capture the queue, mark loaded, then merge — never overwrite
203
+ // `requestsTotal` (which already includes in-flight increments).
193
204
  const startupIncrements = this.queuedRequestIncrements;
194
205
  this.queuedRequestIncrements = 0;
195
- this.requestsTotal = persistedTotal + startupIncrements;
206
+ this.requestsTotalLoaded = true;
207
+
208
+ // Add the persisted baseline on top of whatever the middleware
209
+ // already counted in-memory, instead of overwriting it.
210
+ this.requestsTotal += persistedTotal;
211
+
196
212
  if (startupIncrements > 0) {
197
213
  await this.db.incrementRequestsTotal(startupIncrements);
198
214
  }
199
- this.requestsTotalLoaded = true;
200
215
  }
201
216
 
202
217
  private createActionToken(scope: TokenScopes): ActionToken {
@@ -210,8 +225,9 @@ export class Spire extends EventEmitter {
210
225
  }
211
226
 
212
227
  private deleteActionToken(key: ActionToken) {
213
- if (this.actionTokens.includes(key)) {
214
- this.actionTokens.splice(this.actionTokens.indexOf(key), 1);
228
+ const idx = this.actionTokens.indexOf(key);
229
+ if (idx !== -1) {
230
+ this.actionTokens.splice(idx, 1);
215
231
  }
216
232
  }
217
233
 
@@ -286,11 +302,9 @@ export class Spire extends EventEmitter {
286
302
  );
287
303
 
288
304
  client.on("fail", () => {
289
- if (this.clients.includes(client)) {
290
- this.clients.splice(
291
- this.clients.indexOf(client),
292
- 1,
293
- );
305
+ const idx = this.clients.indexOf(client);
306
+ if (idx !== -1) {
307
+ this.clients.splice(idx, 1);
294
308
  }
295
309
  });
296
310
 
@@ -431,23 +445,74 @@ export class Spire extends EventEmitter {
431
445
  canaryEnv === "true" ||
432
446
  canaryEnv === "yes",
433
447
  checkDurationMs,
434
- commitSha: this.commitSha,
435
- dbHealthy,
436
- dbReady: this.dbReady,
437
- latencyBudgetMs: STATUS_LATENCY_BUDGET_MS,
438
- metrics: {
439
- requestsTotal: this.requestsTotal,
440
- },
441
448
  now: new Date(),
442
449
  ok,
443
- startedAt: this.startedAt.toISOString(),
444
- uptimeSeconds: Math.floor(process.uptime()),
445
450
  version: this.version,
446
- withinLatencyBudget:
447
- checkDurationMs <= STATUS_LATENCY_BUDGET_MS,
448
451
  });
449
452
  });
450
453
 
454
+ /**
455
+ * Dev-only process snapshot (same gate as rate-limit bypass: `DEV_API_KEY`
456
+ * env + `x-dev-api-key` header). Returns 404 when not enabled or key wrong
457
+ * so the route is not advertised to anonymous callers.
458
+ * Lets local stress / `sample <pid>` workflows see RSS, WS count, etc.
459
+ */
460
+ this.api.get("/status/process", (req, res) => {
461
+ if (!devApiKeySkipsRateLimits(req)) {
462
+ res.sendStatus(404);
463
+ return;
464
+ }
465
+ const mu = process.memoryUsage();
466
+ const ru = process.resourceUsage();
467
+ res.json({
468
+ activeRequestsApprox: this.requestsTotal,
469
+ dbReady: this.dbReady,
470
+ hostOs: {
471
+ freemem: freemem(),
472
+ loadavg: loadavg(),
473
+ totalmem: totalmem(),
474
+ },
475
+ memory: {
476
+ arrayBuffers: mu.arrayBuffers,
477
+ external: mu.external,
478
+ heapTotal: mu.heapTotal,
479
+ heapUsed: mu.heapUsed,
480
+ rss: mu.rss,
481
+ },
482
+ pid: process.pid,
483
+ resourceUsage: {
484
+ fsRead: ru.fsRead,
485
+ fsWrite: ru.fsWrite,
486
+ maxRSS: ru.maxRSS,
487
+ systemMicros: ru.systemCPUTime,
488
+ userMicros: ru.userCPUTime,
489
+ },
490
+ uptimeSeconds: Math.floor(process.uptime()),
491
+ websocketClients: this.wss.clients.size,
492
+ });
493
+ });
494
+
495
+ /**
496
+ * Dev-only SQLite file + pragma snapshot (same gate as `/status/process`).
497
+ */
498
+ this.api.get("/status/sqlite", (req, res) => {
499
+ if (!devApiKeySkipsRateLimits(req)) {
500
+ res.sendStatus(404);
501
+ return;
502
+ }
503
+ if (!this.dbReady) {
504
+ res.status(503).json({ dbReady: false, ok: false });
505
+ return;
506
+ }
507
+ try {
508
+ const sqlite = this.db.getDevSqliteMonitor();
509
+ res.json({ ok: true, sqlite });
510
+ } catch (err: unknown) {
511
+ const msg = err instanceof Error ? err.message : String(err);
512
+ res.status(500).json({ error: msg, ok: false });
513
+ }
514
+ });
515
+
451
516
  this.api.post("/goodbye", protect, (req, res) => {
452
517
  jwt.sign({ user: req.user }, getJwtSecret(), { expiresIn: -1 });
453
518
  res.sendStatus(200);
@@ -763,7 +828,10 @@ export class Spire extends EventEmitter {
763
828
  data?: unknown,
764
829
  deviceID?: string,
765
830
  ): void {
766
- for (const client of this.clients) {
831
+ // Snapshot the array so that a synchronous `fail` → splice inside
832
+ // client.send() doesn't corrupt the iteration.
833
+ const snapshot = this.clients.slice();
834
+ for (const client of snapshot) {
767
835
  if (deviceID) {
768
836
  if (client.getDevice().deviceID === deviceID) {
769
837
  const msg: NotifyMsg = {
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Copyright (c) 2020-2026 Vex Heavy Industries LLC
3
+ * Licensed under AGPL-3.0. See LICENSE for details.
4
+ * Commercial licenses available at vex.wtf
5
+ */
6
+
1
7
  import type { SpireOptions } from "../Spire.ts";
2
8
  import type { PreKeysWS } from "@vex-chat/types";
3
9
 
@@ -1 +1,7 @@
1
+ /**
2
+ * Copyright (c) 2020-2026 Vex Heavy Industries LLC
3
+ * Licensed under AGPL-3.0. See LICENSE for details.
4
+ * Commercial licenses available at vex.wtf
5
+ */
6
+
1
7
  declare module "ed2curve";
package/src/db/schema.ts CHANGED
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Copyright (c) 2020-2026 Vex Heavy Industries LLC
3
+ * Licensed under AGPL-3.0. See LICENSE for details.
4
+ * Commercial licenses available at vex.wtf
5
+ */
6
+
1
7
  import type { Insertable, Selectable, Updateable } from "kysely";
2
8
 
3
9
  // ── Table interfaces ────────────────────────────────────────────────────
package/src/index.ts CHANGED
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Copyright (c) 2020-2026 Vex Heavy Industries LLC
3
+ * Licensed under AGPL-3.0. See LICENSE for details.
4
+ * Commercial licenses available at vex.wtf
5
+ */
6
+
1
7
  import { Spire } from "./Spire.ts";
2
8
 
3
9
  export { Spire };
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Copyright (c) 2020-2026 Vex Heavy Industries LLC
3
+ * Licensed under AGPL-3.0. See LICENSE for details.
4
+ * Commercial licenses available at vex.wtf
5
+ */
6
+
1
7
  import type { NextFunction, Request, Response } from "express";
2
8
  import type { z } from "zod/v4";
3
9
 
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Copyright (c) 2020-2026 Vex Heavy Industries LLC
3
+ * Licensed under AGPL-3.0. See LICENSE for details.
4
+ * Commercial licenses available at vex.wtf
5
+ */
6
+
1
7
  import { type Kysely, sql } from "kysely";
2
8
 
3
9
  export async function down(db: Kysely<unknown>): Promise<void> {
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Copyright (c) 2020-2026 Vex Heavy Industries LLC
3
+ * Licensed under AGPL-3.0. See LICENSE for details.
4
+ * Commercial licenses available at vex.wtf
5
+ */
6
+
1
7
  import { type Kysely, sql } from "kysely";
2
8
 
3
9
  export async function down(db: Kysely<unknown>): Promise<void> {
package/src/run.ts CHANGED
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Copyright (c) 2020-2026 Vex Heavy Industries LLC
3
+ * Licensed under AGPL-3.0. See LICENSE for details.
4
+ * Commercial licenses available at vex.wtf
5
+ */
6
+
1
7
  import type { SpireOptions } from "./Spire.ts";
2
8
 
3
9
  import { Spire } from "./Spire.ts";
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Copyright (c) 2020-2026 Vex Heavy Industries LLC
3
+ * Licensed under AGPL-3.0. See LICENSE for details.
4
+ * Commercial licenses available at vex.wtf
5
+ */
6
+
1
7
  import * as fs from "node:fs";
2
8
  import * as fsp from "node:fs/promises";
3
9
 
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Copyright (c) 2020-2026 Vex Heavy Industries LLC
3
+ * Licensed under AGPL-3.0. See LICENSE for details.
4
+ * Commercial licenses available at vex.wtf
5
+ */
6
+
1
7
  /**
2
8
  * Centralized HTTP error handling.
3
9
  *
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Copyright (c) 2020-2026 Vex Heavy Industries LLC
3
+ * Licensed under AGPL-3.0. See LICENSE for details.
4
+ * Commercial licenses available at vex.wtf
5
+ */
6
+
1
7
  import type { Database } from "../Database.ts";
2
8
  import type { FileSQL } from "@vex-chat/types";
3
9
 
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Copyright (c) 2020-2026 Vex Heavy Industries LLC
3
+ * Licensed under AGPL-3.0. See LICENSE for details.
4
+ * Commercial licenses available at vex.wtf
5
+ */
6
+
1
7
  import type { Database } from "../Database.ts";
2
8
  import type { Emoji } from "@vex-chat/types";
3
9
 
@@ -197,11 +203,25 @@ export const initApp = (
197
203
  api.use(checkAuth);
198
204
  api.use(checkDevice);
199
205
 
200
- const allowedOrigins = process.env["CORS_ORIGINS"]?.split(",") ?? [];
206
+ // Browser clients (web, Tauri, Capacitor, etc.) hit Spire from arbitrary
207
+ // origins when self-hosted or embedded in third-party apps. libvex uses
208
+ // `Authorization: Bearer` only (no cookies), so reflecting `Origin` is the
209
+ // usual bearer-API pattern. Set `CORS_ORIGINS` to a comma-separated allowlist
210
+ // when an operator wants to restrict which frontends may call the API.
211
+ const corsRaw = process.env["CORS_ORIGINS"];
212
+ const corsOrigins = corsRaw
213
+ ? corsRaw
214
+ .split(",")
215
+ .map((o) => o.trim())
216
+ .filter((o) => o.length > 0)
217
+ : [];
201
218
  api.use(
202
219
  cors({
203
220
  credentials: true,
204
- origin: allowedOrigins.length > 0 ? allowedOrigins : false,
221
+ origin:
222
+ corsOrigins.length > 0
223
+ ? corsOrigins
224
+ : true /* reflect request Origin */,
205
225
  }),
206
226
  );
207
227
 
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Copyright (c) 2020-2026 Vex Heavy Industries LLC
3
+ * Licensed under AGPL-3.0. See LICENSE for details.
4
+ * Commercial licenses available at vex.wtf
5
+ */
6
+
1
7
  import type { Database } from "../Database.ts";
2
8
  import type { TokenScopes } from "@vex-chat/types";
3
9
 
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Copyright (c) 2020-2026 Vex Heavy Industries LLC
3
+ * Licensed under AGPL-3.0. See LICENSE for details.
4
+ * Commercial licenses available at vex.wtf
5
+ */
6
+
1
7
  /**
2
8
  * API documentation endpoints (development only).
3
9
  *
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Copyright (c) 2020-2026 Vex Heavy Industries LLC
3
+ * Licensed under AGPL-3.0. See LICENSE for details.
4
+ * Commercial licenses available at vex.wtf
5
+ */
6
+
1
7
  import type { Permission } from "@vex-chat/types";
2
8
 
3
9
  /**
@@ -1,3 +1,43 @@
1
+ /**
2
+ * Copyright (c) 2020-2026 Vex Heavy Industries LLC
3
+ * Licensed under AGPL-3.0. See LICENSE for details.
4
+ * Commercial licenses available at vex.wtf
5
+ */
6
+
7
+ import type { Request } from "express";
8
+
9
+ import { timingSafeEqual } from "node:crypto";
10
+
11
+ import rateLimit, { ipKeyGenerator } from "express-rate-limit";
12
+
13
+ /** HTTP header carrying the dev API key (must match {@link process.env.DEV_API_KEY}). */
14
+ export const DEV_API_KEY_HEADER = "x-dev-api-key";
15
+
16
+ /**
17
+ * When `DEV_API_KEY` is set in the environment, any request whose
18
+ * `x-dev-api-key` header matches (constant-time) skips all in-process rate
19
+ * limiters. Dev / load-testing escape hatch only — never set in production.
20
+ * (Future: first-class API keys with scopes may reuse this header name.)
21
+ */
22
+ export function devApiKeySkipsRateLimits(req: Request): boolean {
23
+ const configured = process.env["DEV_API_KEY"]?.trim() ?? "";
24
+ if (configured.length === 0) {
25
+ return false;
26
+ }
27
+ const presented = req.get(DEV_API_KEY_HEADER);
28
+ if (!presented || presented.length !== configured.length) {
29
+ return false;
30
+ }
31
+ try {
32
+ return timingSafeEqual(
33
+ Buffer.from(presented, "utf8"),
34
+ Buffer.from(configured, "utf8"),
35
+ );
36
+ } catch {
37
+ return false;
38
+ }
39
+ }
40
+
1
41
  /**
2
42
  * Rate limiting middleware.
3
43
  *
@@ -21,9 +61,6 @@
21
61
  * `trust proxy` must be set on the Express app (see Spire.ts) so
22
62
  * `req.ip` returns the real client address, not the immediate proxy.
23
63
  */
24
- import type { Request } from "express";
25
-
26
- import rateLimit, { ipKeyGenerator } from "express-rate-limit";
27
64
 
28
65
  /**
29
66
  * Bucket requests by the real client IP, IPv6-safe.
@@ -46,6 +83,7 @@ export const globalLimiter = rateLimit({
46
83
  keyGenerator: keyByIp,
47
84
  legacyHeaders: false,
48
85
  limit: 3000,
86
+ skip: devApiKeySkipsRateLimits,
49
87
  standardHeaders: "draft-7",
50
88
  windowMs: 15 * 60 * 1000,
51
89
  });
@@ -63,6 +101,7 @@ export const authLimiter = rateLimit({
63
101
  keyGenerator: keyByIp,
64
102
  legacyHeaders: false,
65
103
  limit: 50,
104
+ skip: devApiKeySkipsRateLimits,
66
105
  skipSuccessfulRequests: true,
67
106
  standardHeaders: "draft-7",
68
107
  windowMs: 15 * 60 * 1000,
@@ -81,6 +120,7 @@ export const uploadLimiter = rateLimit({
81
120
  keyGenerator: keyByIp,
82
121
  legacyHeaders: false,
83
122
  limit: 200,
123
+ skip: devApiKeySkipsRateLimits,
84
124
  standardHeaders: "draft-7",
85
125
  windowMs: 60 * 1000,
86
126
  });
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Copyright (c) 2020-2026 Vex Heavy Industries LLC
3
+ * Licensed under AGPL-3.0. See LICENSE for details.
4
+ * Commercial licenses available at vex.wtf
5
+ */
6
+
1
7
  import type { Database } from "../Database.ts";
2
8
 
3
9
  import express from "express";
@@ -31,9 +37,13 @@ export const getUserRouter = (
31
37
  });
32
38
 
33
39
  router.get("/:id/devices", protect, async (req, res) => {
34
- const deviceList = await db.retrieveUserDeviceList([
35
- getParam(req, "id"),
36
- ]);
40
+ const id = getParam(req, "id");
41
+ const user = await db.retrieveUser(id);
42
+ if (!user) {
43
+ res.sendStatus(404);
44
+ return;
45
+ }
46
+ const deviceList = await db.retrieveUserDeviceList([id]);
37
47
  return res.send(msgpack.encode(deviceList));
38
48
  });
39
49
 
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Copyright (c) 2020-2026 Vex Heavy Industries LLC
3
+ * Licensed under AGPL-3.0. See LICENSE for details.
4
+ * Commercial licenses available at vex.wtf
5
+ */
6
+
1
7
  import type { Device, User, UserRecord } from "@vex-chat/types";
2
8
  import type { Request } from "express";
3
9
 
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Copyright (c) 2020-2026 Vex Heavy Industries LLC
3
+ * Licensed under AGPL-3.0. See LICENSE for details.
4
+ * Commercial licenses available at vex.wtf
5
+ */
6
+
1
7
  import type { Device, User } from "@vex-chat/types";
2
8
 
3
9
  /**
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Copyright (c) 2020-2026 Vex Heavy Industries LLC
3
+ * Licensed under AGPL-3.0. See LICENSE for details.
4
+ * Commercial licenses available at vex.wtf
5
+ */
6
+
1
7
  import { parse as uuidParse } from "uuid";
2
8
 
3
9
  export function createUint8UUID(): Uint8Array {
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Copyright (c) 2020-2026 Vex Heavy Industries LLC
3
+ * Licensed under AGPL-3.0. See LICENSE for details.
4
+ * Commercial licenses available at vex.wtf
5
+ */
6
+
1
7
  /**
2
8
  * Returns the dedicated JWT HMAC signing secret.
3
9
  *
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Copyright (c) 2020-2026 Vex Heavy Industries LLC
3
+ * Licensed under AGPL-3.0. See LICENSE for details.
4
+ * Commercial licenses available at vex.wtf
5
+ */
6
+
1
7
  import { config } from "dotenv";
2
8
 
3
9
  /* Populate process.env with vars from .env and verify required vars are present. */
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Copyright (c) 2020-2026 Vex Heavy Industries LLC
3
+ * Licensed under AGPL-3.0. See LICENSE for details.
4
+ * Commercial licenses available at vex.wtf
5
+ */
6
+
1
7
  import { Packr } from "msgpackr";
2
8
 
3
9
  // Standard msgpack encoder/decoder configured for broad compatibility.
@@ -1 +0,0 @@
1
- export {};
@@ -1,122 +0,0 @@
1
- import { XUtils } from "@vex-chat/crypto";
2
- import * as uuid from "uuid";
3
- import { describe, expect, it, vi } from "vitest";
4
- import { Database } from "../Database.js";
5
- // vi.mock is hoisted above all imports automatically.
6
- // Minimal stubs for uuid functions used by spire src: v4, parse, stringify.
7
- vi.mock("uuid", () => ({
8
- parse: (s) => {
9
- const matches = s.replace(/-/g, "").match(/.{2}/g);
10
- if (!matches)
11
- throw new Error("Invalid UUID");
12
- return Uint8Array.from(matches.map((b) => parseInt(b, 16)));
13
- },
14
- stringify: (b) => {
15
- const hex = Array.from(b)
16
- .map((x) => x.toString(16).padStart(2, "0"))
17
- .join("");
18
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
19
- },
20
- v4: vi.fn(() => "93ce482b-a0f2-4f6e-b1df-3aed61073552"),
21
- validate: () => true,
22
- }));
23
- describe("Database", () => {
24
- // Reusable test data
25
- const keyID = "de459e05-aa63-4dfa-97b4-ed43d5c7a5f7";
26
- const userID = "4e67b90f-cbf8-44bc-8ce3-d3b248f033f1";
27
- const deviceID = "23cb0b27-7d0c-43b2-87e1-c2b93e0095e5";
28
- const publicKey = XUtils.decodeHex("30c2d0294c1cfdbb73c6b3bbe6010088c2dba8384b04ff2e2b92172431d66b5e");
29
- const signature = XUtils.decodeHex("dd0665079426c3efcf4dce9b1487e4aca132f8147581b3294c3f23ddd2b4ba8240a10082bd06805d7eb320d91af971da3306e11b60073ccc3d829710f5036004000030c2d0294c1cfdbb73c6b3bbe6010088c2dba8384b04ff2e2b92172431d66b5e");
30
- const testSQLPreKey = {
31
- deviceID,
32
- index: 1,
33
- keyID,
34
- publicKey: "30c2d0294c1cfdbb73c6b3bbe6010088c2dba8384b04ff2e2b92172431d66b5e",
35
- signature: "dd0665079426c3efcf4dce9b1487e4aca132f8147581b3294c3f23ddd2b4ba8240a10082bd06805d7eb320d91af971da3306e11b60073ccc3d829710f5036004000030c2d0294c1cfdbb73c6b3bbe6010088c2dba8384b04ff2e2b92172431d66b5e",
36
- userID,
37
- };
38
- const testWSPreKey = {
39
- deviceID,
40
- index: 1,
41
- publicKey,
42
- signature,
43
- };
44
- const options = {
45
- dbType: "sqlite3mem",
46
- };
47
- describe("saveOTK", () => {
48
- it("takes a userId and one time key, adds a keyId and saves it to oneTimeKey table", async () => {
49
- expect.assertions(1);
50
- vi.mocked(uuid.v4).mockReturnValueOnce(keyID);
51
- const provider = new Database(options);
52
- await new Promise((resolve, reject) => {
53
- provider.once("ready", () => {
54
- void (async () => {
55
- try {
56
- await provider.saveOTK(testSQLPreKey.userID, testSQLPreKey.deviceID, [
57
- {
58
- deviceID,
59
- index: 1,
60
- publicKey,
61
- signature,
62
- },
63
- ]);
64
- const oneTimeKey = await provider.getOTK(deviceID);
65
- expect(oneTimeKey).toEqual(testWSPreKey);
66
- await provider.close();
67
- resolve();
68
- }
69
- catch (e) {
70
- reject(e instanceof Error ? e : new Error(String(e)));
71
- }
72
- })();
73
- });
74
- });
75
- });
76
- });
77
- describe("getPreKeys", () => {
78
- it("returns a preKey by deviceID if said preKey exists.", async () => {
79
- expect.assertions(1);
80
- const provider = new Database(options);
81
- await new Promise((resolve, reject) => {
82
- provider.once("ready", () => {
83
- void (async () => {
84
- try {
85
- await provider["db"]
86
- .insertInto("preKeys")
87
- .values(testSQLPreKey)
88
- .execute();
89
- const result = await provider.getPreKeys(deviceID);
90
- expect(result).toEqual(testWSPreKey);
91
- await provider.close();
92
- resolve();
93
- }
94
- catch (e) {
95
- reject(e instanceof Error ? e : new Error(String(e)));
96
- }
97
- })();
98
- });
99
- });
100
- });
101
- it("return null if there are no preKeys with deviceID param", async () => {
102
- expect.assertions(1);
103
- const provider = new Database(options);
104
- await new Promise((resolve, reject) => {
105
- provider.once("ready", () => {
106
- void (async () => {
107
- try {
108
- const result = await provider.getPreKeys(deviceID);
109
- expect(result).toBeNull();
110
- await provider.close();
111
- resolve();
112
- }
113
- catch (e) {
114
- reject(e instanceof Error ? e : new Error(String(e)));
115
- }
116
- })();
117
- });
118
- });
119
- });
120
- });
121
- });
122
- //# sourceMappingURL=Database.spec.js.map