@vex-chat/spire 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +37 -38
  2. package/dist/ClientManager.d.ts +1 -3
  3. package/dist/ClientManager.js +6 -48
  4. package/dist/ClientManager.js.map +1 -1
  5. package/dist/Database.d.ts +25 -5
  6. package/dist/Database.js +54 -16
  7. package/dist/Database.js.map +1 -1
  8. package/dist/Spire.d.ts +0 -3
  9. package/dist/Spire.js +35 -73
  10. package/dist/Spire.js.map +1 -1
  11. package/dist/__tests__/Database.spec.js +0 -14
  12. package/dist/__tests__/Database.spec.js.map +1 -1
  13. package/dist/db/schema.d.ts +1 -0
  14. package/dist/migrations/2026-04-14_argon2id-password-hashing.d.ts +3 -0
  15. package/dist/migrations/2026-04-14_argon2id-password-hashing.js +12 -0
  16. package/dist/migrations/2026-04-14_argon2id-password-hashing.js.map +1 -0
  17. package/dist/run.js +0 -1
  18. package/dist/run.js.map +1 -1
  19. package/dist/server/avatar.d.ts +1 -3
  20. package/dist/server/avatar.js +7 -11
  21. package/dist/server/avatar.js.map +1 -1
  22. package/dist/server/errors.d.ts +3 -6
  23. package/dist/server/errors.js +2 -28
  24. package/dist/server/errors.js.map +1 -1
  25. package/dist/server/file.d.ts +1 -2
  26. package/dist/server/file.js +5 -7
  27. package/dist/server/file.js.map +1 -1
  28. package/dist/server/index.d.ts +1 -2
  29. package/dist/server/index.js +22 -33
  30. package/dist/server/index.js.map +1 -1
  31. package/dist/server/invite.d.ts +1 -2
  32. package/dist/server/invite.js +1 -1
  33. package/dist/server/invite.js.map +1 -1
  34. package/dist/server/rateLimit.d.ts +3 -3
  35. package/dist/server/rateLimit.js +6 -6
  36. package/dist/server/rateLimit.js.map +1 -1
  37. package/dist/server/user.d.ts +1 -2
  38. package/dist/server/user.js +3 -5
  39. package/dist/server/user.js.map +1 -1
  40. package/dist/utils/jwtSecret.d.ts +3 -3
  41. package/dist/utils/jwtSecret.js +5 -6
  42. package/dist/utils/jwtSecret.js.map +1 -1
  43. package/dist/utils/loadEnv.js +1 -1
  44. package/dist/utils/loadEnv.js.map +1 -1
  45. package/package.json +6 -4
  46. package/src/ClientManager.ts +6 -58
  47. package/src/Database.ts +83 -21
  48. package/src/Spire.ts +37 -108
  49. package/src/__tests__/Database.spec.ts +0 -17
  50. package/src/db/schema.ts +1 -0
  51. package/src/migrations/2026-04-14_argon2id-password-hashing.ts +18 -0
  52. package/src/run.ts +0 -1
  53. package/src/server/avatar.ts +7 -14
  54. package/src/server/errors.ts +3 -32
  55. package/src/server/file.ts +5 -10
  56. package/src/server/index.ts +23 -37
  57. package/src/server/invite.ts +0 -2
  58. package/src/server/rateLimit.ts +6 -6
  59. package/src/server/user.ts +2 -6
  60. package/src/utils/jwtSecret.ts +5 -6
  61. package/src/utils/loadEnv.ts +1 -1
  62. package/dist/utils/createLogger.d.ts +0 -5
  63. package/dist/utils/createLogger.js +0 -35
  64. package/dist/utils/createLogger.js.map +0 -1
  65. package/src/utils/createLogger.ts +0 -47
@@ -11,14 +11,14 @@ const keyByIp = (req) => ipKeyGenerator(req.ip ?? "");
11
11
  /**
12
12
  * Global per-IP limiter. Applied app-wide via `api.use(globalLimiter)`.
13
13
  *
14
- * 300 requests per 15 minutes per client IP. A human chatting via a
14
+ * 3000 requests per 15 minutes per client IP. A human chatting via a
15
15
  * browser or the libvex client won't come close; a single-host DoS
16
16
  * gets throttled quickly.
17
17
  */
18
18
  export const globalLimiter = rateLimit({
19
19
  keyGenerator: keyByIp,
20
20
  legacyHeaders: false,
21
- limit: 300,
21
+ limit: 3000,
22
22
  standardHeaders: "draft-7",
23
23
  windowMs: 15 * 60 * 1000,
24
24
  });
@@ -26,7 +26,7 @@ export const globalLimiter = rateLimit({
26
26
  * Strict auth endpoint limiter. Applied per-route to /auth, /register,
27
27
  * and /auth/device.
28
28
  *
29
- * 5 failed attempts per 15 minutes per IP. Successful logins don't
29
+ * 50 failed attempts per 15 minutes per IP. Successful logins don't
30
30
  * count (`skipSuccessfulRequests`), so a normal user doesn't lock
31
31
  * themselves out by fat-fingering a password once. Blocks brute force
32
32
  * (CWE-307) without harming UX.
@@ -34,7 +34,7 @@ export const globalLimiter = rateLimit({
34
34
  export const authLimiter = rateLimit({
35
35
  keyGenerator: keyByIp,
36
36
  legacyHeaders: false,
37
- limit: 5,
37
+ limit: 50,
38
38
  skipSuccessfulRequests: true,
39
39
  standardHeaders: "draft-7",
40
40
  windowMs: 15 * 60 * 1000,
@@ -45,13 +45,13 @@ export const authLimiter = rateLimit({
45
45
  * upload attempts per minute so an attacker can't force spire to
46
46
  * spend CPU/IO on repeated large-body parses.
47
47
  *
48
- * 20 uploads per minute per IP — generous for a chat client (rapid-
48
+ * 200 uploads per minute per IP — generous for a chat client (rapid-
49
49
  * fire image attachments) but tight enough to shield the disk.
50
50
  */
51
51
  export const uploadLimiter = rateLimit({
52
52
  keyGenerator: keyByIp,
53
53
  legacyHeaders: false,
54
- limit: 20,
54
+ limit: 200,
55
55
  standardHeaders: "draft-7",
56
56
  windowMs: 60 * 1000,
57
57
  });
@@ -1 +1 @@
1
- {"version":3,"file":"rateLimit.js","sourceRoot":"","sources":["../../src/server/rateLimit.ts"],"names":[],"mappings":"AAyBA,OAAO,SAAS,EAAE,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAE/D;;;;;;;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,GAAG;IACV,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,CAAC;IACR,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,EAAE;IACT,eAAe,EAAE,SAAS;IAC1B,QAAQ,EAAE,EAAE,GAAG,IAAI;CACtB,CAAC,CAAC"}
1
+ {"version":3,"file":"rateLimit.js","sourceRoot":"","sources":["../../src/server/rateLimit.ts"],"names":[],"mappings":"AAyBA,OAAO,SAAS,EAAE,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAE/D;;;;;;;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,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,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,eAAe,EAAE,SAAS;IAC1B,QAAQ,EAAE,EAAE,GAAG,IAAI;CACtB,CAAC,CAAC"}
@@ -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, log: winston.Logger, tokenValidator: (key: string, scope: TokenScopes) => boolean) => import("express-serve-static-core").Router;
3
+ export declare const getUserRouter: (db: Database, tokenValidator: (key: string, scope: TokenScopes) => boolean) => import("express-serve-static-core").Router;
@@ -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, log, tokenValidator) => {
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"));
@@ -69,7 +69,6 @@ export const getUserRouter = (db, log, tokenValidator) => {
69
69
  const deviceData = parsed.data;
70
70
  const token = xSignOpen(XUtils.decodeHex(deviceData.signed), XUtils.decodeHex(deviceData.signKey));
71
71
  if (!token) {
72
- log.warn("Invalid signature on token.");
73
72
  res.sendStatus(400);
74
73
  return;
75
74
  }
@@ -78,9 +77,8 @@ export const getUserRouter = (db, log, tokenValidator) => {
78
77
  const device = await db.createDevice(userDetails.userID, deviceData);
79
78
  res.send(msgpack.encode(device));
80
79
  }
81
- catch (err) {
82
- log.warn(String(err));
83
- // failed registration due to signkey being taken
80
+ catch (_err) {
81
+ // signkey already taken
84
82
  res.sendStatus(470);
85
83
  return;
86
84
  }
@@ -1 +1 @@
1
- {"version":3,"file":"user.js","sourceRoot":"","sources":["../../src/server/user.ts"],"names":[],"mappings":"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"}
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
- * Prefers JWT_SECRET (dedicated HMAC key) over SPK (NaCl server signing key).
5
- * Falls back to SPK for backward compat with existing deployments.
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;
@@ -1,14 +1,13 @@
1
1
  /**
2
- * Returns the JWT signing secret.
2
+ * Returns the dedicated JWT HMAC signing secret.
3
3
  *
4
- * Prefers JWT_SECRET (dedicated HMAC key) over SPK (NaCl server signing key).
5
- * Falls back to SPK for backward compat with existing deployments.
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"] ?? process.env["SPK"];
8
+ const secret = process.env["JWT_SECRET"];
9
9
  if (!secret) {
10
- throw new Error("Neither JWT_SECRET nor SPK is set. " +
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,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAC/D,IAAI,CAAC,MAAM,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CACX,qCAAqC;YACjC,wDAAwD,CAC/D,CAAC;IACN,CAAC;IACD,OAAO,MAAM,CAAC;AAClB,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"}
@@ -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 = ["CANARY", "DB_TYPE", "SPK"];
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,QAAQ,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;IAC/D,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"}
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.0",
3
+ "version": "1.0.1",
4
4
  "description": "Vex server implementation in NodeJS.",
5
5
  "type": "module",
6
6
  "files": [
@@ -28,7 +28,7 @@
28
28
  "format:check": "prettier --check .",
29
29
  "lint": "eslint src/",
30
30
  "lint:fix": "eslint src/ --fix",
31
- "prepare": "husky",
31
+ "prepare": "husky || true",
32
32
  "test": "vitest run",
33
33
  "gen-spk": "node scripts/gen-spk.js",
34
34
  "docs": "node ./scripts/generate-docs.js",
@@ -72,6 +72,9 @@
72
72
  "*.ts": [
73
73
  "eslint --fix",
74
74
  "prettier --write"
75
+ ],
76
+ "*.{yml,yaml}": [
77
+ "prettier --write"
75
78
  ]
76
79
  },
77
80
  "overrides": {
@@ -85,6 +88,7 @@
85
88
  "@scalar/express-api-reference": "0.9.7",
86
89
  "@vex-chat/crypto": "2.0.0",
87
90
  "@vex-chat/types": "2.0.0",
91
+ "argon2": "0.44.0",
88
92
  "better-sqlite3": "11.10.0",
89
93
  "cors": "2.8.6",
90
94
  "dotenv": "17.4.1",
@@ -98,10 +102,8 @@
98
102
  "msgpackr": "1.11.8",
99
103
  "multer": "2.1.1",
100
104
  "parse-duration": "2.1.6",
101
- "picocolors": "1.1.1",
102
105
  "tweetnacl": "1.0.3",
103
106
  "uuid": "8.3.2",
104
- "winston": "3.19.0",
105
107
  "ws": "8.20.0",
106
108
  "zod": "4.3.6"
107
109
  }
@@ -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 { type SpireOptions, TOKEN_EXPIRY } from "./Spire.ts";
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 (err: unknown) {
120
- this.log.warn(String(err));
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
- msg.transmissionID,
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
- this.log.info(
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
- this.log.info("Unsupported resource type " + msg.resourceType);
259
+ break;
298
260
  }
299
261
  }
300
262
 
@@ -384,7 +346,6 @@ export class ClientManager extends EventEmitter {
384
346
  }
385
347
  }
386
348
  if (!message) {
387
- this.log.warn("Signature verification failed!");
388
349
  this.sendAuthError(SocketAuthErrors.BadSignature);
389
350
  this.fail();
390
351
  return;
@@ -394,11 +355,9 @@ export class ClientManager extends EventEmitter {
394
355
  this.user = user;
395
356
  this.authorize(msg.transmissionID);
396
357
  } else {
397
- this.log.warn("Token is bad!");
398
358
  this.sendAuthError(SocketAuthErrors.InvalidToken);
399
359
  }
400
360
  } else {
401
- this.log.info("User is not registered.");
402
361
  this.sendAuthError(SocketAuthErrors.UserNotRegistered);
403
362
 
404
363
  this.fail();
@@ -421,14 +380,3 @@ function unpackMessage(msg: Buffer): [Uint8Array, BaseMsg] {
421
380
 
422
381
  return [msgh, msgb];
423
382
  }
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
- };
package/src/Database.ts CHANGED
@@ -18,7 +18,6 @@ import type {
18
18
  UserRecord,
19
19
  } from "@vex-chat/types";
20
20
  import type { Migration, MigrationProvider } from "kysely";
21
- import type winston from "winston";
22
21
 
23
22
  import { EventEmitter } from "events";
24
23
  import { pbkdf2Sync } from "node:crypto";
@@ -26,9 +25,11 @@ import * as fs from "node:fs/promises";
26
25
  import path from "node:path";
27
26
  import { fileURLToPath, pathToFileURL } from "node:url";
28
27
 
29
- import { xMakeNonce, XUtils } from "@vex-chat/crypto";
28
+ import { XUtils } from "@vex-chat/crypto";
30
29
  import { MailType } from "@vex-chat/types";
31
30
 
31
+ import argon2 from "argon2";
32
+
32
33
  /**
33
34
  * Narrow a plain integer from the `mailType` SQL column to the
34
35
  * `MailType` union (0 = initial, 1 = subsequent). Throws if the
@@ -46,8 +47,6 @@ import BetterSqlite3 from "better-sqlite3";
46
47
  import { Kysely, Migrator, sql, SqliteDialect } from "kysely";
47
48
  import { stringify as uuidStringify, validate as uuidValidate } from "uuid";
48
49
 
49
- import { createLogger } from "./utils/createLogger.ts";
50
-
51
50
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
52
51
  const migrationFolder = path.join(__dirname, "migrations");
53
52
 
@@ -104,21 +103,25 @@ function isMigration(mod: unknown): mod is Migration {
104
103
  }
105
104
 
106
105
  const pubkeyRegex = /[0-9a-f]{64}/;
107
- export const ITERATIONS = 1000;
106
+
107
+ /** Legacy iteration count kept only for verifying old PBKDF2 hashes. */
108
+ const PBKDF2_ITERATIONS = 1000;
108
109
 
109
110
  // ── Row-to-interface converters ─────────────────────────────────────────
110
111
  // SQLite stores booleans as integers and dates as strings, but the
111
112
  // @vex-chat/types interfaces expect boolean / Date.
112
113
 
114
+ /** Internal record that includes the hash algorithm discriminator. */
115
+ export interface InternalUserRecord extends UserRecord {
116
+ hashAlgo: string;
117
+ }
118
+
113
119
  export class Database extends EventEmitter {
114
120
  private db: Kysely<ServerDatabase>;
115
- private log: winston.Logger;
116
121
 
117
122
  constructor(options?: SpireOptions) {
118
123
  super();
119
124
 
120
- this.log = createLogger("spire-db", options?.logLevel || "error");
121
-
122
125
  const dbType = options?.dbType || "sqlite3";
123
126
 
124
127
  let filename: string;
@@ -151,7 +154,6 @@ export class Database extends EventEmitter {
151
154
  }
152
155
 
153
156
  public async close(): Promise<void> {
154
- this.log.info("Closing database.");
155
157
  await this.db.destroy();
156
158
  }
157
159
 
@@ -280,13 +282,12 @@ export class Database extends EventEmitter {
280
282
  regPayload: RegistrationPayload,
281
283
  ): Promise<[null | UserRecord, Error | null]> {
282
284
  try {
283
- const salt = xMakeNonce();
284
- const passwordHash = hashPassword(regPayload.password, salt);
285
+ const passwordHash = await hashPasswordArgon2(regPayload.password);
285
286
 
286
287
  const user: UserRecord = {
287
288
  lastSeen: new Date().toISOString(),
288
- passwordHash: passwordHash.toString("hex"),
289
- passwordSalt: XUtils.encodeHex(salt),
289
+ passwordHash,
290
+ passwordSalt: "",
290
291
  userID: uuidStringify(regKey),
291
292
  username: regPayload.username,
292
293
  };
@@ -295,6 +296,7 @@ export class Database extends EventEmitter {
295
296
  .insertInto("users")
296
297
  .values({
297
298
  ...user,
299
+ hashAlgo: "argon2id",
298
300
  lastSeen: user.lastSeen,
299
301
  })
300
302
  .execute();
@@ -489,8 +491,8 @@ export class Database extends EventEmitter {
489
491
  try {
490
492
  await sql`select 1 as ok`.execute(this.db);
491
493
  return true;
492
- } catch (err: unknown) {
493
- this.log.warn("Database health check failed: " + String(err));
494
+ } catch (_err: unknown) {
495
+ // debugger: health check failed
494
496
  return false;
495
497
  }
496
498
  }
@@ -511,6 +513,21 @@ export class Database extends EventEmitter {
511
513
  .execute();
512
514
  }
513
515
 
516
+ public async rehashPassword(
517
+ userID: string,
518
+ newHash: string,
519
+ ): Promise<void> {
520
+ await this.db
521
+ .updateTable("users")
522
+ .set({
523
+ hashAlgo: "argon2id",
524
+ passwordHash: newHash,
525
+ passwordSalt: "",
526
+ })
527
+ .where("userID", "=", userID)
528
+ .execute();
529
+ }
530
+
514
531
  /**
515
532
  * Retrives a list of users that should be notified when a specific resourceID
516
533
  * experiences changes.
@@ -763,7 +780,7 @@ export class Database extends EventEmitter {
763
780
  // the identifier can be username, public key, or userID
764
781
  public async retrieveUser(
765
782
  userIdentifier: string,
766
- ): Promise<null | UserRecord> {
783
+ ): Promise<InternalUserRecord | null> {
767
784
  let rows;
768
785
  if (uuidValidate(userIdentifier)) {
769
786
  rows = await this.db
@@ -795,7 +812,7 @@ export class Database extends EventEmitter {
795
812
  return rows.map(toDevice);
796
813
  }
797
814
 
798
- public async retrieveUsers(): Promise<UserRecord[]> {
815
+ public async retrieveUsers(): Promise<InternalUserRecord[]> {
799
816
  const rows = await this.db.selectFrom("users").selectAll().execute();
800
817
  return rows.map(toUserRecord);
801
818
  }
@@ -864,6 +881,53 @@ export class Database extends EventEmitter {
864
881
  }
865
882
  }
866
883
 
884
+ /**
885
+ * Hash a password with Argon2id (new default).
886
+ * Returns the encoded hash string which embeds salt, params, and digest.
887
+ */
888
+ export async function hashPasswordArgon2(password: string): Promise<string> {
889
+ return argon2.hash(password, {
890
+ memoryCost: 65536,
891
+ parallelism: 4,
892
+ timeCost: 3,
893
+ type: argon2.argon2id,
894
+ });
895
+ }
896
+
897
+ /**
898
+ * Verify a password against either Argon2id or legacy PBKDF2 storage.
899
+ * Returns `{ valid, needsRehash }` — callers should rehash on success
900
+ * when `needsRehash` is true.
901
+ */
902
+ export async function verifyPassword(
903
+ password: string,
904
+ stored: { hashAlgo: string; passwordHash: string; passwordSalt: string },
905
+ ): Promise<{ needsRehash: boolean; valid: boolean }> {
906
+ if (stored.hashAlgo === "argon2id") {
907
+ const valid = await argon2.verify(stored.passwordHash, password);
908
+ return { needsRehash: false, valid };
909
+ }
910
+
911
+ // Legacy PBKDF2 path
912
+ const salt = XUtils.decodeHex(stored.passwordSalt);
913
+ const computed = pbkdf2Sync(
914
+ password,
915
+ salt,
916
+ PBKDF2_ITERATIONS,
917
+ 32,
918
+ "sha512",
919
+ );
920
+ const storedBuf = XUtils.decodeHex(stored.passwordHash);
921
+
922
+ if (computed.length !== storedBuf.length) {
923
+ return { needsRehash: false, valid: false };
924
+ }
925
+
926
+ const { timingSafeEqual } = await import("node:crypto");
927
+ const valid = timingSafeEqual(computed, storedBuf);
928
+ return { needsRehash: valid, valid };
929
+ }
930
+
867
931
  function toDevice(row: {
868
932
  deleted: number;
869
933
  deviceID: string;
@@ -912,14 +976,12 @@ function toServer(row: {
912
976
  }
913
977
 
914
978
  function toUserRecord(row: {
979
+ hashAlgo: string;
915
980
  lastSeen: string;
916
981
  passwordHash: string;
917
982
  passwordSalt: string;
918
983
  userID: string;
919
984
  username: string;
920
- }): UserRecord {
985
+ }): InternalUserRecord {
921
986
  return { ...row };
922
987
  }
923
-
924
- export const hashPassword = (password: string, salt: Uint8Array) =>
925
- pbkdf2Sync(password, salt, ITERATIONS, 32, "sha512");