@vex-chat/spire 1.0.0 → 1.0.2
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 +38 -38
- package/dist/ClientManager.d.ts +1 -3
- package/dist/ClientManager.js +7 -50
- package/dist/ClientManager.js.map +1 -1
- package/dist/Database.d.ts +49 -6
- package/dist/Database.js +255 -80
- package/dist/Database.js.map +1 -1
- package/dist/Spire.d.ts +0 -3
- package/dist/Spire.js +116 -81
- package/dist/Spire.js.map +1 -1
- package/dist/db/schema.d.ts +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 +0 -1
- package/dist/run.js.map +1 -1
- package/dist/server/avatar.d.ts +1 -3
- package/dist/server/avatar.js +7 -11
- package/dist/server/avatar.js.map +1 -1
- package/dist/server/errors.d.ts +3 -6
- package/dist/server/errors.js +2 -28
- package/dist/server/errors.js.map +1 -1
- package/dist/server/file.d.ts +1 -2
- package/dist/server/file.js +5 -7
- package/dist/server/file.js.map +1 -1
- package/dist/server/index.d.ts +1 -2
- package/dist/server/index.js +35 -33
- package/dist/server/index.js.map +1 -1
- package/dist/server/invite.d.ts +1 -2
- package/dist/server/invite.js +1 -1
- package/dist/server/invite.js.map +1 -1
- package/dist/server/rateLimit.d.ts +13 -3
- package/dist/server/rateLimit.js +57 -6
- package/dist/server/rateLimit.js.map +1 -1
- package/dist/server/user.d.ts +1 -2
- package/dist/server/user.js +10 -8
- package/dist/server/user.js.map +1 -1
- package/dist/utils/jwtSecret.d.ts +3 -3
- package/dist/utils/jwtSecret.js +5 -6
- package/dist/utils/jwtSecret.js.map +1 -1
- package/dist/utils/loadEnv.js +1 -1
- package/dist/utils/loadEnv.js.map +1 -1
- package/package.json +17 -8
- package/src/ClientManager.ts +7 -60
- package/src/Database.ts +308 -89
- package/src/Spire.ts +122 -119
- package/src/__tests__/Database.spec.ts +0 -17
- package/src/db/schema.ts +1 -0
- package/src/migrations/2026-04-14_argon2id-password-hashing.ts +18 -0
- package/src/run.ts +0 -1
- package/src/server/avatar.ts +7 -14
- package/src/server/errors.ts +3 -32
- package/src/server/file.ts +5 -10
- package/src/server/index.ts +37 -37
- package/src/server/invite.ts +0 -2
- package/src/server/rateLimit.ts +43 -9
- package/src/server/user.ts +9 -9
- package/src/utils/jwtSecret.ts +5 -6
- package/src/utils/loadEnv.ts +1 -1
- package/dist/__tests__/Database.spec.d.ts +0 -1
- package/dist/__tests__/Database.spec.js +0 -136
- package/dist/__tests__/Database.spec.js.map +0 -1
- package/dist/utils/createLogger.d.ts +0 -5
- package/dist/utils/createLogger.js +0 -35
- package/dist/utils/createLogger.js.map +0 -1
- package/src/utils/createLogger.ts +0 -47
package/dist/server/rateLimit.js
CHANGED
|
@@ -1,4 +1,52 @@
|
|
|
1
|
+
import { timingSafeEqual } from "node:crypto";
|
|
1
2
|
import rateLimit, { ipKeyGenerator } from "express-rate-limit";
|
|
3
|
+
/** HTTP header carrying the dev API key (must match {@link process.env.DEV_API_KEY}). */
|
|
4
|
+
export const DEV_API_KEY_HEADER = "x-dev-api-key";
|
|
5
|
+
/**
|
|
6
|
+
* When `DEV_API_KEY` is set in the environment, any request whose
|
|
7
|
+
* `x-dev-api-key` header matches (constant-time) skips all in-process rate
|
|
8
|
+
* limiters. Dev / load-testing escape hatch only — never set in production.
|
|
9
|
+
* (Future: first-class API keys with scopes may reuse this header name.)
|
|
10
|
+
*/
|
|
11
|
+
export function devApiKeySkipsRateLimits(req) {
|
|
12
|
+
const configured = process.env["DEV_API_KEY"]?.trim() ?? "";
|
|
13
|
+
if (configured.length === 0) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
const presented = req.get(DEV_API_KEY_HEADER);
|
|
17
|
+
if (!presented || presented.length !== configured.length) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
return timingSafeEqual(Buffer.from(presented, "utf8"), Buffer.from(configured, "utf8"));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Rate limiting middleware.
|
|
29
|
+
*
|
|
30
|
+
* Three tiers matching CWE-307 (brute-force auth), CWE-400 / CWE-770
|
|
31
|
+
* (unrestricted resource consumption), and OWASP API4:2023:
|
|
32
|
+
*
|
|
33
|
+
* - `globalLimiter` — baseline per-IP limit across every route. Wide
|
|
34
|
+
* enough to not bother normal clients, tight enough to shield the
|
|
35
|
+
* server from a single-host flood.
|
|
36
|
+
* - `authLimiter` — strict per-IP limit on auth endpoints (register,
|
|
37
|
+
* login, device challenge). `skipSuccessfulRequests` means only the
|
|
38
|
+
* failed attempts count, so a correct login doesn't eat the budget.
|
|
39
|
+
* - `uploadLimiter` — upload-specific limit applied before multer,
|
|
40
|
+
* so multer never even parses a request that's over quota.
|
|
41
|
+
*
|
|
42
|
+
* All three use `ipKeyGenerator` from `express-rate-limit@7.4+` to
|
|
43
|
+
* bucket IPv4 and IPv4-mapped IPv6 correctly (CVE-2026-30827 — older
|
|
44
|
+
* versions silently collapsed all IPv4-mapped IPv6 addresses into
|
|
45
|
+
* one bucket, which let attackers bypass the limiter).
|
|
46
|
+
*
|
|
47
|
+
* `trust proxy` must be set on the Express app (see Spire.ts) so
|
|
48
|
+
* `req.ip` returns the real client address, not the immediate proxy.
|
|
49
|
+
*/
|
|
2
50
|
/**
|
|
3
51
|
* Bucket requests by the real client IP, IPv6-safe.
|
|
4
52
|
*
|
|
@@ -11,14 +59,15 @@ const keyByIp = (req) => ipKeyGenerator(req.ip ?? "");
|
|
|
11
59
|
/**
|
|
12
60
|
* Global per-IP limiter. Applied app-wide via `api.use(globalLimiter)`.
|
|
13
61
|
*
|
|
14
|
-
*
|
|
62
|
+
* 3000 requests per 15 minutes per client IP. A human chatting via a
|
|
15
63
|
* browser or the libvex client won't come close; a single-host DoS
|
|
16
64
|
* gets throttled quickly.
|
|
17
65
|
*/
|
|
18
66
|
export const globalLimiter = rateLimit({
|
|
19
67
|
keyGenerator: keyByIp,
|
|
20
68
|
legacyHeaders: false,
|
|
21
|
-
limit:
|
|
69
|
+
limit: 3000,
|
|
70
|
+
skip: devApiKeySkipsRateLimits,
|
|
22
71
|
standardHeaders: "draft-7",
|
|
23
72
|
windowMs: 15 * 60 * 1000,
|
|
24
73
|
});
|
|
@@ -26,7 +75,7 @@ export const globalLimiter = rateLimit({
|
|
|
26
75
|
* Strict auth endpoint limiter. Applied per-route to /auth, /register,
|
|
27
76
|
* and /auth/device.
|
|
28
77
|
*
|
|
29
|
-
*
|
|
78
|
+
* 50 failed attempts per 15 minutes per IP. Successful logins don't
|
|
30
79
|
* count (`skipSuccessfulRequests`), so a normal user doesn't lock
|
|
31
80
|
* themselves out by fat-fingering a password once. Blocks brute force
|
|
32
81
|
* (CWE-307) without harming UX.
|
|
@@ -34,7 +83,8 @@ export const globalLimiter = rateLimit({
|
|
|
34
83
|
export const authLimiter = rateLimit({
|
|
35
84
|
keyGenerator: keyByIp,
|
|
36
85
|
legacyHeaders: false,
|
|
37
|
-
limit:
|
|
86
|
+
limit: 50,
|
|
87
|
+
skip: devApiKeySkipsRateLimits,
|
|
38
88
|
skipSuccessfulRequests: true,
|
|
39
89
|
standardHeaders: "draft-7",
|
|
40
90
|
windowMs: 15 * 60 * 1000,
|
|
@@ -45,13 +95,14 @@ export const authLimiter = rateLimit({
|
|
|
45
95
|
* upload attempts per minute so an attacker can't force spire to
|
|
46
96
|
* spend CPU/IO on repeated large-body parses.
|
|
47
97
|
*
|
|
48
|
-
*
|
|
98
|
+
* 200 uploads per minute per IP — generous for a chat client (rapid-
|
|
49
99
|
* fire image attachments) but tight enough to shield the disk.
|
|
50
100
|
*/
|
|
51
101
|
export const uploadLimiter = rateLimit({
|
|
52
102
|
keyGenerator: keyByIp,
|
|
53
103
|
legacyHeaders: false,
|
|
54
|
-
limit:
|
|
104
|
+
limit: 200,
|
|
105
|
+
skip: devApiKeySkipsRateLimits,
|
|
55
106
|
standardHeaders: "draft-7",
|
|
56
107
|
windowMs: 60 * 1000,
|
|
57
108
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rateLimit.js","sourceRoot":"","sources":["../../src/server/rateLimit.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"rateLimit.js","sourceRoot":"","sources":["../../src/server/rateLimit.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,OAAO,SAAS,EAAE,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAE/D,yFAAyF;AACzF,MAAM,CAAC,MAAM,kBAAkB,GAAG,eAAe,CAAC;AAElD;;;;;GAKG;AACH,MAAM,UAAU,wBAAwB,CAAC,GAAY;IACjD,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAC5D,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,KAAK,CAAC;IACjB,CAAC;IACD,MAAM,SAAS,GAAG,GAAG,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;IAC9C,IAAI,CAAC,SAAS,IAAI,SAAS,CAAC,MAAM,KAAK,UAAU,CAAC,MAAM,EAAE,CAAC;QACvD,OAAO,KAAK,CAAC;IACjB,CAAC;IACD,IAAI,CAAC;QACD,OAAO,eAAe,CAClB,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,EAC9B,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC,CAClC,CAAC;IACN,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,KAAK,CAAC;IACjB,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH;;;;;;;GAOG;AACH,MAAM,OAAO,GAAG,CAAC,GAAY,EAAU,EAAE,CAAC,cAAc,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;AAEvE;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,SAAS,CAAC;IACnC,YAAY,EAAE,OAAO;IACrB,aAAa,EAAE,KAAK;IACpB,KAAK,EAAE,IAAI;IACX,IAAI,EAAE,wBAAwB;IAC9B,eAAe,EAAE,SAAS;IAC1B,QAAQ,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI;CAC3B,CAAC,CAAC;AAEH;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,SAAS,CAAC;IACjC,YAAY,EAAE,OAAO;IACrB,aAAa,EAAE,KAAK;IACpB,KAAK,EAAE,EAAE;IACT,IAAI,EAAE,wBAAwB;IAC9B,sBAAsB,EAAE,IAAI;IAC5B,eAAe,EAAE,SAAS;IAC1B,QAAQ,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI;CAC3B,CAAC,CAAC;AAEH;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,SAAS,CAAC;IACnC,YAAY,EAAE,OAAO;IACrB,aAAa,EAAE,KAAK;IACpB,KAAK,EAAE,GAAG;IACV,IAAI,EAAE,wBAAwB;IAC9B,eAAe,EAAE,SAAS;IAC1B,QAAQ,EAAE,EAAE,GAAG,IAAI;CACtB,CAAC,CAAC"}
|
package/dist/server/user.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
import type { Database } from "../Database.ts";
|
|
2
|
-
import type winston from "winston";
|
|
3
2
|
import { TokenScopes } from "@vex-chat/types";
|
|
4
|
-
export declare const getUserRouter: (db: Database,
|
|
3
|
+
export declare const getUserRouter: (db: Database, tokenValidator: (key: string, scope: TokenScopes) => boolean) => import("express-serve-static-core").Router;
|
package/dist/server/user.js
CHANGED
|
@@ -6,7 +6,7 @@ import { stringify } from "uuid";
|
|
|
6
6
|
import { msgpack } from "../utils/msgpack.js";
|
|
7
7
|
import { censorUser, getParam, getUser } from "./utils.js";
|
|
8
8
|
import { protect } from "./index.js";
|
|
9
|
-
export const getUserRouter = (db,
|
|
9
|
+
export const getUserRouter = (db, tokenValidator) => {
|
|
10
10
|
const router = express.Router();
|
|
11
11
|
router.get("/:id", protect, async (req, res) => {
|
|
12
12
|
const user = await db.retrieveUser(getParam(req, "id"));
|
|
@@ -18,9 +18,13 @@ export const getUserRouter = (db, log, tokenValidator) => {
|
|
|
18
18
|
}
|
|
19
19
|
});
|
|
20
20
|
router.get("/:id/devices", protect, async (req, res) => {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
const id = getParam(req, "id");
|
|
22
|
+
const user = await db.retrieveUser(id);
|
|
23
|
+
if (!user) {
|
|
24
|
+
res.sendStatus(404);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const deviceList = await db.retrieveUserDeviceList([id]);
|
|
24
28
|
return res.send(msgpack.encode(deviceList));
|
|
25
29
|
});
|
|
26
30
|
router.get("/:id/permissions", protect, async (req, res) => {
|
|
@@ -69,7 +73,6 @@ export const getUserRouter = (db, log, tokenValidator) => {
|
|
|
69
73
|
const deviceData = parsed.data;
|
|
70
74
|
const token = xSignOpen(XUtils.decodeHex(deviceData.signed), XUtils.decodeHex(deviceData.signKey));
|
|
71
75
|
if (!token) {
|
|
72
|
-
log.warn("Invalid signature on token.");
|
|
73
76
|
res.sendStatus(400);
|
|
74
77
|
return;
|
|
75
78
|
}
|
|
@@ -78,9 +81,8 @@ export const getUserRouter = (db, log, tokenValidator) => {
|
|
|
78
81
|
const device = await db.createDevice(userDetails.userID, deviceData);
|
|
79
82
|
res.send(msgpack.encode(device));
|
|
80
83
|
}
|
|
81
|
-
catch (
|
|
82
|
-
|
|
83
|
-
// failed registration due to signkey being taken
|
|
84
|
+
catch (_err) {
|
|
85
|
+
// signkey already taken
|
|
84
86
|
res.sendStatus(470);
|
|
85
87
|
return;
|
|
86
88
|
}
|
package/dist/server/user.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user.js","sourceRoot":"","sources":["../../src/server/user.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"user.js","sourceRoot":"","sources":["../../src/server/user.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Returns the JWT signing secret.
|
|
2
|
+
* Returns the dedicated JWT HMAC signing secret.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* This MUST be a separate key from SPK (the Ed25519 server signing key)
|
|
5
|
+
* so that compromise of one does not affect the other.
|
|
6
6
|
*/
|
|
7
7
|
export declare function getJwtSecret(): string;
|
package/dist/utils/jwtSecret.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Returns the JWT signing secret.
|
|
2
|
+
* Returns the dedicated JWT HMAC signing secret.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* This MUST be a separate key from SPK (the Ed25519 server signing key)
|
|
5
|
+
* so that compromise of one does not affect the other.
|
|
6
6
|
*/
|
|
7
7
|
export function getJwtSecret() {
|
|
8
|
-
const secret = process.env["JWT_SECRET"]
|
|
8
|
+
const secret = process.env["JWT_SECRET"];
|
|
9
9
|
if (!secret) {
|
|
10
|
-
throw new Error("
|
|
11
|
-
"Set JWT_SECRET (preferred) or SPK in your environment.");
|
|
10
|
+
throw new Error("JWT_SECRET is not set. Generate one with: node scripts/gen-spk.js");
|
|
12
11
|
}
|
|
13
12
|
return secret;
|
|
14
13
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jwtSecret.js","sourceRoot":"","sources":["../../src/utils/jwtSecret.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,UAAU,YAAY;IACxB,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,
|
|
1
|
+
{"version":3,"file":"jwtSecret.js","sourceRoot":"","sources":["../../src/utils/jwtSecret.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,UAAU,YAAY;IACxB,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IACzC,IAAI,CAAC,MAAM,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CACX,mEAAmE,CACtE,CAAC;IACN,CAAC;IACD,OAAO,MAAM,CAAC;AAClB,CAAC"}
|
package/dist/utils/loadEnv.js
CHANGED
|
@@ -2,7 +2,7 @@ import { config } from "dotenv";
|
|
|
2
2
|
/* Populate process.env with vars from .env and verify required vars are present. */
|
|
3
3
|
export function loadEnv() {
|
|
4
4
|
config();
|
|
5
|
-
const requiredEnvVars = ["
|
|
5
|
+
const requiredEnvVars = ["DB_TYPE", "JWT_SECRET", "SPK"];
|
|
6
6
|
for (const required of requiredEnvVars) {
|
|
7
7
|
if (process.env[required] === undefined) {
|
|
8
8
|
process.stderr.write(`Required environment variable '${required}' is not set. Please consult the README.\n`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loadEnv.js","sourceRoot":"","sources":["../../src/utils/loadEnv.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,oFAAoF;AACpF,MAAM,UAAU,OAAO;IACnB,MAAM,EAAE,CAAC;IACT,MAAM,eAAe,GAAa,CAAC,
|
|
1
|
+
{"version":3,"file":"loadEnv.js","sourceRoot":"","sources":["../../src/utils/loadEnv.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,oFAAoF;AACpF,MAAM,UAAU,OAAO;IACnB,MAAM,EAAE,CAAC;IACT,MAAM,eAAe,GAAa,CAAC,SAAS,EAAE,YAAY,EAAE,KAAK,CAAC,CAAC;IACnE,KAAK,MAAM,QAAQ,IAAI,eAAe,EAAE,CAAC;QACrC,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,SAAS,EAAE,CAAC;YACtC,OAAO,CAAC,MAAM,CAAC,KAAK,CAChB,kCAAkC,QAAQ,4CAA4C,CACzF,CAAC;YACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACpB,CAAC;IACL,CAAC;AACL,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vex-chat/spire",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Vex server implementation in NodeJS.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -23,13 +23,18 @@
|
|
|
23
23
|
"preinstall": "npx only-allow npm",
|
|
24
24
|
"start": "node --experimental-strip-types src/run.ts",
|
|
25
25
|
"restart": "pm2 restart spire && pm2 logs spire",
|
|
26
|
-
"build": "tsc",
|
|
26
|
+
"build": "tsc && node scripts/clean-dist-tests.mjs",
|
|
27
27
|
"format": "prettier --write .",
|
|
28
28
|
"format:check": "prettier --check .",
|
|
29
|
-
"lint": "eslint src/",
|
|
30
|
-
"lint:fix": "eslint src/ --fix",
|
|
31
|
-
"prepare": "husky",
|
|
29
|
+
"lint": "eslint src/ scripts/stress/",
|
|
30
|
+
"lint:fix": "eslint src/ scripts/stress/ --fix",
|
|
31
|
+
"prepare": "husky || true",
|
|
32
32
|
"test": "vitest run",
|
|
33
|
+
"stress:web": "node --experimental-strip-types scripts/stress/spire-stress.ts",
|
|
34
|
+
"stress:cli": "node --experimental-strip-types scripts/stress/stress-cli-sweep.ts",
|
|
35
|
+
"stress:repo-manifest": "node --experimental-strip-types scripts/stress/stress-repo-manifest.ts",
|
|
36
|
+
"stress:docs-pack": "node --experimental-strip-types scripts/stress/stress-docs-pack.ts",
|
|
37
|
+
"stress:llm-triage": "node --experimental-strip-types scripts/stress/stress-llm-triage.ts",
|
|
33
38
|
"gen-spk": "node scripts/gen-spk.js",
|
|
34
39
|
"docs": "node ./scripts/generate-docs.js",
|
|
35
40
|
"license:check": "node scripts/fix-license-checker.mjs && npx @onebeyond/license-checker scan --allowOnly MIT MIT-0 ISC BSD-2-Clause BSD-3-Clause Apache-2.0 0BSD MPL-2.0 CC0-1.0 CC-BY-3.0 CC-BY-4.0 Unlicense BlueOak-1.0.0 Python-2.0 Zlib PSF-2.0 Artistic-2.0 AGPL-3.0-or-later WTFPL \"(MIT AND CC-BY-3.0)\" --ignoreRootPackageLicense --disableReport",
|
|
@@ -50,7 +55,9 @@
|
|
|
50
55
|
"@types/node": "25.6.0",
|
|
51
56
|
"@types/uuid": "10.0.0",
|
|
52
57
|
"@types/ws": "8.18.1",
|
|
58
|
+
"@vex-chat/libvex": "5.1.0",
|
|
53
59
|
"@vitest/eslint-plugin": "1.6.15",
|
|
60
|
+
"axios": "1.15.0",
|
|
54
61
|
"eslint": "10.2.0",
|
|
55
62
|
"eslint-config-prettier": "10.1.8",
|
|
56
63
|
"eslint-plugin-n": "17.24.0",
|
|
@@ -72,6 +79,9 @@
|
|
|
72
79
|
"*.ts": [
|
|
73
80
|
"eslint --fix",
|
|
74
81
|
"prettier --write"
|
|
82
|
+
],
|
|
83
|
+
"*.{yml,yaml}": [
|
|
84
|
+
"prettier --write"
|
|
75
85
|
]
|
|
76
86
|
},
|
|
77
87
|
"overrides": {
|
|
@@ -83,8 +93,9 @@
|
|
|
83
93
|
"dependencies": {
|
|
84
94
|
"@asyncapi/web-component": "2.6.5",
|
|
85
95
|
"@scalar/express-api-reference": "0.9.7",
|
|
86
|
-
"@vex-chat/crypto": "2.0.
|
|
96
|
+
"@vex-chat/crypto": "2.0.1",
|
|
87
97
|
"@vex-chat/types": "2.0.0",
|
|
98
|
+
"argon2": "0.44.0",
|
|
88
99
|
"better-sqlite3": "11.10.0",
|
|
89
100
|
"cors": "2.8.6",
|
|
90
101
|
"dotenv": "17.4.1",
|
|
@@ -98,10 +109,8 @@
|
|
|
98
109
|
"msgpackr": "1.11.8",
|
|
99
110
|
"multer": "2.1.1",
|
|
100
111
|
"parse-duration": "2.1.6",
|
|
101
|
-
"picocolors": "1.1.1",
|
|
102
112
|
"tweetnacl": "1.0.3",
|
|
103
113
|
"uuid": "8.3.2",
|
|
104
|
-
"winston": "3.19.0",
|
|
105
114
|
"ws": "8.20.0",
|
|
106
115
|
"zod": "4.3.6"
|
|
107
116
|
}
|
package/src/ClientManager.ts
CHANGED
|
@@ -11,7 +11,6 @@ import type {
|
|
|
11
11
|
User,
|
|
12
12
|
UserRecord,
|
|
13
13
|
} from "@vex-chat/types";
|
|
14
|
-
import type winston from "winston";
|
|
15
14
|
import type WebSocket from "ws";
|
|
16
15
|
|
|
17
16
|
import { EventEmitter } from "events";
|
|
@@ -21,11 +20,9 @@ import { xConcat, XUtils } from "@vex-chat/crypto";
|
|
|
21
20
|
import { xSignOpen } from "@vex-chat/crypto";
|
|
22
21
|
import { MailWSSchema, SocketAuthErrors } from "@vex-chat/types";
|
|
23
22
|
|
|
24
|
-
import pc from "picocolors";
|
|
25
23
|
import { parse as uuidParse, validate as uuidValidate } from "uuid";
|
|
26
24
|
|
|
27
|
-
import {
|
|
28
|
-
import { createLogger } from "./utils/createLogger.ts";
|
|
25
|
+
import { TOKEN_EXPIRY } from "./Spire.ts";
|
|
29
26
|
import { createUint8UUID } from "./utils/createUint8UUID.ts";
|
|
30
27
|
import { msgpack } from "./utils/msgpack.ts";
|
|
31
28
|
|
|
@@ -50,7 +47,6 @@ export class ClientManager extends EventEmitter {
|
|
|
50
47
|
private db: Database;
|
|
51
48
|
private device: Device | null;
|
|
52
49
|
private failed: boolean = false;
|
|
53
|
-
private log: winston.Logger;
|
|
54
50
|
private notify: (
|
|
55
51
|
userID: string,
|
|
56
52
|
event: string,
|
|
@@ -66,7 +62,6 @@ export class ClientManager extends EventEmitter {
|
|
|
66
62
|
db: Database,
|
|
67
63
|
notify: (userID: string, event: string, transmissionID: string) => void,
|
|
68
64
|
userDetails: User,
|
|
69
|
-
options?: SpireOptions,
|
|
70
65
|
) {
|
|
71
66
|
super();
|
|
72
67
|
this.conn = ws;
|
|
@@ -75,7 +70,6 @@ export class ClientManager extends EventEmitter {
|
|
|
75
70
|
this.userDetails = userDetails;
|
|
76
71
|
this.device = null;
|
|
77
72
|
this.notify = notify;
|
|
78
|
-
this.log = createLogger("client-manager", options?.logLevel || "error");
|
|
79
73
|
|
|
80
74
|
this.initListeners();
|
|
81
75
|
this.challenge();
|
|
@@ -96,28 +90,11 @@ export class ClientManager extends EventEmitter {
|
|
|
96
90
|
}
|
|
97
91
|
|
|
98
92
|
public send(msg: BaseMsg, header?: Uint8Array) {
|
|
99
|
-
if (header) {
|
|
100
|
-
this.log.debug(pc.bold(pc.red("OUTH")), header.toString());
|
|
101
|
-
} else {
|
|
102
|
-
this.log.debug(pc.bold(pc.red("OUTH")), emptyHeader.toString());
|
|
103
|
-
}
|
|
104
|
-
|
|
105
93
|
const packedMessage = packMessage(msg, header);
|
|
106
|
-
|
|
107
|
-
this.log.info(
|
|
108
|
-
pc.bold("⟶ ") +
|
|
109
|
-
responseColor(msg.type.toUpperCase()) +
|
|
110
|
-
" " +
|
|
111
|
-
this.toString() +
|
|
112
|
-
" " +
|
|
113
|
-
pc.yellow(Buffer.byteLength(packedMessage)),
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
this.log.debug(pc.bold(pc.red("OUT")), msg);
|
|
117
94
|
try {
|
|
118
95
|
this.conn.send(packedMessage);
|
|
119
|
-
} catch (
|
|
120
|
-
|
|
96
|
+
} catch (_err: unknown) {
|
|
97
|
+
// debugger: WS send failed
|
|
121
98
|
this.fail();
|
|
122
99
|
}
|
|
123
100
|
}
|
|
@@ -150,7 +127,6 @@ export class ClientManager extends EventEmitter {
|
|
|
150
127
|
if (this.failed) {
|
|
151
128
|
return;
|
|
152
129
|
}
|
|
153
|
-
this.log.warn("Connection closed.");
|
|
154
130
|
this.conn.close();
|
|
155
131
|
this.failed = true;
|
|
156
132
|
this.emit("fail");
|
|
@@ -173,12 +149,11 @@ export class ClientManager extends EventEmitter {
|
|
|
173
149
|
this.fail();
|
|
174
150
|
});
|
|
175
151
|
this.conn.on("message", (message: Buffer) => {
|
|
176
|
-
const [header, msg] = unpackMessage(message);
|
|
177
152
|
const size = Buffer.byteLength(message);
|
|
178
153
|
|
|
179
154
|
if (size > MAX_MSG_SIZE) {
|
|
180
155
|
this.sendErr(
|
|
181
|
-
|
|
156
|
+
"00000000-0000-0000-0000-000000000000",
|
|
182
157
|
"Message is too big. Received size " +
|
|
183
158
|
String(size) +
|
|
184
159
|
" while max size is " +
|
|
@@ -187,16 +162,7 @@ export class ClientManager extends EventEmitter {
|
|
|
187
162
|
return;
|
|
188
163
|
}
|
|
189
164
|
|
|
190
|
-
|
|
191
|
-
pc.bold("⟵ ") +
|
|
192
|
-
pc.bold(msg.type.toUpperCase()) +
|
|
193
|
-
" " +
|
|
194
|
-
this.toString() +
|
|
195
|
-
" " +
|
|
196
|
-
pc.yellow(String(size)),
|
|
197
|
-
);
|
|
198
|
-
this.log.debug(pc.bold(pc.red("INH")), header.toString());
|
|
199
|
-
this.log.debug(pc.bold(pc.red("IN")), msg);
|
|
165
|
+
const [header, msg] = unpackMessage(message);
|
|
200
166
|
|
|
201
167
|
if (!msg.type) {
|
|
202
168
|
this.sendErr(msg.transmissionID, "Message type is required.");
|
|
@@ -238,7 +204,6 @@ export class ClientManager extends EventEmitter {
|
|
|
238
204
|
void this.verifyResponse(msg as RespMsg);
|
|
239
205
|
break;
|
|
240
206
|
default:
|
|
241
|
-
this.log.info("unsupported message %s", msg.type);
|
|
242
207
|
break;
|
|
243
208
|
}
|
|
244
209
|
});
|
|
@@ -266,8 +231,6 @@ export class ClientManager extends EventEmitter {
|
|
|
266
231
|
this.getDevice().deviceID,
|
|
267
232
|
this.getUser().userID,
|
|
268
233
|
);
|
|
269
|
-
this.log.info("Received mail for " + mail.recipient);
|
|
270
|
-
|
|
271
234
|
const deviceDetails = await this.db.retrieveDevice(
|
|
272
235
|
mail.recipient,
|
|
273
236
|
);
|
|
@@ -288,13 +251,12 @@ export class ClientManager extends EventEmitter {
|
|
|
288
251
|
mail.recipient,
|
|
289
252
|
);
|
|
290
253
|
} catch (err: unknown) {
|
|
291
|
-
this.log.error(String(err));
|
|
292
254
|
this.sendErr(msg.transmissionID, String(err));
|
|
293
255
|
}
|
|
294
256
|
}
|
|
295
257
|
break;
|
|
296
258
|
default:
|
|
297
|
-
|
|
259
|
+
break;
|
|
298
260
|
}
|
|
299
261
|
}
|
|
300
262
|
|
|
@@ -309,8 +271,7 @@ export class ClientManager extends EventEmitter {
|
|
|
309
271
|
}
|
|
310
272
|
|
|
311
273
|
private async pingLoop() {
|
|
312
|
-
|
|
313
|
-
while (true) {
|
|
274
|
+
while (!this.failed) {
|
|
314
275
|
this.ping();
|
|
315
276
|
await sleep(5000);
|
|
316
277
|
}
|
|
@@ -384,7 +345,6 @@ export class ClientManager extends EventEmitter {
|
|
|
384
345
|
}
|
|
385
346
|
}
|
|
386
347
|
if (!message) {
|
|
387
|
-
this.log.warn("Signature verification failed!");
|
|
388
348
|
this.sendAuthError(SocketAuthErrors.BadSignature);
|
|
389
349
|
this.fail();
|
|
390
350
|
return;
|
|
@@ -394,11 +354,9 @@ export class ClientManager extends EventEmitter {
|
|
|
394
354
|
this.user = user;
|
|
395
355
|
this.authorize(msg.transmissionID);
|
|
396
356
|
} else {
|
|
397
|
-
this.log.warn("Token is bad!");
|
|
398
357
|
this.sendAuthError(SocketAuthErrors.InvalidToken);
|
|
399
358
|
}
|
|
400
359
|
} else {
|
|
401
|
-
this.log.info("User is not registered.");
|
|
402
360
|
this.sendAuthError(SocketAuthErrors.UserNotRegistered);
|
|
403
361
|
|
|
404
362
|
this.fail();
|
|
@@ -421,14 +379,3 @@ function unpackMessage(msg: Buffer): [Uint8Array, BaseMsg] {
|
|
|
421
379
|
|
|
422
380
|
return [msgh, msgb];
|
|
423
381
|
}
|
|
424
|
-
|
|
425
|
-
const responseColor = (status: string): string => {
|
|
426
|
-
switch (status) {
|
|
427
|
-
case "ERROR":
|
|
428
|
-
return pc.bold(pc.red(status));
|
|
429
|
-
case "SUCCESS":
|
|
430
|
-
return pc.bold(pc.green(status));
|
|
431
|
-
default:
|
|
432
|
-
return status;
|
|
433
|
-
}
|
|
434
|
-
};
|