@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.
Files changed (66) hide show
  1. package/README.md +38 -38
  2. package/dist/ClientManager.d.ts +1 -3
  3. package/dist/ClientManager.js +7 -50
  4. package/dist/ClientManager.js.map +1 -1
  5. package/dist/Database.d.ts +49 -6
  6. package/dist/Database.js +255 -80
  7. package/dist/Database.js.map +1 -1
  8. package/dist/Spire.d.ts +0 -3
  9. package/dist/Spire.js +116 -81
  10. package/dist/Spire.js.map +1 -1
  11. package/dist/db/schema.d.ts +1 -0
  12. package/dist/migrations/2026-04-14_argon2id-password-hashing.d.ts +3 -0
  13. package/dist/migrations/2026-04-14_argon2id-password-hashing.js +12 -0
  14. package/dist/migrations/2026-04-14_argon2id-password-hashing.js.map +1 -0
  15. package/dist/run.js +0 -1
  16. package/dist/run.js.map +1 -1
  17. package/dist/server/avatar.d.ts +1 -3
  18. package/dist/server/avatar.js +7 -11
  19. package/dist/server/avatar.js.map +1 -1
  20. package/dist/server/errors.d.ts +3 -6
  21. package/dist/server/errors.js +2 -28
  22. package/dist/server/errors.js.map +1 -1
  23. package/dist/server/file.d.ts +1 -2
  24. package/dist/server/file.js +5 -7
  25. package/dist/server/file.js.map +1 -1
  26. package/dist/server/index.d.ts +1 -2
  27. package/dist/server/index.js +35 -33
  28. package/dist/server/index.js.map +1 -1
  29. package/dist/server/invite.d.ts +1 -2
  30. package/dist/server/invite.js +1 -1
  31. package/dist/server/invite.js.map +1 -1
  32. package/dist/server/rateLimit.d.ts +13 -3
  33. package/dist/server/rateLimit.js +57 -6
  34. package/dist/server/rateLimit.js.map +1 -1
  35. package/dist/server/user.d.ts +1 -2
  36. package/dist/server/user.js +10 -8
  37. package/dist/server/user.js.map +1 -1
  38. package/dist/utils/jwtSecret.d.ts +3 -3
  39. package/dist/utils/jwtSecret.js +5 -6
  40. package/dist/utils/jwtSecret.js.map +1 -1
  41. package/dist/utils/loadEnv.js +1 -1
  42. package/dist/utils/loadEnv.js.map +1 -1
  43. package/package.json +17 -8
  44. package/src/ClientManager.ts +7 -60
  45. package/src/Database.ts +308 -89
  46. package/src/Spire.ts +122 -119
  47. package/src/__tests__/Database.spec.ts +0 -17
  48. package/src/db/schema.ts +1 -0
  49. package/src/migrations/2026-04-14_argon2id-password-hashing.ts +18 -0
  50. package/src/run.ts +0 -1
  51. package/src/server/avatar.ts +7 -14
  52. package/src/server/errors.ts +3 -32
  53. package/src/server/file.ts +5 -10
  54. package/src/server/index.ts +37 -37
  55. package/src/server/invite.ts +0 -2
  56. package/src/server/rateLimit.ts +43 -9
  57. package/src/server/user.ts +9 -9
  58. package/src/utils/jwtSecret.ts +5 -6
  59. package/src/utils/loadEnv.ts +1 -1
  60. package/dist/__tests__/Database.spec.d.ts +0 -1
  61. package/dist/__tests__/Database.spec.js +0 -136
  62. package/dist/__tests__/Database.spec.js.map +0 -1
  63. package/dist/utils/createLogger.d.ts +0 -5
  64. package/dist/utils/createLogger.js +0 -35
  65. package/dist/utils/createLogger.js.map +0 -1
  66. package/src/utils/createLogger.ts +0 -47
package/src/Spire.ts CHANGED
@@ -1,10 +1,10 @@
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";
7
6
  import * as fs from "node:fs";
7
+ import { freemem, loadavg, totalmem } from "node:os";
8
8
  import path from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
10
 
@@ -30,11 +30,10 @@ import { WebSocketServer } from "ws";
30
30
  import { z } from "zod/v4";
31
31
 
32
32
  import { ClientManager } from "./ClientManager.ts";
33
- import { Database, hashPassword } from "./Database.ts";
33
+ import { Database, hashPasswordArgon2, verifyPassword } from "./Database.ts";
34
34
  import { initApp, protect } from "./server/index.ts";
35
- import { authLimiter } from "./server/rateLimit.ts";
35
+ import { authLimiter, devApiKeySkipsRateLimits } from "./server/rateLimit.ts";
36
36
  import { censorUser, getParam, getUser } from "./server/utils.ts";
37
- import { createLogger } from "./utils/createLogger.ts";
38
37
  import { getJwtSecret } from "./utils/jwtSecret.ts";
39
38
  import { msgpack } from "./utils/msgpack.ts";
40
39
 
@@ -130,14 +129,6 @@ const getCommitSha = (): string => {
130
129
  export interface SpireOptions {
131
130
  apiPort?: number;
132
131
  dbType?: "mysql" | "sqlite3" | "sqlite3mem" | "sqlite";
133
- logLevel?:
134
- | "debug"
135
- | "error"
136
- | "http"
137
- | "info"
138
- | "silly"
139
- | "verbose"
140
- | "warn";
141
132
  }
142
133
 
143
134
  export class Spire extends EventEmitter {
@@ -151,9 +142,6 @@ export class Spire extends EventEmitter {
151
142
  string,
152
143
  { deviceID: string; nonce: string; time: number }
153
144
  >();
154
- private log: winston.Logger;
155
- private options: SpireOptions | undefined;
156
-
157
145
  private queuedRequestIncrements = 0;
158
146
  private requestsTotal = 0;
159
147
 
@@ -164,7 +152,10 @@ export class Spire extends EventEmitter {
164
152
 
165
153
  private readonly startedAt = new Date();
166
154
  private readonly version = getAppVersion();
167
- private wss: WebSocketServer = new WebSocketServer({ noServer: true });
155
+ private wss: WebSocketServer = new WebSocketServer({
156
+ maxPayload: 4096,
157
+ noServer: true,
158
+ });
168
159
 
169
160
  constructor(SK: string, options?: SpireOptions) {
170
161
  super();
@@ -180,17 +171,12 @@ export class Spire extends EventEmitter {
180
171
  this.db = new Database(options);
181
172
  this.db.on("ready", () => {
182
173
  this.dbReady = true;
183
- this.bootstrapRequestCounter().catch((err: unknown) => {
184
- this.log.error(
185
- "Failed to load persisted request counter: " + String(err),
186
- );
174
+ this.bootstrapRequestCounter().catch((_err: unknown) => {
175
+ // debugger: bootstrap request counter failed
187
176
  });
188
177
  });
189
178
 
190
- this.log = createLogger("spire", options?.logLevel || "error");
191
179
  this.init(options?.apiPort || 16777);
192
-
193
- this.options = options;
194
180
  }
195
181
 
196
182
  public async close(): Promise<void> {
@@ -198,29 +184,29 @@ export class Spire extends EventEmitter {
198
184
  ws.terminate();
199
185
  });
200
186
 
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
187
  this.server?.close();
210
188
  this.wss.close();
211
189
  await this.db.close();
212
- return;
213
190
  }
214
191
 
215
192
  private async bootstrapRequestCounter(): Promise<void> {
216
193
  const persistedTotal = await this.db.getRequestsTotal();
194
+
195
+ // Between the await above and this synchronous block, requests may
196
+ // have incremented both `requestsTotal` and `queuedRequestIncrements`.
197
+ // Capture the queue, mark loaded, then merge — never overwrite
198
+ // `requestsTotal` (which already includes in-flight increments).
217
199
  const startupIncrements = this.queuedRequestIncrements;
218
200
  this.queuedRequestIncrements = 0;
219
- this.requestsTotal = persistedTotal + startupIncrements;
201
+ this.requestsTotalLoaded = true;
202
+
203
+ // Add the persisted baseline on top of whatever the middleware
204
+ // already counted in-memory, instead of overwriting it.
205
+ this.requestsTotal += persistedTotal;
206
+
220
207
  if (startupIncrements > 0) {
221
208
  await this.db.incrementRequestsTotal(startupIncrements);
222
209
  }
223
- this.requestsTotalLoaded = true;
224
210
  }
225
211
 
226
212
  private createActionToken(scope: TokenScopes): ActionToken {
@@ -234,8 +220,9 @@ export class Spire extends EventEmitter {
234
220
  }
235
221
 
236
222
  private deleteActionToken(key: ActionToken) {
237
- if (this.actionTokens.includes(key)) {
238
- this.actionTokens.splice(this.actionTokens.indexOf(key), 1);
223
+ const idx = this.actionTokens.indexOf(key);
224
+ if (idx !== -1) {
225
+ this.actionTokens.splice(idx, 1);
239
226
  }
240
227
  }
241
228
 
@@ -246,11 +233,8 @@ export class Spire extends EventEmitter {
246
233
  if (!this.requestsTotalLoaded) {
247
234
  this.queuedRequestIncrements += 1;
248
235
  } else {
249
- this.db.incrementRequestsTotal(1).catch((err: unknown) => {
250
- this.log.warn(
251
- "Failed to persist request counter increment: " +
252
- String(err),
253
- );
236
+ this.db.incrementRequestsTotal(1).catch((_err: unknown) => {
237
+ // debugger: failed to persist request counter
254
238
  });
255
239
  }
256
240
 
@@ -261,7 +245,6 @@ export class Spire extends EventEmitter {
261
245
  initApp(
262
246
  this.api,
263
247
  this.db,
264
- this.log,
265
248
  this.validateToken.bind(this),
266
249
  this.signKeys,
267
250
  this.notify.bind(this),
@@ -269,11 +252,9 @@ export class Spire extends EventEmitter {
269
252
 
270
253
  // WS auth: client sends { type: "auth", token } as first message
271
254
  this.wss.on("connection", (ws) => {
272
- this.log.info("WS connection established, waiting for auth...");
273
255
  const AUTH_TIMEOUT = 10_000;
274
256
 
275
257
  const timer = setTimeout(() => {
276
- this.log.warn("WS auth timeout — closing.");
277
258
  ws.close();
278
259
  }, AUTH_TIMEOUT);
279
260
 
@@ -308,47 +289,25 @@ export class Spire extends EventEmitter {
308
289
  }
309
290
  const userDetails: User = jwtResult.data.user;
310
291
 
311
- this.log.info(
312
- "WS auth succeeded for " + userDetails.username,
313
- );
314
-
315
292
  const client = new ClientManager(
316
293
  ws,
317
294
  this.db,
318
295
  this.notify.bind(this),
319
296
  userDetails,
320
- this.options,
321
297
  );
322
298
 
323
299
  client.on("fail", () => {
324
- this.log.info(
325
- "Client connection is down, removing: " +
326
- client.toString(),
327
- );
328
- if (this.clients.includes(client)) {
329
- this.clients.splice(
330
- this.clients.indexOf(client),
331
- 1,
332
- );
300
+ const idx = this.clients.indexOf(client);
301
+ if (idx !== -1) {
302
+ this.clients.splice(idx, 1);
333
303
  }
334
- this.log.info(
335
- "Current authorized clients: " +
336
- String(this.clients.length),
337
- );
338
304
  });
339
305
 
340
306
  client.on("authed", () => {
341
- this.log.info(
342
- "New client authorized: " + client.toString(),
343
- );
344
307
  this.clients.push(client);
345
- this.log.info(
346
- "Current authorized clients: " +
347
- String(this.clients.length),
348
- );
349
308
  });
350
- } catch (err: unknown) {
351
- this.log.warn("WS auth failed: " + String(err));
309
+ } catch (_err: unknown) {
310
+ // debugger: WS auth failed
352
311
  const errMsg: BaseMsg = {
353
312
  transmissionID: crypto.randomUUID(),
354
313
  type: "unauthorized",
@@ -421,9 +380,7 @@ export class Spire extends EventEmitter {
421
380
  }
422
381
 
423
382
  try {
424
- this.log.info("New token requested of type " + tokenType);
425
383
  const token = this.createActionToken(scope);
426
- this.log.info("New token created: " + token.key);
427
384
 
428
385
  setTimeout(() => {
429
386
  this.deleteActionToken(token);
@@ -441,8 +398,8 @@ export class Spire extends EventEmitter {
441
398
 
442
399
  res.set("Content-Type", "application/msgpack");
443
400
  return res.send(msgpack.encode(token));
444
- } catch (err: unknown) {
445
- this.log.error(String(err));
401
+ } catch (_err: unknown) {
402
+ // debugger: token creation failed
446
403
  return res.sendStatus(500);
447
404
  }
448
405
  },
@@ -457,7 +414,6 @@ export class Spire extends EventEmitter {
457
414
  res.send(
458
415
  msgpack.encode({
459
416
  exp: req.exp,
460
- token: req.bearerToken,
461
417
  user: req.user,
462
418
  }),
463
419
  );
@@ -477,7 +433,12 @@ export class Spire extends EventEmitter {
477
433
  const checkDurationMs = Date.now() - started;
478
434
 
479
435
  const ok = dbHealthy;
436
+ const canaryEnv = process.env["CANARY"]?.trim().toLowerCase();
480
437
  res.json({
438
+ canary:
439
+ canaryEnv === "1" ||
440
+ canaryEnv === "true" ||
441
+ canaryEnv === "yes",
481
442
  checkDurationMs,
482
443
  commitSha: this.commitSha,
483
444
  dbHealthy,
@@ -496,6 +457,68 @@ export class Spire extends EventEmitter {
496
457
  });
497
458
  });
498
459
 
460
+ /**
461
+ * Dev-only process snapshot (same gate as rate-limit bypass: `DEV_API_KEY`
462
+ * env + `x-dev-api-key` header). Returns 404 when not enabled or key wrong
463
+ * so the route is not advertised to anonymous callers.
464
+ * Lets local stress / `sample <pid>` workflows see RSS, WS count, etc.
465
+ */
466
+ this.api.get("/status/process", (req, res) => {
467
+ if (!devApiKeySkipsRateLimits(req)) {
468
+ res.sendStatus(404);
469
+ return;
470
+ }
471
+ const mu = process.memoryUsage();
472
+ const ru = process.resourceUsage();
473
+ res.json({
474
+ activeRequestsApprox: this.requestsTotal,
475
+ dbReady: this.dbReady,
476
+ hostOs: {
477
+ freemem: freemem(),
478
+ loadavg: loadavg(),
479
+ totalmem: totalmem(),
480
+ },
481
+ memory: {
482
+ arrayBuffers: mu.arrayBuffers,
483
+ external: mu.external,
484
+ heapTotal: mu.heapTotal,
485
+ heapUsed: mu.heapUsed,
486
+ rss: mu.rss,
487
+ },
488
+ pid: process.pid,
489
+ resourceUsage: {
490
+ fsRead: ru.fsRead,
491
+ fsWrite: ru.fsWrite,
492
+ maxRSS: ru.maxRSS,
493
+ systemMicros: ru.systemCPUTime,
494
+ userMicros: ru.userCPUTime,
495
+ },
496
+ uptimeSeconds: Math.floor(process.uptime()),
497
+ websocketClients: this.wss.clients.size,
498
+ });
499
+ });
500
+
501
+ /**
502
+ * Dev-only SQLite file + pragma snapshot (same gate as `/status/process`).
503
+ */
504
+ this.api.get("/status/sqlite", (req, res) => {
505
+ if (!devApiKeySkipsRateLimits(req)) {
506
+ res.sendStatus(404);
507
+ return;
508
+ }
509
+ if (!this.dbReady) {
510
+ res.status(503).json({ dbReady: false, ok: false });
511
+ return;
512
+ }
513
+ try {
514
+ const sqlite = this.db.getDevSqliteMonitor();
515
+ res.json({ ok: true, sqlite });
516
+ } catch (err: unknown) {
517
+ const msg = err instanceof Error ? err.message : String(err);
518
+ res.status(500).json({ error: msg, ok: false });
519
+ }
520
+ });
521
+
499
522
  this.api.post("/goodbye", protect, (req, res) => {
500
523
  jwt.sign({ user: req.user }, getJwtSecret(), { expiresIn: -1 });
501
524
  res.sendStatus(200);
@@ -532,12 +555,11 @@ export class Spire extends EventEmitter {
532
555
  this.deviceChallenges.delete(challengeID);
533
556
  }, DEVICE_CHALLENGE_EXPIRY);
534
557
 
535
- this.log.info("Device challenge issued for " + deviceID);
536
558
  return res.send(
537
559
  msgpack.encode({ challenge: nonce, challengeID }),
538
560
  );
539
- } catch (err: unknown) {
540
- this.log.error("Device challenge error: " + String(err));
561
+ } catch (_err: unknown) {
562
+ // debugger: device challenge error
541
563
  return res.sendStatus(500);
542
564
  }
543
565
  });
@@ -608,18 +630,11 @@ export class Spire extends EventEmitter {
608
630
  getJwtSecret(),
609
631
  { expiresIn: DEVICE_AUTH_JWT_EXPIRY },
610
632
  );
611
- this.log.info(
612
- "Device-key auth succeeded for " +
613
- user.username +
614
- " (device " +
615
- device.deviceID +
616
- ")",
617
- );
618
633
  return res.send(
619
634
  msgpack.encode({ token, user: censorUser(user) }),
620
635
  );
621
- } catch (err: unknown) {
622
- this.log.error("Device verify error: " + String(err));
636
+ } catch (_err: unknown) {
637
+ // debugger: device verify error
623
638
  return res.sendStatus(500);
624
639
  }
625
640
  });
@@ -648,8 +663,6 @@ export class Spire extends EventEmitter {
648
663
  senderDeviceDetails.deviceID,
649
664
  authorUserDetails.userID,
650
665
  );
651
- this.log.info("Received mail for " + mail.recipient);
652
-
653
666
  const recipientDeviceDetails = await this.db.retrieveDevice(
654
667
  mail.recipient,
655
668
  );
@@ -681,21 +694,25 @@ export class Spire extends EventEmitter {
681
694
  try {
682
695
  const userEntry = await this.db.retrieveUser(username);
683
696
  if (!userEntry) {
684
- res.sendStatus(404);
685
- this.log.warn("User does not exist.");
697
+ res.sendStatus(401);
686
698
  return;
687
699
  }
688
700
 
689
- const salt = XUtils.decodeHex(userEntry.passwordSalt);
690
- const payloadHash = XUtils.encodeHex(
691
- hashPassword(password, salt),
701
+ const { needsRehash, valid } = await verifyPassword(
702
+ password,
703
+ userEntry,
692
704
  );
693
705
 
694
- if (payloadHash !== userEntry.passwordHash) {
706
+ if (!valid) {
695
707
  res.sendStatus(401);
696
708
  return;
697
709
  }
698
710
 
711
+ if (needsRehash) {
712
+ const newHash = await hashPasswordArgon2(password);
713
+ await this.db.rehashPassword(userEntry.userID, newHash);
714
+ }
715
+
699
716
  const token = jwt.sign(
700
717
  { user: censorUser(userEntry) },
701
718
  getJwtSecret(),
@@ -708,8 +725,8 @@ export class Spire extends EventEmitter {
708
725
  res.send(
709
726
  msgpack.encode({ token, user: censorUser(userEntry) }),
710
727
  );
711
- } catch (err: unknown) {
712
- this.log.error(String(err));
728
+ } catch (_err: unknown) {
729
+ // debugger: auth error
713
730
  res.sendStatus(500);
714
731
  }
715
732
  });
@@ -762,9 +779,6 @@ export class Spire extends EventEmitter {
762
779
  "users_signkey_unique",
763
780
  );
764
781
 
765
- this.log.warn(
766
- "User attempted to register duplicate account.",
767
- );
768
782
  if (usernameConflict) {
769
783
  res.status(400).send({
770
784
  error: "Username is already registered.",
@@ -782,16 +796,10 @@ export class Spire extends EventEmitter {
782
796
  });
783
797
  break;
784
798
  default:
785
- this.log.info(
786
- "Unsupported sql error type: " +
787
- String(errCode),
788
- );
789
- this.log.error(String(err));
790
799
  res.sendStatus(500);
791
800
  break;
792
801
  }
793
802
  } else {
794
- this.log.info("Registration success.");
795
803
  if (!user) {
796
804
  res.sendStatus(500);
797
805
  return;
@@ -803,15 +811,13 @@ export class Spire extends EventEmitter {
803
811
  error: "Invalid or no token supplied.",
804
812
  });
805
813
  }
806
- } catch (err: unknown) {
807
- this.log.error("error registering user: " + String(err));
814
+ } catch (_err: unknown) {
815
+ // debugger: registration error
808
816
  res.sendStatus(500);
809
817
  }
810
818
  });
811
819
 
812
- this.server = this.api.listen(apiPort, () => {
813
- this.log.info("API started on port " + String(apiPort));
814
- });
820
+ this.server = this.api.listen(apiPort);
815
821
 
816
822
  // Accept all WS upgrades — auth happens post-connection.
817
823
  this.server.on("upgrade", (req, socket, head) => {
@@ -828,7 +834,10 @@ export class Spire extends EventEmitter {
828
834
  data?: unknown,
829
835
  deviceID?: string,
830
836
  ): void {
831
- for (const client of this.clients) {
837
+ // Snapshot the array so that a synchronous `fail` → splice inside
838
+ // client.send() doesn't corrupt the iteration.
839
+ const snapshot = this.clients.slice();
840
+ for (const client of snapshot) {
832
841
  if (deviceID) {
833
842
  if (client.getDevice().deviceID === deviceID) {
834
843
  const msg: NotifyMsg = {
@@ -854,7 +863,6 @@ export class Spire extends EventEmitter {
854
863
  }
855
864
 
856
865
  private validateToken(key: string, scope: TokenScopes): boolean {
857
- this.log.info("Validating token: " + key);
858
866
  for (const rKey of this.actionTokens) {
859
867
  if (rKey.key === key) {
860
868
  if (rKey.scope !== scope) {
@@ -862,17 +870,12 @@ export class Spire extends EventEmitter {
862
870
  }
863
871
 
864
872
  const age = Date.now() - new Date(rKey.time).getTime();
865
- this.log.info("Token found, " + String(age) + " ms old.");
866
873
  if (age < TOKEN_EXPIRY) {
867
- this.log.info("Token is valid.");
868
874
  this.deleteActionToken(rKey);
869
875
  return true;
870
- } else {
871
- this.log.info("Token is expired.");
872
876
  }
873
877
  }
874
878
  }
875
- this.log.info("Token not found.");
876
879
  return false;
877
880
  }
878
881
  }
@@ -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
  },