@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
@@ -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
  },
@@ -1,6 +1,5 @@
1
1
  import type { Database } from "../Database.ts";
2
2
  import type { Emoji } 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";
@@ -15,7 +14,6 @@ import cors from "cors";
15
14
  import { fileTypeFromBuffer, fileTypeFromFile } from "file-type";
16
15
  import helmet from "helmet";
17
16
  import jwt from "jsonwebtoken";
18
- import morgan from "morgan";
19
17
  import multer from "multer";
20
18
  import parseDuration from "parse-duration";
21
19
  import { stringify as uuidStringify } from "uuid";
@@ -156,8 +154,6 @@ export const msgpackParser: express.RequestHandler = (req, res, next) => {
156
154
  next();
157
155
  };
158
156
 
159
- const isProduction = process.env["NODE_ENV"] === "production";
160
-
161
157
  const directories = ["files", "avatars"];
162
158
  for (const dir of directories) {
163
159
  if (!fs.existsSync(dir)) {
@@ -168,7 +164,6 @@ for (const dir of directories) {
168
164
  export const initApp = (
169
165
  api: express.Application,
170
166
  db: Database,
171
- log: winston.Logger,
172
167
  tokenValidator: (key: string, scope: TokenScopes) => boolean,
173
168
  signKeys: KeyPair,
174
169
  notify: (
@@ -180,10 +175,10 @@ export const initApp = (
180
175
  ) => void,
181
176
  ) => {
182
177
  // INIT ROUTERS
183
- const userRouter = getUserRouter(db, log, tokenValidator);
184
- const fileRouter = getFileRouter(db, log);
185
- const avatarRouter = getAvatarRouter(db, log);
186
- const inviteRouter = getInviteRouter(db, log, tokenValidator, notify);
178
+ const userRouter = getUserRouter(db, tokenValidator);
179
+ const fileRouter = getFileRouter(db);
180
+ const avatarRouter = getAvatarRouter();
181
+ const inviteRouter = getInviteRouter(db, tokenValidator, notify);
187
182
 
188
183
  // MIDDLEWARE
189
184
  // Global per-IP rate limit is the FIRST middleware so a flooded
@@ -197,18 +192,32 @@ export const initApp = (
197
192
  type: "application/msgpack",
198
193
  }),
199
194
  );
200
- if (isProduction) {
201
- api.use(helmet());
202
- }
195
+ api.use(helmet());
203
196
  api.use(msgpackParser);
204
197
  api.use(checkAuth);
205
198
  api.use(checkDevice);
206
199
 
207
- if (!jestRun()) {
208
- api.use(morgan("dev", { stream: process.stdout }));
209
- }
210
-
211
- api.use(cors({ credentials: true }));
200
+ // Browser clients (web, Tauri, Capacitor, etc.) hit Spire from arbitrary
201
+ // origins when self-hosted or embedded in third-party apps. libvex uses
202
+ // `Authorization: Bearer` only (no cookies), so reflecting `Origin` is the
203
+ // usual bearer-API pattern. Set `CORS_ORIGINS` to a comma-separated allowlist
204
+ // when an operator wants to restrict which frontends may call the API.
205
+ const corsRaw = process.env["CORS_ORIGINS"];
206
+ const corsOrigins = corsRaw
207
+ ? corsRaw
208
+ .split(",")
209
+ .map((o) => o.trim())
210
+ .filter((o) => o.length > 0)
211
+ : [];
212
+ api.use(
213
+ cors({
214
+ credentials: true,
215
+ origin:
216
+ corsOrigins.length > 0
217
+ ? corsOrigins
218
+ : true /* reflect request Origin */,
219
+ }),
220
+ );
212
221
 
213
222
  api.get("/server/:id", protect, async (req, res) => {
214
223
  const server = await db.retrieveServer(getParam(req, "id"));
@@ -259,7 +268,6 @@ export const initApp = (
259
268
  POWER_LEVELS.INVITE,
260
269
  )
261
270
  ) {
262
- log.warn("No permission!");
263
271
  res.sendStatus(401);
264
272
  return;
265
273
  }
@@ -649,8 +657,8 @@ export const initApp = (
649
657
  res.set("Cache-control", "public, max-age=31536000");
650
658
 
651
659
  const stream = fs.createReadStream(filePath);
652
- stream.on("error", (err) => {
653
- log.error(err.toString());
660
+ stream.on("error", (_err) => {
661
+ // debugger: emoji stream read error
654
662
  res.sendStatus(500);
655
663
  });
656
664
  stream.pipe(res);
@@ -703,10 +711,11 @@ export const initApp = (
703
711
  }
704
712
  if (!payload.name) {
705
713
  res.sendStatus(400);
714
+ return;
706
715
  }
707
716
  if (Buffer.byteLength(buf) > 256000) {
708
- log.warn("File too big.");
709
717
  res.sendStatus(413);
718
+ return;
710
719
  }
711
720
 
712
721
  const mimeType = await fileTypeFromBuffer(buf);
@@ -730,10 +739,9 @@ export const initApp = (
730
739
  try {
731
740
  // write the file to disk
732
741
  await fsp.writeFile("emoji/" + emoji.emojiID, buf);
733
- log.info("Wrote new emoji " + emoji.emojiID);
734
742
  res.send(msgpack.encode(emoji));
735
- } catch (err: unknown) {
736
- log.warn(String(err));
743
+ } catch (_err: unknown) {
744
+ // debugger: emoji write failed
737
745
  res.sendStatus(500);
738
746
  }
739
747
  });
@@ -786,17 +794,17 @@ export const initApp = (
786
794
 
787
795
  if (!payload.name) {
788
796
  res.sendStatus(400);
797
+ return;
789
798
  }
790
799
 
791
800
  if (!req.file) {
792
- log.warn("MISSING FILE");
793
801
  res.sendStatus(400);
794
802
  return;
795
803
  }
796
804
 
797
805
  if (Buffer.byteLength(req.file.buffer) > 256000) {
798
- log.warn("File too big.");
799
806
  res.sendStatus(413);
807
+ return;
800
808
  }
801
809
 
802
810
  const mimeType = await fileTypeFromBuffer(req.file.buffer);
@@ -820,10 +828,9 @@ export const initApp = (
820
828
  try {
821
829
  // write the file to disk
822
830
  await fsp.writeFile("emoji/" + emoji.emojiID, req.file.buffer);
823
- log.info("Wrote new emoji " + emoji.emojiID);
824
831
  res.send(msgpack.encode(emoji));
825
- } catch (err: unknown) {
826
- log.warn(String(err));
832
+ } catch (_err: unknown) {
833
+ // debugger: emoji write failed
827
834
  res.sendStatus(500);
828
835
  }
829
836
  },
@@ -844,12 +851,5 @@ export const initApp = (
844
851
  // (client-safe status + message) and programmer errors (generic 500
845
852
  // with full details logged server-side). See src/server/errors.ts
846
853
  // for the CWE mapping.
847
- api.use(errorHandler(log));
848
- };
849
-
850
- /**
851
- * @ignore
852
- */
853
- const jestRun = () => {
854
- return process.env["JEST_WORKER_ID"] !== undefined;
854
+ api.use(errorHandler());
855
855
  };
@@ -1,6 +1,5 @@
1
1
  import type { Database } from "../Database.ts";
2
2
  import type { TokenScopes } from "@vex-chat/types";
3
- import type winston from "winston";
4
3
 
5
4
  import express from "express";
6
5
 
@@ -12,7 +11,6 @@ import { protect } from "./index.ts";
12
11
 
13
12
  export const getInviteRouter = (
14
13
  db: Database,
15
- log: winston.Logger,
16
14
  tokenValidator: (key: string, scope: TokenScopes) => boolean,
17
15
  notify: (
18
16
  userID: string,
@@ -1,3 +1,37 @@
1
+ import type { Request } from "express";
2
+
3
+ import { timingSafeEqual } from "node:crypto";
4
+
5
+ import rateLimit, { ipKeyGenerator } from "express-rate-limit";
6
+
7
+ /** HTTP header carrying the dev API key (must match {@link process.env.DEV_API_KEY}). */
8
+ export const DEV_API_KEY_HEADER = "x-dev-api-key";
9
+
10
+ /**
11
+ * When `DEV_API_KEY` is set in the environment, any request whose
12
+ * `x-dev-api-key` header matches (constant-time) skips all in-process rate
13
+ * limiters. Dev / load-testing escape hatch only — never set in production.
14
+ * (Future: first-class API keys with scopes may reuse this header name.)
15
+ */
16
+ export function devApiKeySkipsRateLimits(req: Request): boolean {
17
+ const configured = process.env["DEV_API_KEY"]?.trim() ?? "";
18
+ if (configured.length === 0) {
19
+ return false;
20
+ }
21
+ const presented = req.get(DEV_API_KEY_HEADER);
22
+ if (!presented || presented.length !== configured.length) {
23
+ return false;
24
+ }
25
+ try {
26
+ return timingSafeEqual(
27
+ Buffer.from(presented, "utf8"),
28
+ Buffer.from(configured, "utf8"),
29
+ );
30
+ } catch {
31
+ return false;
32
+ }
33
+ }
34
+
1
35
  /**
2
36
  * Rate limiting middleware.
3
37
  *
@@ -21,9 +55,6 @@
21
55
  * `trust proxy` must be set on the Express app (see Spire.ts) so
22
56
  * `req.ip` returns the real client address, not the immediate proxy.
23
57
  */
24
- import type { Request } from "express";
25
-
26
- import rateLimit, { ipKeyGenerator } from "express-rate-limit";
27
58
 
28
59
  /**
29
60
  * Bucket requests by the real client IP, IPv6-safe.
@@ -38,14 +69,15 @@ const keyByIp = (req: Request): string => ipKeyGenerator(req.ip ?? "");
38
69
  /**
39
70
  * Global per-IP limiter. Applied app-wide via `api.use(globalLimiter)`.
40
71
  *
41
- * 300 requests per 15 minutes per client IP. A human chatting via a
72
+ * 3000 requests per 15 minutes per client IP. A human chatting via a
42
73
  * browser or the libvex client won't come close; a single-host DoS
43
74
  * gets throttled quickly.
44
75
  */
45
76
  export const globalLimiter = rateLimit({
46
77
  keyGenerator: keyByIp,
47
78
  legacyHeaders: false,
48
- limit: 300,
79
+ limit: 3000,
80
+ skip: devApiKeySkipsRateLimits,
49
81
  standardHeaders: "draft-7",
50
82
  windowMs: 15 * 60 * 1000,
51
83
  });
@@ -54,7 +86,7 @@ export const globalLimiter = rateLimit({
54
86
  * Strict auth endpoint limiter. Applied per-route to /auth, /register,
55
87
  * and /auth/device.
56
88
  *
57
- * 5 failed attempts per 15 minutes per IP. Successful logins don't
89
+ * 50 failed attempts per 15 minutes per IP. Successful logins don't
58
90
  * count (`skipSuccessfulRequests`), so a normal user doesn't lock
59
91
  * themselves out by fat-fingering a password once. Blocks brute force
60
92
  * (CWE-307) without harming UX.
@@ -62,7 +94,8 @@ export const globalLimiter = rateLimit({
62
94
  export const authLimiter = rateLimit({
63
95
  keyGenerator: keyByIp,
64
96
  legacyHeaders: false,
65
- limit: 5,
97
+ limit: 50,
98
+ skip: devApiKeySkipsRateLimits,
66
99
  skipSuccessfulRequests: true,
67
100
  standardHeaders: "draft-7",
68
101
  windowMs: 15 * 60 * 1000,
@@ -74,13 +107,14 @@ export const authLimiter = rateLimit({
74
107
  * upload attempts per minute so an attacker can't force spire to
75
108
  * spend CPU/IO on repeated large-body parses.
76
109
  *
77
- * 20 uploads per minute per IP — generous for a chat client (rapid-
110
+ * 200 uploads per minute per IP — generous for a chat client (rapid-
78
111
  * fire image attachments) but tight enough to shield the disk.
79
112
  */
80
113
  export const uploadLimiter = rateLimit({
81
114
  keyGenerator: keyByIp,
82
115
  legacyHeaders: false,
83
- limit: 20,
116
+ limit: 200,
117
+ skip: devApiKeySkipsRateLimits,
84
118
  standardHeaders: "draft-7",
85
119
  windowMs: 60 * 1000,
86
120
  });
@@ -1,5 +1,4 @@
1
1
  import type { Database } from "../Database.ts";
2
- import type winston from "winston";
3
2
 
4
3
  import express from "express";
5
4
 
@@ -17,7 +16,6 @@ import { protect } from "./index.ts";
17
16
 
18
17
  export const getUserRouter = (
19
18
  db: Database,
20
- log: winston.Logger,
21
19
  tokenValidator: (key: string, scope: TokenScopes) => boolean,
22
20
  ) => {
23
21
  const router = express.Router();
@@ -33,9 +31,13 @@ export const getUserRouter = (
33
31
  });
34
32
 
35
33
  router.get("/:id/devices", protect, async (req, res) => {
36
- const deviceList = await db.retrieveUserDeviceList([
37
- getParam(req, "id"),
38
- ]);
34
+ const id = getParam(req, "id");
35
+ const user = await db.retrieveUser(id);
36
+ if (!user) {
37
+ res.sendStatus(404);
38
+ return;
39
+ }
40
+ const deviceList = await db.retrieveUserDeviceList([id]);
39
41
  return res.send(msgpack.encode(deviceList));
40
42
  });
41
43
 
@@ -98,7 +100,6 @@ export const getUserRouter = (
98
100
  );
99
101
 
100
102
  if (!token) {
101
- log.warn("Invalid signature on token.");
102
103
  res.sendStatus(400);
103
104
  return;
104
105
  }
@@ -110,9 +111,8 @@ export const getUserRouter = (
110
111
  deviceData,
111
112
  );
112
113
  res.send(msgpack.encode(device));
113
- } catch (err: unknown) {
114
- log.warn(String(err));
115
- // failed registration due to signkey being taken
114
+ } catch (_err: unknown) {
115
+ // signkey already taken
116
116
  res.sendStatus(470);
117
117
  return;
118
118
  }
@@ -1,15 +1,14 @@
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(): string {
8
- const secret = process.env["JWT_SECRET"] ?? process.env["SPK"];
8
+ const secret = process.env["JWT_SECRET"];
9
9
  if (!secret) {
10
10
  throw new Error(
11
- "Neither JWT_SECRET nor SPK is set. " +
12
- "Set JWT_SECRET (preferred) or SPK in your environment.",
11
+ "JWT_SECRET is not set. Generate one with: node scripts/gen-spk.js",
13
12
  );
14
13
  }
15
14
  return secret;
@@ -3,7 +3,7 @@ import { config } from "dotenv";
3
3
  /* Populate process.env with vars from .env and verify required vars are present. */
4
4
  export function loadEnv(): void {
5
5
  config();
6
- const requiredEnvVars: string[] = ["CANARY", "DB_TYPE", "SPK"];
6
+ const requiredEnvVars: string[] = ["DB_TYPE", "JWT_SECRET", "SPK"];
7
7
  for (const required of requiredEnvVars) {
8
8
  if (process.env[required] === undefined) {
9
9
  process.stderr.write(
@@ -1 +0,0 @@
1
- export {};
@@ -1,136 +0,0 @@
1
- import { XUtils } from "@vex-chat/crypto";
2
- import * as uuid from "uuid";
3
- import { describe, expect, it, vi } from "vitest";
4
- import winston from "winston";
5
- import { Database } from "../Database.js";
6
- // vi.mock is hoisted above all imports automatically.
7
- // Minimal stubs for uuid functions used by spire src: v4, parse, stringify.
8
- vi.mock("uuid", () => ({
9
- parse: (s) => {
10
- const matches = s.replace(/-/g, "").match(/.{2}/g);
11
- if (!matches)
12
- throw new Error("Invalid UUID");
13
- return Uint8Array.from(matches.map((b) => parseInt(b, 16)));
14
- },
15
- stringify: (b) => {
16
- const hex = Array.from(b)
17
- .map((x) => x.toString(16).padStart(2, "0"))
18
- .join("");
19
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
20
- },
21
- v4: vi.fn(() => "93ce482b-a0f2-4f6e-b1df-3aed61073552"),
22
- validate: () => true,
23
- }));
24
- /** Winston logger stub — Database.close() calls `.info`, and `{}` breaks that. */
25
- function silentLogger() {
26
- const noop = vi.fn();
27
- return {
28
- debug: noop,
29
- error: noop,
30
- info: noop,
31
- log: noop,
32
- verbose: noop,
33
- warn: noop,
34
- };
35
- }
36
- describe("Database", () => {
37
- // Reusable test data
38
- const keyID = "de459e05-aa63-4dfa-97b4-ed43d5c7a5f7";
39
- const userID = "4e67b90f-cbf8-44bc-8ce3-d3b248f033f1";
40
- const deviceID = "23cb0b27-7d0c-43b2-87e1-c2b93e0095e5";
41
- const publicKey = XUtils.decodeHex("30c2d0294c1cfdbb73c6b3bbe6010088c2dba8384b04ff2e2b92172431d66b5e");
42
- const signature = XUtils.decodeHex("dd0665079426c3efcf4dce9b1487e4aca132f8147581b3294c3f23ddd2b4ba8240a10082bd06805d7eb320d91af971da3306e11b60073ccc3d829710f5036004000030c2d0294c1cfdbb73c6b3bbe6010088c2dba8384b04ff2e2b92172431d66b5e");
43
- const testSQLPreKey = {
44
- deviceID,
45
- index: 1,
46
- keyID,
47
- publicKey: "30c2d0294c1cfdbb73c6b3bbe6010088c2dba8384b04ff2e2b92172431d66b5e",
48
- signature: "dd0665079426c3efcf4dce9b1487e4aca132f8147581b3294c3f23ddd2b4ba8240a10082bd06805d7eb320d91af971da3306e11b60073ccc3d829710f5036004000030c2d0294c1cfdbb73c6b3bbe6010088c2dba8384b04ff2e2b92172431d66b5e",
49
- userID,
50
- };
51
- const testWSPreKey = {
52
- deviceID,
53
- index: 1,
54
- publicKey,
55
- signature,
56
- };
57
- const options = {
58
- dbType: "sqlite3mem",
59
- };
60
- describe("saveOTK", () => {
61
- it("takes a userId and one time key, adds a keyId and saves it to oneTimeKey table", async () => {
62
- expect.assertions(1);
63
- vi.mocked(uuid.v4).mockReturnValueOnce(keyID);
64
- vi.spyOn(winston, "createLogger").mockReturnValueOnce(silentLogger());
65
- const provider = new Database(options);
66
- await new Promise((resolve, reject) => {
67
- provider.once("ready", () => {
68
- void (async () => {
69
- try {
70
- await provider.saveOTK(testSQLPreKey.userID, testSQLPreKey.deviceID, [
71
- {
72
- deviceID,
73
- index: 1,
74
- publicKey,
75
- signature,
76
- },
77
- ]);
78
- const oneTimeKey = await provider.getOTK(deviceID);
79
- expect(oneTimeKey).toEqual(testWSPreKey);
80
- await provider.close();
81
- resolve();
82
- }
83
- catch (e) {
84
- reject(e instanceof Error ? e : new Error(String(e)));
85
- }
86
- })();
87
- });
88
- });
89
- });
90
- });
91
- describe("getPreKeys", () => {
92
- it("returns a preKey by deviceID if said preKey exists.", async () => {
93
- expect.assertions(1);
94
- const provider = new Database(options);
95
- await new Promise((resolve, reject) => {
96
- provider.once("ready", () => {
97
- void (async () => {
98
- try {
99
- await provider["db"]
100
- .insertInto("preKeys")
101
- .values(testSQLPreKey)
102
- .execute();
103
- const result = await provider.getPreKeys(deviceID);
104
- expect(result).toEqual(testWSPreKey);
105
- await provider.close();
106
- resolve();
107
- }
108
- catch (e) {
109
- reject(e instanceof Error ? e : new Error(String(e)));
110
- }
111
- })();
112
- });
113
- });
114
- });
115
- it("return null if there are no preKeys with deviceID param", async () => {
116
- expect.assertions(1);
117
- const provider = new Database(options);
118
- await new Promise((resolve, reject) => {
119
- provider.once("ready", () => {
120
- void (async () => {
121
- try {
122
- const result = await provider.getPreKeys(deviceID);
123
- expect(result).toBeNull();
124
- await provider.close();
125
- resolve();
126
- }
127
- catch (e) {
128
- reject(e instanceof Error ? e : new Error(String(e)));
129
- }
130
- })();
131
- });
132
- });
133
- });
134
- });
135
- });
136
- //# sourceMappingURL=Database.spec.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"Database.spec.js","sourceRoot":"","sources":["../../src/__tests__/Database.spec.ts"],"names":[],"mappings":"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"}
@@ -1,5 +0,0 @@
1
- import winston from "winston";
2
- /**
3
- * @ignore
4
- */
5
- export declare function createLogger(logName: string, logLevel?: string): winston.Logger;