@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
package/src/Spire.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import type { ActionToken, BaseMsg, NotifyMsg, User } from "@vex-chat/types";
2
2
  import type { Server } from "http";
3
- import type winston from "winston";
4
3
 
5
4
  import { EventEmitter } from "events";
6
5
  import { execSync } from "node:child_process";
@@ -30,11 +29,10 @@ import { WebSocketServer } from "ws";
30
29
  import { z } from "zod/v4";
31
30
 
32
31
  import { ClientManager } from "./ClientManager.ts";
33
- import { Database, hashPassword } from "./Database.ts";
32
+ import { Database, hashPasswordArgon2, verifyPassword } from "./Database.ts";
34
33
  import { initApp, protect } from "./server/index.ts";
35
34
  import { authLimiter } from "./server/rateLimit.ts";
36
35
  import { censorUser, getParam, getUser } from "./server/utils.ts";
37
- import { createLogger } from "./utils/createLogger.ts";
38
36
  import { getJwtSecret } from "./utils/jwtSecret.ts";
39
37
  import { msgpack } from "./utils/msgpack.ts";
40
38
 
@@ -130,14 +128,6 @@ const getCommitSha = (): string => {
130
128
  export interface SpireOptions {
131
129
  apiPort?: number;
132
130
  dbType?: "mysql" | "sqlite3" | "sqlite3mem" | "sqlite";
133
- logLevel?:
134
- | "debug"
135
- | "error"
136
- | "http"
137
- | "info"
138
- | "silly"
139
- | "verbose"
140
- | "warn";
141
131
  }
142
132
 
143
133
  export class Spire extends EventEmitter {
@@ -151,9 +141,6 @@ export class Spire extends EventEmitter {
151
141
  string,
152
142
  { deviceID: string; nonce: string; time: number }
153
143
  >();
154
- private log: winston.Logger;
155
- private options: SpireOptions | undefined;
156
-
157
144
  private queuedRequestIncrements = 0;
158
145
  private requestsTotal = 0;
159
146
 
@@ -164,7 +151,10 @@ export class Spire extends EventEmitter {
164
151
 
165
152
  private readonly startedAt = new Date();
166
153
  private readonly version = getAppVersion();
167
- private wss: WebSocketServer = new WebSocketServer({ noServer: true });
154
+ private wss: WebSocketServer = new WebSocketServer({
155
+ maxPayload: 4096,
156
+ noServer: true,
157
+ });
168
158
 
169
159
  constructor(SK: string, options?: SpireOptions) {
170
160
  super();
@@ -180,17 +170,12 @@ export class Spire extends EventEmitter {
180
170
  this.db = new Database(options);
181
171
  this.db.on("ready", () => {
182
172
  this.dbReady = true;
183
- this.bootstrapRequestCounter().catch((err: unknown) => {
184
- this.log.error(
185
- "Failed to load persisted request counter: " + String(err),
186
- );
173
+ this.bootstrapRequestCounter().catch((_err: unknown) => {
174
+ // debugger: bootstrap request counter failed
187
175
  });
188
176
  });
189
177
 
190
- this.log = createLogger("spire", options?.logLevel || "error");
191
178
  this.init(options?.apiPort || 16777);
192
-
193
- this.options = options;
194
179
  }
195
180
 
196
181
  public async close(): Promise<void> {
@@ -198,18 +183,9 @@ export class Spire extends EventEmitter {
198
183
  ws.terminate();
199
184
  });
200
185
 
201
- this.wss.on("close", () => {
202
- this.log.info("ws: closed.");
203
- });
204
-
205
- this.server?.on("close", () => {
206
- this.log.info("http: closed.");
207
- });
208
-
209
186
  this.server?.close();
210
187
  this.wss.close();
211
188
  await this.db.close();
212
- return;
213
189
  }
214
190
 
215
191
  private async bootstrapRequestCounter(): Promise<void> {
@@ -246,11 +222,8 @@ export class Spire extends EventEmitter {
246
222
  if (!this.requestsTotalLoaded) {
247
223
  this.queuedRequestIncrements += 1;
248
224
  } else {
249
- this.db.incrementRequestsTotal(1).catch((err: unknown) => {
250
- this.log.warn(
251
- "Failed to persist request counter increment: " +
252
- String(err),
253
- );
225
+ this.db.incrementRequestsTotal(1).catch((_err: unknown) => {
226
+ // debugger: failed to persist request counter
254
227
  });
255
228
  }
256
229
 
@@ -261,7 +234,6 @@ export class Spire extends EventEmitter {
261
234
  initApp(
262
235
  this.api,
263
236
  this.db,
264
- this.log,
265
237
  this.validateToken.bind(this),
266
238
  this.signKeys,
267
239
  this.notify.bind(this),
@@ -269,11 +241,9 @@ export class Spire extends EventEmitter {
269
241
 
270
242
  // WS auth: client sends { type: "auth", token } as first message
271
243
  this.wss.on("connection", (ws) => {
272
- this.log.info("WS connection established, waiting for auth...");
273
244
  const AUTH_TIMEOUT = 10_000;
274
245
 
275
246
  const timer = setTimeout(() => {
276
- this.log.warn("WS auth timeout — closing.");
277
247
  ws.close();
278
248
  }, AUTH_TIMEOUT);
279
249
 
@@ -308,47 +278,27 @@ export class Spire extends EventEmitter {
308
278
  }
309
279
  const userDetails: User = jwtResult.data.user;
310
280
 
311
- this.log.info(
312
- "WS auth succeeded for " + userDetails.username,
313
- );
314
-
315
281
  const client = new ClientManager(
316
282
  ws,
317
283
  this.db,
318
284
  this.notify.bind(this),
319
285
  userDetails,
320
- this.options,
321
286
  );
322
287
 
323
288
  client.on("fail", () => {
324
- this.log.info(
325
- "Client connection is down, removing: " +
326
- client.toString(),
327
- );
328
289
  if (this.clients.includes(client)) {
329
290
  this.clients.splice(
330
291
  this.clients.indexOf(client),
331
292
  1,
332
293
  );
333
294
  }
334
- this.log.info(
335
- "Current authorized clients: " +
336
- String(this.clients.length),
337
- );
338
295
  });
339
296
 
340
297
  client.on("authed", () => {
341
- this.log.info(
342
- "New client authorized: " + client.toString(),
343
- );
344
298
  this.clients.push(client);
345
- this.log.info(
346
- "Current authorized clients: " +
347
- String(this.clients.length),
348
- );
349
299
  });
350
- } catch (err: unknown) {
351
- this.log.warn("WS auth failed: " + String(err));
300
+ } catch (_err: unknown) {
301
+ // debugger: WS auth failed
352
302
  const errMsg: BaseMsg = {
353
303
  transmissionID: crypto.randomUUID(),
354
304
  type: "unauthorized",
@@ -421,9 +371,7 @@ export class Spire extends EventEmitter {
421
371
  }
422
372
 
423
373
  try {
424
- this.log.info("New token requested of type " + tokenType);
425
374
  const token = this.createActionToken(scope);
426
- this.log.info("New token created: " + token.key);
427
375
 
428
376
  setTimeout(() => {
429
377
  this.deleteActionToken(token);
@@ -441,8 +389,8 @@ export class Spire extends EventEmitter {
441
389
 
442
390
  res.set("Content-Type", "application/msgpack");
443
391
  return res.send(msgpack.encode(token));
444
- } catch (err: unknown) {
445
- this.log.error(String(err));
392
+ } catch (_err: unknown) {
393
+ // debugger: token creation failed
446
394
  return res.sendStatus(500);
447
395
  }
448
396
  },
@@ -457,7 +405,6 @@ export class Spire extends EventEmitter {
457
405
  res.send(
458
406
  msgpack.encode({
459
407
  exp: req.exp,
460
- token: req.bearerToken,
461
408
  user: req.user,
462
409
  }),
463
410
  );
@@ -477,7 +424,12 @@ export class Spire extends EventEmitter {
477
424
  const checkDurationMs = Date.now() - started;
478
425
 
479
426
  const ok = dbHealthy;
427
+ const canaryEnv = process.env["CANARY"]?.trim().toLowerCase();
480
428
  res.json({
429
+ canary:
430
+ canaryEnv === "1" ||
431
+ canaryEnv === "true" ||
432
+ canaryEnv === "yes",
481
433
  checkDurationMs,
482
434
  commitSha: this.commitSha,
483
435
  dbHealthy,
@@ -532,12 +484,11 @@ export class Spire extends EventEmitter {
532
484
  this.deviceChallenges.delete(challengeID);
533
485
  }, DEVICE_CHALLENGE_EXPIRY);
534
486
 
535
- this.log.info("Device challenge issued for " + deviceID);
536
487
  return res.send(
537
488
  msgpack.encode({ challenge: nonce, challengeID }),
538
489
  );
539
- } catch (err: unknown) {
540
- this.log.error("Device challenge error: " + String(err));
490
+ } catch (_err: unknown) {
491
+ // debugger: device challenge error
541
492
  return res.sendStatus(500);
542
493
  }
543
494
  });
@@ -608,18 +559,11 @@ export class Spire extends EventEmitter {
608
559
  getJwtSecret(),
609
560
  { expiresIn: DEVICE_AUTH_JWT_EXPIRY },
610
561
  );
611
- this.log.info(
612
- "Device-key auth succeeded for " +
613
- user.username +
614
- " (device " +
615
- device.deviceID +
616
- ")",
617
- );
618
562
  return res.send(
619
563
  msgpack.encode({ token, user: censorUser(user) }),
620
564
  );
621
- } catch (err: unknown) {
622
- this.log.error("Device verify error: " + String(err));
565
+ } catch (_err: unknown) {
566
+ // debugger: device verify error
623
567
  return res.sendStatus(500);
624
568
  }
625
569
  });
@@ -648,8 +592,6 @@ export class Spire extends EventEmitter {
648
592
  senderDeviceDetails.deviceID,
649
593
  authorUserDetails.userID,
650
594
  );
651
- this.log.info("Received mail for " + mail.recipient);
652
-
653
595
  const recipientDeviceDetails = await this.db.retrieveDevice(
654
596
  mail.recipient,
655
597
  );
@@ -681,21 +623,25 @@ export class Spire extends EventEmitter {
681
623
  try {
682
624
  const userEntry = await this.db.retrieveUser(username);
683
625
  if (!userEntry) {
684
- res.sendStatus(404);
685
- this.log.warn("User does not exist.");
626
+ res.sendStatus(401);
686
627
  return;
687
628
  }
688
629
 
689
- const salt = XUtils.decodeHex(userEntry.passwordSalt);
690
- const payloadHash = XUtils.encodeHex(
691
- hashPassword(password, salt),
630
+ const { needsRehash, valid } = await verifyPassword(
631
+ password,
632
+ userEntry,
692
633
  );
693
634
 
694
- if (payloadHash !== userEntry.passwordHash) {
635
+ if (!valid) {
695
636
  res.sendStatus(401);
696
637
  return;
697
638
  }
698
639
 
640
+ if (needsRehash) {
641
+ const newHash = await hashPasswordArgon2(password);
642
+ await this.db.rehashPassword(userEntry.userID, newHash);
643
+ }
644
+
699
645
  const token = jwt.sign(
700
646
  { user: censorUser(userEntry) },
701
647
  getJwtSecret(),
@@ -708,8 +654,8 @@ export class Spire extends EventEmitter {
708
654
  res.send(
709
655
  msgpack.encode({ token, user: censorUser(userEntry) }),
710
656
  );
711
- } catch (err: unknown) {
712
- this.log.error(String(err));
657
+ } catch (_err: unknown) {
658
+ // debugger: auth error
713
659
  res.sendStatus(500);
714
660
  }
715
661
  });
@@ -762,9 +708,6 @@ export class Spire extends EventEmitter {
762
708
  "users_signkey_unique",
763
709
  );
764
710
 
765
- this.log.warn(
766
- "User attempted to register duplicate account.",
767
- );
768
711
  if (usernameConflict) {
769
712
  res.status(400).send({
770
713
  error: "Username is already registered.",
@@ -782,16 +725,10 @@ export class Spire extends EventEmitter {
782
725
  });
783
726
  break;
784
727
  default:
785
- this.log.info(
786
- "Unsupported sql error type: " +
787
- String(errCode),
788
- );
789
- this.log.error(String(err));
790
728
  res.sendStatus(500);
791
729
  break;
792
730
  }
793
731
  } else {
794
- this.log.info("Registration success.");
795
732
  if (!user) {
796
733
  res.sendStatus(500);
797
734
  return;
@@ -803,15 +740,13 @@ export class Spire extends EventEmitter {
803
740
  error: "Invalid or no token supplied.",
804
741
  });
805
742
  }
806
- } catch (err: unknown) {
807
- this.log.error("error registering user: " + String(err));
743
+ } catch (_err: unknown) {
744
+ // debugger: registration error
808
745
  res.sendStatus(500);
809
746
  }
810
747
  });
811
748
 
812
- this.server = this.api.listen(apiPort, () => {
813
- this.log.info("API started on port " + String(apiPort));
814
- });
749
+ this.server = this.api.listen(apiPort);
815
750
 
816
751
  // Accept all WS upgrades — auth happens post-connection.
817
752
  this.server.on("upgrade", (req, socket, head) => {
@@ -854,7 +789,6 @@ export class Spire extends EventEmitter {
854
789
  }
855
790
 
856
791
  private validateToken(key: string, scope: TokenScopes): boolean {
857
- this.log.info("Validating token: " + key);
858
792
  for (const rKey of this.actionTokens) {
859
793
  if (rKey.key === key) {
860
794
  if (rKey.scope !== scope) {
@@ -862,17 +796,12 @@ export class Spire extends EventEmitter {
862
796
  }
863
797
 
864
798
  const age = Date.now() - new Date(rKey.time).getTime();
865
- this.log.info("Token found, " + String(age) + " ms old.");
866
799
  if (age < TOKEN_EXPIRY) {
867
- this.log.info("Token is valid.");
868
800
  this.deleteActionToken(rKey);
869
801
  return true;
870
- } else {
871
- this.log.info("Token is expired.");
872
802
  }
873
803
  }
874
804
  }
875
- this.log.info("Token not found.");
876
805
  return false;
877
806
  }
878
807
  }
@@ -5,7 +5,6 @@ import { XUtils } from "@vex-chat/crypto";
5
5
 
6
6
  import * as uuid from "uuid";
7
7
  import { describe, expect, it, vi } from "vitest";
8
- import winston from "winston";
9
8
 
10
9
  import { Database } from "../Database.ts";
11
10
 
@@ -27,19 +26,6 @@ vi.mock("uuid", () => ({
27
26
  validate: () => true,
28
27
  }));
29
28
 
30
- /** Winston logger stub — Database.close() calls `.info`, and `{}` breaks that. */
31
- function silentLogger(): winston.Logger {
32
- const noop = vi.fn();
33
- return {
34
- debug: noop,
35
- error: noop,
36
- info: noop,
37
- log: noop,
38
- verbose: noop,
39
- warn: noop,
40
- } as unknown as winston.Logger;
41
- }
42
-
43
29
  describe("Database", () => {
44
30
  // Reusable test data
45
31
  const keyID = "de459e05-aa63-4dfa-97b4-ed43d5c7a5f7";
@@ -80,9 +66,6 @@ describe("Database", () => {
80
66
  expect.assertions(1);
81
67
 
82
68
  vi.mocked(uuid.v4).mockReturnValueOnce(keyID);
83
- vi.spyOn(winston, "createLogger").mockReturnValueOnce(
84
- silentLogger(),
85
- );
86
69
 
87
70
  const provider = new Database(options);
88
71
  await new Promise<void>((resolve, reject) => {
package/src/db/schema.ts CHANGED
@@ -156,6 +156,7 @@ export type ServiceMetricUpdate = Updateable<ServiceMetricsTable>;
156
156
 
157
157
  export type UserRow = Selectable<UsersTable>;
158
158
  export interface UsersTable {
159
+ hashAlgo: string;
159
160
  lastSeen: string;
160
161
  passwordHash: string;
161
162
  passwordSalt: string;
@@ -0,0 +1,18 @@
1
+ import { type Kysely, sql } from "kysely";
2
+
3
+ export async function down(db: Kysely<unknown>): Promise<void> {
4
+ await db.schema.alterTable("users").dropColumn("hashAlgo").execute();
5
+ }
6
+
7
+ export async function up(db: Kysely<unknown>): Promise<void> {
8
+ await db.schema
9
+ .alterTable("users")
10
+ .addColumn("hashAlgo", "varchar(16)", (cb) =>
11
+ cb.defaultTo("pbkdf2").notNull(),
12
+ )
13
+ .execute();
14
+
15
+ await sql`UPDATE users SET hashAlgo = 'pbkdf2' WHERE hashAlgo = 'pbkdf2'`.execute(
16
+ db,
17
+ );
18
+ }
package/src/run.ts CHANGED
@@ -18,7 +18,6 @@ function main() {
18
18
  new Spire(spk, {
19
19
  ...(apiPort !== undefined ? { apiPort: Number(apiPort) } : {}),
20
20
  ...(dbType !== undefined ? { dbType } : {}),
21
- logLevel: "info",
22
21
  });
23
22
  }
24
23
 
@@ -1,6 +1,3 @@
1
- import type { Database } from "../Database.ts";
2
- import type winston from "winston";
3
-
4
1
  import * as fs from "node:fs";
5
2
  import * as fsp from "node:fs/promises";
6
3
 
@@ -20,7 +17,7 @@ import { ALLOWED_IMAGE_TYPES, protect } from "./index.ts";
20
17
 
21
18
  const safePathParam = z.string().regex(/^[a-zA-Z0-9._-]+$/);
22
19
 
23
- export const getAvatarRouter = (db: Database, log: winston.Logger) => {
20
+ export const getAvatarRouter = () => {
24
21
  const router = express.Router();
25
22
 
26
23
  router.get("/:userID", async (req, res) => {
@@ -39,8 +36,8 @@ export const getAvatarRouter = (db: Database, log: winston.Logger) => {
39
36
  res.set("Cache-control", "public, max-age=31536000");
40
37
 
41
38
  const stream = fs.createReadStream(filePath);
42
- stream.on("error", (err) => {
43
- log.error(err.toString());
39
+ stream.on("error", (_err) => {
40
+ // debugger: avatar stream read error
44
41
  res.sendStatus(500);
45
42
  });
46
43
  stream.pipe(res);
@@ -65,7 +62,6 @@ export const getAvatarRouter = (db: Database, log: winston.Logger) => {
65
62
  }
66
63
 
67
64
  if (!payload.file) {
68
- log.warn("MISSING FILE");
69
65
  res.sendStatus(400);
70
66
  return;
71
67
  }
@@ -84,10 +80,9 @@ export const getAvatarRouter = (db: Database, log: winston.Logger) => {
84
80
  try {
85
81
  // write the file to disk
86
82
  await fsp.writeFile("avatars/" + userDetails.userID, buf);
87
- log.info("Wrote new avatar " + userDetails.userID);
88
83
  res.sendStatus(200);
89
- } catch (err: unknown) {
90
- log.warn(String(err));
84
+ } catch (_err: unknown) {
85
+ // debugger: avatar write failed
91
86
  res.sendStatus(500);
92
87
  }
93
88
  });
@@ -107,7 +102,6 @@ export const getAvatarRouter = (db: Database, log: winston.Logger) => {
107
102
  }
108
103
 
109
104
  if (!req.file) {
110
- log.warn("MISSING FILE");
111
105
  res.sendStatus(400);
112
106
  return;
113
107
  }
@@ -128,10 +122,9 @@ export const getAvatarRouter = (db: Database, log: winston.Logger) => {
128
122
  "avatars/" + userDetails.userID,
129
123
  req.file.buffer,
130
124
  );
131
- log.info("Wrote new avatar " + userDetails.userID);
132
125
  res.sendStatus(200);
133
- } catch (err: unknown) {
134
- log.warn(String(err));
126
+ } catch (_err: unknown) {
127
+ // debugger: avatar write failed
135
128
  res.sendStatus(500);
136
129
  }
137
130
  },
@@ -9,8 +9,7 @@
9
9
  * (which include database internals, file paths, and stack traces)
10
10
  * back to the client. The new pattern: throw `AppError(status, msg)`
11
11
  * or let an unknown error propagate, and the single 4-arg middleware
12
- * below converts it into a JSON response with a generic message
13
- * while logging the full internals server-side via winston.
12
+ * below converts it into a JSON response with a generic message.
14
13
  *
15
14
  * - **CWE-79 / CWE-116 — Exception text reinterpreted as HTML.** Express's
16
15
  * default finalhandler sends thrown `Error` objects as
@@ -27,7 +26,6 @@
27
26
  * `express-async-errors` shim needed.
28
27
  */
29
28
  import type { ErrorRequestHandler } from "express";
30
- import type winston from "winston";
31
29
 
32
30
  import { randomUUID } from "node:crypto";
33
31
 
@@ -39,8 +37,7 @@ import { ZodError } from "zod/v4";
39
37
  * - `status` — HTTP status code (400, 401, 403, 404, 409, etc.)
40
38
  * - `message` — client-safe message, MUST NOT contain request data
41
39
  * (route params, body fields, query strings). Anything operator-
42
- * only (database errors, file paths) belongs in winston logs, not
43
- * in this string.
40
+ * only (database errors, file paths) should not appear in this string.
44
41
  *
45
42
  * Anything that isn't an `AppError` (raw `Error`, `TypeError`, a
46
43
  * rejected promise from a DB query, etc.) is treated by the central
@@ -68,8 +65,7 @@ export class AppError extends Error {
68
65
  * api.use(errorHandler(log));
69
66
  */
70
67
  export const errorHandler =
71
- (log: winston.Logger): ErrorRequestHandler =>
72
- (err, req, res, _next) => {
68
+ (): ErrorRequestHandler => (err, _req, res, _next) => {
73
69
  // If headers already went out there's nothing safe to do except
74
70
  // let Express's default handler close the socket.
75
71
  if (res.headersSent) {
@@ -95,31 +91,6 @@ export const errorHandler =
95
91
  clientMessage = err.message;
96
92
  }
97
93
 
98
- // Log the full internals server-side. `err instanceof Error` also
99
- // catches AppError (extends Error), so we get stacks and messages
100
- // for every branch in the logs.
101
- if (err instanceof Error) {
102
- log.error("request failed", {
103
- error: {
104
- message: err.message,
105
- name: err.name,
106
- stack: err.stack,
107
- },
108
- method: req.method,
109
- path: req.path,
110
- requestId,
111
- status,
112
- });
113
- } else {
114
- log.error("request failed", {
115
- error: String(err),
116
- method: req.method,
117
- path: req.path,
118
- requestId,
119
- status,
120
- });
121
- }
122
-
123
94
  // ALWAYS JSON — prevents the exception-text-as-HTML XSS vector.
124
95
  res.status(status)
125
96
  .type("application/json")
@@ -1,6 +1,5 @@
1
1
  import type { Database } from "../Database.ts";
2
2
  import type { FileSQL } from "@vex-chat/types";
3
- import type winston from "winston";
4
3
 
5
4
  import * as fs from "node:fs";
6
5
  import * as fsp from "node:fs/promises";
@@ -23,7 +22,7 @@ import { protect } from "./index.ts";
23
22
 
24
23
  const safePathParam = z.string().regex(/^[a-zA-Z0-9._-]+$/);
25
24
 
26
- export const getFileRouter = (db: Database, log: winston.Logger) => {
25
+ export const getFileRouter = (db: Database) => {
27
26
  const router = express.Router();
28
27
 
29
28
  router.get("/:id", protect, async (req, res) => {
@@ -37,8 +36,8 @@ export const getFileRouter = (db: Database, log: winston.Logger) => {
37
36
  res.sendStatus(404);
38
37
  } else {
39
38
  const stream = fs.createReadStream("./files/" + entry.fileID);
40
- stream.on("error", (err) => {
41
- log.error(err.toString());
39
+ stream.on("error", (_err) => {
40
+ // debugger: file stream read error
42
41
  res.sendStatus(500);
43
42
  });
44
43
  stream.pipe(res);
@@ -105,12 +104,10 @@ export const getFileRouter = (db: Database, log: winston.Logger) => {
105
104
  const newFile: FileSQL = {
106
105
  fileID: crypto.randomUUID(),
107
106
  nonce: payload.nonce,
108
- owner: payload.owner,
107
+ owner: deviceDetails.deviceID,
109
108
  };
110
109
 
111
110
  await fsp.writeFile("files/" + newFile.fileID, buf);
112
- log.info("Wrote new file " + newFile.fileID);
113
-
114
111
  await db.createFile(newFile);
115
112
  res.send(msgpack.encode(newFile));
116
113
  });
@@ -157,12 +154,10 @@ export const getFileRouter = (db: Database, log: winston.Logger) => {
157
154
  const newFile: FileSQL = {
158
155
  fileID: crypto.randomUUID(),
159
156
  nonce: payload.nonce,
160
- owner: payload.owner,
157
+ owner: deviceDetails.deviceID,
161
158
  };
162
159
 
163
160
  await fsp.writeFile("files/" + newFile.fileID, req.file.buffer);
164
- log.info("Wrote new file " + newFile.fileID);
165
-
166
161
  await db.createFile(newFile);
167
162
  res.send(msgpack.encode(newFile));
168
163
  },