@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.
- package/CLA.md +38 -0
- package/LICENSE-COMMERCIAL +10 -0
- package/LICENSING.md +15 -0
- package/README.md +15 -13
- package/dist/ClientManager.d.ts +5 -0
- package/dist/ClientManager.js +6 -2
- package/dist/ClientManager.js.map +1 -1
- package/dist/Database.d.ts +29 -1
- package/dist/Database.js +206 -64
- package/dist/Database.js.map +1 -1
- package/dist/Spire.d.ts +5 -0
- package/dist/Spire.js +86 -19
- package/dist/Spire.js.map +1 -1
- package/dist/db/schema.d.ts +5 -0
- package/dist/db/schema.js +5 -0
- package/dist/db/schema.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/middleware/validate.d.ts +5 -0
- package/dist/middleware/validate.js +5 -0
- package/dist/middleware/validate.js.map +1 -1
- package/dist/migrations/2026-04-06_initial-schema.d.ts +5 -0
- package/dist/migrations/2026-04-06_initial-schema.js +5 -0
- package/dist/migrations/2026-04-06_initial-schema.js.map +1 -1
- package/dist/migrations/2026-04-14_argon2id-password-hashing.d.ts +5 -0
- package/dist/migrations/2026-04-14_argon2id-password-hashing.js +5 -0
- package/dist/migrations/2026-04-14_argon2id-password-hashing.js.map +1 -1
- package/dist/run.d.ts +5 -0
- package/dist/run.js +5 -0
- package/dist/run.js.map +1 -1
- package/dist/server/avatar.d.ts +5 -0
- package/dist/server/avatar.js +5 -0
- package/dist/server/avatar.js.map +1 -1
- package/dist/server/errors.d.ts +5 -0
- package/dist/server/errors.js +5 -0
- package/dist/server/errors.js.map +1 -1
- package/dist/server/file.d.ts +5 -0
- package/dist/server/file.js +5 -0
- package/dist/server/file.js.map +1 -1
- package/dist/server/index.d.ts +5 -0
- package/dist/server/index.js +20 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/invite.d.ts +5 -0
- package/dist/server/invite.js +5 -0
- package/dist/server/invite.js.map +1 -1
- package/dist/server/openapi.d.ts +5 -0
- package/dist/server/openapi.js +5 -0
- package/dist/server/openapi.js.map +1 -1
- package/dist/server/permissions.d.ts +5 -0
- package/dist/server/permissions.js +5 -0
- package/dist/server/permissions.js.map +1 -1
- package/dist/server/rateLimit.d.ts +15 -0
- package/dist/server/rateLimit.js +56 -0
- package/dist/server/rateLimit.js.map +1 -1
- package/dist/server/user.d.ts +5 -0
- package/dist/server/user.js +12 -3
- package/dist/server/user.js.map +1 -1
- package/dist/server/utils.d.ts +5 -0
- package/dist/server/utils.js +5 -0
- package/dist/server/utils.js.map +1 -1
- package/dist/types/express.d.ts +5 -0
- package/dist/types/express.js +5 -0
- package/dist/types/express.js.map +1 -1
- package/dist/utils/createUint8UUID.d.ts +5 -0
- package/dist/utils/createUint8UUID.js +5 -0
- package/dist/utils/createUint8UUID.js.map +1 -1
- package/dist/utils/jwtSecret.d.ts +5 -0
- package/dist/utils/jwtSecret.js +5 -0
- package/dist/utils/jwtSecret.js.map +1 -1
- package/dist/utils/loadEnv.d.ts +5 -0
- package/dist/utils/loadEnv.js +5 -0
- package/dist/utils/loadEnv.js.map +1 -1
- package/dist/utils/msgpack.d.ts +5 -0
- package/dist/utils/msgpack.js +5 -0
- package/dist/utils/msgpack.js.map +1 -1
- package/package.json +18 -8
- package/src/ClientManager.ts +7 -2
- package/src/Database.ts +231 -68
- package/src/Spire.ts +91 -23
- package/src/__tests__/Database.spec.ts +6 -0
- package/src/ambient-modules.d.ts +6 -0
- package/src/db/schema.ts +6 -0
- package/src/index.ts +6 -0
- package/src/middleware/validate.ts +6 -0
- package/src/migrations/2026-04-06_initial-schema.ts +6 -0
- package/src/migrations/2026-04-14_argon2id-password-hashing.ts +6 -0
- package/src/run.ts +6 -0
- package/src/server/avatar.ts +6 -0
- package/src/server/errors.ts +6 -0
- package/src/server/file.ts +6 -0
- package/src/server/index.ts +22 -2
- package/src/server/invite.ts +6 -0
- package/src/server/openapi.ts +6 -0
- package/src/server/permissions.ts +6 -0
- package/src/server/rateLimit.ts +43 -3
- package/src/server/user.ts +13 -3
- package/src/server/utils.ts +6 -0
- package/src/types/express.ts +6 -0
- package/src/utils/createUint8UUID.ts +6 -0
- package/src/utils/jwtSecret.ts +6 -0
- package/src/utils/loadEnv.ts +6 -0
- package/src/utils/msgpack.ts +6 -0
- package/dist/__tests__/Database.spec.d.ts +0 -1
- package/dist/__tests__/Database.spec.js +0 -122
- 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.
|
|
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
|
-
|
|
214
|
-
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
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
|
|
package/src/ambient-modules.d.ts
CHANGED
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 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
package/src/server/avatar.ts
CHANGED
package/src/server/errors.ts
CHANGED
package/src/server/file.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 { Database } from "../Database.ts";
|
|
2
8
|
import type { FileSQL } from "@vex-chat/types";
|
|
3
9
|
|
package/src/server/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 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
|
-
|
|
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:
|
|
221
|
+
origin:
|
|
222
|
+
corsOrigins.length > 0
|
|
223
|
+
? corsOrigins
|
|
224
|
+
: true /* reflect request Origin */,
|
|
205
225
|
}),
|
|
206
226
|
);
|
|
207
227
|
|
package/src/server/invite.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 { Database } from "../Database.ts";
|
|
2
8
|
import type { TokenScopes } from "@vex-chat/types";
|
|
3
9
|
|
package/src/server/openapi.ts
CHANGED
package/src/server/rateLimit.ts
CHANGED
|
@@ -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
|
});
|
package/src/server/user.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 { 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
|
|
35
|
-
|
|
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
|
|
package/src/server/utils.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 { Device, User, UserRecord } from "@vex-chat/types";
|
|
2
8
|
import type { Request } from "express";
|
|
3
9
|
|
package/src/types/express.ts
CHANGED
package/src/utils/jwtSecret.ts
CHANGED
package/src/utils/loadEnv.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 { config } from "dotenv";
|
|
2
8
|
|
|
3
9
|
/* Populate process.env with vars from .env and verify required vars are present. */
|
package/src/utils/msgpack.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 { 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
|