@vex-chat/spire 0.7.4 → 1.0.0

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 (202) hide show
  1. package/README.md +82 -26
  2. package/dist/ClientManager.d.ts +24 -25
  3. package/dist/ClientManager.js +232 -509
  4. package/dist/ClientManager.js.map +1 -0
  5. package/dist/Database.d.ts +49 -41
  6. package/dist/Database.js +698 -716
  7. package/dist/Database.js.map +1 -0
  8. package/dist/Spire.d.ts +23 -15
  9. package/dist/Spire.js +518 -218
  10. package/dist/Spire.js.map +1 -0
  11. package/dist/__tests__/Database.spec.js +113 -73
  12. package/dist/__tests__/Database.spec.js.map +1 -0
  13. package/dist/db/schema.d.ts +134 -0
  14. package/dist/db/schema.js +2 -0
  15. package/dist/db/schema.js.map +1 -0
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.js +3 -5
  18. package/dist/index.js.map +1 -0
  19. package/dist/middleware/validate.d.ts +12 -0
  20. package/dist/middleware/validate.js +35 -0
  21. package/dist/middleware/validate.js.map +1 -0
  22. package/dist/migrations/2026-04-06_initial-schema.d.ts +3 -0
  23. package/dist/migrations/2026-04-06_initial-schema.js +192 -0
  24. package/dist/migrations/2026-04-06_initial-schema.js.map +1 -0
  25. package/dist/run.js +26 -21
  26. package/dist/run.js.map +1 -0
  27. package/dist/server/avatar.d.ts +3 -4
  28. package/dist/server/avatar.js +85 -67
  29. package/dist/server/avatar.js.map +1 -0
  30. package/dist/server/errors.d.ts +59 -0
  31. package/dist/server/errors.js +94 -0
  32. package/dist/server/errors.js.map +1 -0
  33. package/dist/server/file.d.ts +3 -4
  34. package/dist/server/file.js +101 -61
  35. package/dist/server/file.js.map +1 -0
  36. package/dist/server/index.d.ts +9 -6
  37. package/dist/server/index.js +595 -70
  38. package/dist/server/index.js.map +1 -0
  39. package/dist/server/invite.d.ts +4 -5
  40. package/dist/server/invite.js +21 -103
  41. package/dist/server/invite.js.map +1 -0
  42. package/dist/server/openapi.d.ts +2 -0
  43. package/dist/server/openapi.js +40 -0
  44. package/dist/server/openapi.js.map +1 -0
  45. package/dist/server/permissions.d.ts +16 -0
  46. package/dist/server/permissions.js +22 -0
  47. package/dist/server/permissions.js.map +1 -0
  48. package/dist/server/rateLimit.d.ts +28 -0
  49. package/dist/server/rateLimit.js +58 -0
  50. package/dist/server/rateLimit.js.map +1 -0
  51. package/dist/server/user.d.ts +4 -7
  52. package/dist/server/user.js +66 -76
  53. package/dist/server/user.js.map +1 -0
  54. package/dist/server/utils.d.ts +35 -7
  55. package/dist/server/utils.js +50 -6
  56. package/dist/server/utils.js.map +1 -0
  57. package/dist/types/express.d.ts +20 -0
  58. package/dist/types/express.js +2 -0
  59. package/dist/types/express.js.map +1 -0
  60. package/dist/utils/createLogger.js +13 -19
  61. package/dist/utils/createLogger.js.map +1 -0
  62. package/dist/utils/createUint8UUID.js +6 -10
  63. package/dist/utils/createUint8UUID.js.map +1 -0
  64. package/dist/utils/jwtSecret.d.ts +7 -0
  65. package/dist/utils/jwtSecret.js +15 -0
  66. package/dist/utils/jwtSecret.js.map +1 -0
  67. package/dist/utils/loadEnv.js +7 -22
  68. package/dist/utils/loadEnv.js.map +1 -0
  69. package/dist/utils/msgpack.d.ts +2 -0
  70. package/dist/utils/msgpack.js +4 -0
  71. package/dist/utils/msgpack.js.map +1 -0
  72. package/package.json +91 -63
  73. package/src/ClientManager.ts +434 -0
  74. package/src/Database.ts +925 -0
  75. package/src/Spire.ts +878 -0
  76. package/src/__tests__/Database.spec.ts +167 -0
  77. package/src/ambient-modules.d.ts +1 -0
  78. package/src/db/schema.ts +165 -0
  79. package/src/index.ts +3 -0
  80. package/src/middleware/validate.ts +38 -0
  81. package/src/migrations/2026-04-06_initial-schema.ts +218 -0
  82. package/src/run.ts +37 -0
  83. package/src/server/avatar.ts +141 -0
  84. package/src/server/errors.ts +133 -0
  85. package/src/server/file.ts +172 -0
  86. package/src/server/index.ts +855 -0
  87. package/src/server/invite.ts +65 -0
  88. package/src/server/openapi.ts +51 -0
  89. package/src/server/permissions.ts +40 -0
  90. package/src/server/rateLimit.ts +86 -0
  91. package/src/server/user.ts +125 -0
  92. package/src/server/utils.ts +59 -0
  93. package/src/types/express.ts +23 -0
  94. package/src/utils/createLogger.ts +47 -0
  95. package/src/utils/createUint8UUID.ts +9 -0
  96. package/src/utils/jwtSecret.ts +16 -0
  97. package/src/utils/loadEnv.ts +15 -0
  98. package/src/utils/msgpack.ts +4 -0
  99. package/avatars/169d76cb-6e7c-4e24-8224-017673eed8ff +0 -0
  100. package/avatars/1cae8d0b-0c6b-4c73-b25c-2d2349a57122 +0 -0
  101. package/avatars/1d87bc2b-71fc-4818-8004-40d04093e5fc +0 -0
  102. package/avatars/1f2c3d62-8b4d-465a-9895-51a24caef00d +0 -0
  103. package/avatars/245ee7fc-1004-41ab-adab-a04c9ceb9d7a +0 -0
  104. package/avatars/2465c28c-bdaf-4fa2-b42a-d054f04dc39b +0 -0
  105. package/avatars/3900a674-a2dd-4996-a61d-8c29b3270f41 +0 -0
  106. package/avatars/3c3b9c77-ea50-45e7-bb25-65d6f3d2a219 +0 -0
  107. package/avatars/414a2ff4-ad2f-4a7d-aa27-3b09ad3522b2 +0 -0
  108. package/avatars/522fe504-f0ad-4ed4-9dc6-e5dc2338e531 +0 -0
  109. package/avatars/53e5eb29-e7d1-4d58-add9-d44f39f0cfb7 +0 -0
  110. package/avatars/54d3f757-1038-41c8-bfb9-efd37b6e8ebe +0 -0
  111. package/avatars/623e86d7-c49c-46f6-9b76-ca70c9dbc43b +0 -0
  112. package/avatars/66e2abae-60f5-4dd1-b9fb-297a4bedfeb0 +0 -0
  113. package/avatars/6f37980e-f6fa-4d6d-9206-24050b403f45 +0 -0
  114. package/avatars/80138ece-eb5c-4b20-817b-903d6b0f54ae +0 -0
  115. package/avatars/841c77d3-37c4-431b-be22-672888062874 +0 -0
  116. package/avatars/88051a61-2bda-4750-95a3-5fb0b4918149 +0 -0
  117. package/avatars/89540973-c421-4bf1-8b89-9f1eaa51c1b5 +0 -0
  118. package/avatars/8a802fad-8c99-4942-8f80-47cec600149c +0 -0
  119. package/avatars/90531d8a-907a-4a1a-ac45-c85e4acb0df9 +0 -0
  120. package/avatars/9b7d0da9-b8d6-4801-b128-9993f79f464a +0 -0
  121. package/avatars/9bc456f1-c4c4-48a1-b9e6-fd44dd744a72 +0 -0
  122. package/avatars/9cf878bf-7430-49ec-a47a-78f3c93793dd +0 -0
  123. package/avatars/9ee82847-6ad3-45e5-92b9-f474a6c54d96 +0 -0
  124. package/avatars/ab44c857-d81d-4c88-85db-32f9532e5376 +0 -0
  125. package/avatars/b396a8d2-dc14-48d2-aac4-3755dc637051 +0 -0
  126. package/avatars/b6ac11c5-a8b2-4e0a-995a-9c87f1e58787 +0 -0
  127. package/avatars/b79d6855-b738-434c-be32-809637e62b9b +0 -0
  128. package/avatars/bbcd0188-d6a5-48ae-90fb-be5ff30599ab +0 -0
  129. package/avatars/bc7a9e0e-4720-4a6e-a90d-c11fec94d380 +0 -0
  130. package/avatars/c1c4889f-8383-4041-8bdd-9fded4046f37 +0 -0
  131. package/avatars/c4c7203c-d93a-4749-ade2-17053acf1d2a +0 -0
  132. package/avatars/ca974a70-0a23-4668-8b80-c4304dc7f793 +0 -0
  133. package/avatars/cf119a0d-eb3f-4bed-905b-f14a876c3535 +0 -0
  134. package/avatars/d464b03d-30c2-49e3-a666-80aefa8a1b35 +0 -0
  135. package/avatars/da0eee89-82f0-4d45-ab48-7d2786b634c5 +0 -0
  136. package/avatars/de4c77a5-68e9-4bb2-b40d-d964bf377d61 +0 -0
  137. package/avatars/dea95395-7d0b-42aa-a9ed-40c7d4fb4c48 +0 -0
  138. package/avatars/edb30749-59ba-4aa2-9c52-0fb22048f4cf +0 -0
  139. package/avatars/f17c245a-af7d-445b-9365-49f7f54b1eeb +0 -0
  140. package/avatars/f1ee6a35-b262-4dbf-99f5-3d011e3b98ec +0 -0
  141. package/avatars/f802bdd0-345d-41f6-9184-0f30e1258fb3 +0 -0
  142. package/dist/migrations/20210103192527_users.d.ts +0 -3
  143. package/dist/migrations/20210103192527_users.js +0 -30
  144. package/dist/migrations/20210103193502_mail.d.ts +0 -3
  145. package/dist/migrations/20210103193502_mail.js +0 -35
  146. package/dist/migrations/20210103193525_preKeys.d.ts +0 -3
  147. package/dist/migrations/20210103193525_preKeys.js +0 -30
  148. package/dist/migrations/20210103193553_oneTimeKeys.d.ts +0 -3
  149. package/dist/migrations/20210103193553_oneTimeKeys.js +0 -30
  150. package/dist/migrations/20210103193615_servers.d.ts +0 -3
  151. package/dist/migrations/20210103193615_servers.js +0 -28
  152. package/dist/migrations/20210103193729_channels.d.ts +0 -3
  153. package/dist/migrations/20210103193729_channels.js +0 -28
  154. package/dist/migrations/20210103193749_permissions.d.ts +0 -3
  155. package/dist/migrations/20210103193749_permissions.js +0 -30
  156. package/dist/migrations/20210103193801_files.d.ts +0 -3
  157. package/dist/migrations/20210103193801_files.js +0 -28
  158. package/files/00ea7368-45a7-4f6a-a199-974da14be1a0 +0 -0
  159. package/files/039503d2-a170-4962-b921-c97994ba64ff +0 -0
  160. package/files/14b6fa02-4cbb-40df-be4f-a07187cb619e +0 -0
  161. package/files/15c04cb1-dc6a-4f19-aa6f-4a3b92d05bf7 +0 -0
  162. package/files/2a8d411c-8b92-4532-b84d-d64c638d6293 +0 -0
  163. package/files/37de2cd3-08a8-4044-9c37-e13386765f3d +0 -0
  164. package/files/42452029-284e-4c81-9f18-feb6ce309eed +0 -0
  165. package/files/43d09f2f-29c8-415f-8c4a-23f2a32eb79e +0 -0
  166. package/files/52992923-33ab-44a1-8118-605e9b4856a7 +0 -0
  167. package/files/53180681-36e2-49c0-8382-94dca0da09bf +0 -0
  168. package/files/5a56cd7b-1d04-4619-b60f-1e5515b9164a +0 -0
  169. package/files/5ced7676-20c4-4219-a4f2-70a25eb7eea8 +0 -0
  170. package/files/60e787ff-8ec5-444d-b963-0aaf5313b53e +0 -0
  171. package/files/67a17729-fcb7-4339-9499-1fc08fea72ca +0 -0
  172. package/files/68d09565-908d-4a67-8f09-e183f8708eb4 +0 -0
  173. package/files/70e587c5-56e8-47f0-bd36-691efcb0cc2e +0 -4
  174. package/files/7a227619-b715-4e2c-a79d-b2158d56a799 +0 -0
  175. package/files/7e3cc3ea-b706-4835-994d-65d33acaf369 +0 -0
  176. package/files/816f72a0-65dc-40c1-8862-1d22065cca3c +0 -0
  177. package/files/8bf84972-5086-4631-a752-093d7b1a098b +0 -0
  178. package/files/8c46e3bc-3f2e-441d-b8cc-17428fc3d219 +0 -0
  179. package/files/8eb83364-8826-4eee-895a-ba5cd3ab85e9 +0 -0
  180. package/files/8faefaea-14e3-49e4-ac74-9710622bfae9 +0 -0
  181. package/files/91af08c2-9dca-41f4-b6c6-b7bf33505df9 +0 -0
  182. package/files/9349dffa-35dd-49a0-a651-10b438038f95 +0 -0
  183. package/files/b0a12a41-6283-4f27-b2c8-66774991d96c +0 -0
  184. package/files/b28afb08-d18b-48a8-93c3-6a59c66d8fee +0 -0
  185. package/files/be60fe01-1578-4363-a908-41500092b77d +0 -0
  186. package/files/cf7b90e3-734a-4453-9ff3-9a331404b5ef +0 -0
  187. package/files/d1be30aa-8ff4-41c1-b360-f65922029047 +0 -0
  188. package/files/d2d31a57-a413-443c-9b97-fa9ca394ef0b +0 -0
  189. package/files/d357f223-b786-478d-a113-b53cb62acdab +0 -0
  190. package/files/d4a69ee0-05c4-4ff0-8753-624cdd0f24e9 +0 -0
  191. package/files/d57f411b-1874-4f00-a6b0-2f86de6b229d +0 -0
  192. package/files/d85aaa33-7f70-4a70-b3d2-cad667beb38c +0 -0
  193. package/files/db2fc236-dbe0-4d24-bfaf-884829fd090a +0 -0
  194. package/files/df2e20ec-6dcc-4402-94ee-7d1163f84197 +0 -0
  195. package/files/e0880ab5-6d49-4dc0-a5ec-0d3793a8a7b8 +0 -0
  196. package/files/e59ee9c6-5aea-48ea-b3d9-94a324dc2260 +0 -0
  197. package/files/e6d7aad6-4220-4ce7-85f8-89d0b1f0cc89 +0 -0
  198. package/files/f0bfc98c-3534-459d-931a-7c6e77bde153 +0 -0
  199. package/files/f8443740-050c-4714-9bf6-334783ae6ffd +0 -0
  200. package/files/fc405ae7-f9fb-455d-ac8c-0ca0d88d4115 +0 -0
  201. package/jest.config.js +0 -13
  202. package/spire.sqlite +0 -0
package/dist/Spire.js CHANGED
@@ -1,127 +1,166 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
5
- }) : (function(o, m, k, k2) {
6
- if (k2 === undefined) k2 = k;
7
- o[k2] = m[k];
8
- }));
9
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
10
- Object.defineProperty(o, "default", { enumerable: true, value: v });
11
- }) : function(o, v) {
12
- o["default"] = v;
13
- });
14
- var __importStar = (this && this.__importStar) || function (mod) {
15
- if (mod && mod.__esModule) return mod;
16
- var result = {};
17
- if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
18
- __setModuleDefault(result, mod);
19
- return result;
20
- };
21
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
22
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
23
- return new (P || (P = Promise))(function (resolve, reject) {
24
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
25
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
26
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
27
- step((generator = generator.apply(thisArg, _arguments || [])).next());
28
- });
29
- };
30
- var __importDefault = (this && this.__importDefault) || function (mod) {
31
- return (mod && mod.__esModule) ? mod : { "default": mod };
32
- };
33
- Object.defineProperty(exports, "__esModule", { value: true });
34
- exports.Spire = exports.JWT_EXPIRY = exports.TOKEN_EXPIRY = void 0;
35
- const fs_1 = __importDefault(require("fs"));
36
- const crypto_1 = require("@vex-chat/crypto");
37
- const types_1 = require("@vex-chat/types");
38
- const events_1 = require("events");
39
- const express_1 = __importDefault(require("express"));
40
- const express_ws_1 = __importDefault(require("express-ws"));
41
- const tweetnacl_1 = __importDefault(require("tweetnacl"));
42
- const uuid = __importStar(require("uuid"));
43
- const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
44
- const ClientManager_1 = require("./ClientManager");
45
- const Database_1 = require("./Database");
46
- const server_1 = require("./server");
47
- const utils_1 = require("./server/utils");
48
- const createLogger_1 = require("./utils/createLogger");
1
+ import { EventEmitter } from "events";
2
+ import { execSync } from "node:child_process";
3
+ import * as fs from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import express from "express";
7
+ import { XUtils } from "@vex-chat/crypto";
8
+ import { xRandomBytes, xSignKeyPairFromSecret, xSignOpen, } from "@vex-chat/crypto";
9
+ import { MailWSSchema, RegistrationPayloadSchema, TokenScopes, UserSchema, } from "@vex-chat/types";
10
+ import jwt from "jsonwebtoken";
11
+ import { stringify as uuidStringify } from "uuid";
12
+ import { WebSocketServer } from "ws";
13
+ import { z } from "zod/v4";
14
+ import { ClientManager } from "./ClientManager.js";
15
+ import { Database, hashPassword } from "./Database.js";
16
+ import { initApp, protect } from "./server/index.js";
17
+ import { authLimiter } from "./server/rateLimit.js";
18
+ import { censorUser, getParam, getUser } from "./server/utils.js";
19
+ import { createLogger } from "./utils/createLogger.js";
20
+ import { getJwtSecret } from "./utils/jwtSecret.js";
21
+ import { msgpack } from "./utils/msgpack.js";
49
22
  // expiry of regkeys = 24hr
50
- exports.TOKEN_EXPIRY = 1000 * 60 * 10;
51
- exports.JWT_EXPIRY = 1000 * 60 * 60 * 24;
23
+ export const TOKEN_EXPIRY = 1000 * 60 * 10;
24
+ export const JWT_EXPIRY = "7d";
25
+ export const DEVICE_AUTH_JWT_EXPIRY = "1h";
26
+ const DEVICE_CHALLENGE_EXPIRY = 1000 * 60; // 60 seconds
27
+ const STATUS_LATENCY_BUDGET_MS = 250;
52
28
  // 3-19 chars long
53
29
  const usernameRegex = /^(\w{3,19})$/;
54
- const directories = ["files", "avatars"];
30
+ // ── Zod schemas for trust-boundary validation ──────────────────────────
31
+ const wsAuthMsg = z.object({
32
+ token: z.string().min(1),
33
+ type: z.literal("auth"),
34
+ });
35
+ const jwtPayload = z.object({
36
+ bearerToken: z.string().optional(),
37
+ exp: z.number().optional(),
38
+ user: UserSchema,
39
+ });
40
+ const authPayload = z.object({
41
+ password: z.string().min(1),
42
+ username: z.string().min(1),
43
+ });
44
+ const deviceAuthPayload = z.object({
45
+ deviceID: z.string().min(1),
46
+ signKey: z.string().min(1),
47
+ });
48
+ const deviceVerifyPayload = z.object({
49
+ challengeID: z.string().min(1),
50
+ signed: z.string().min(1),
51
+ });
52
+ const mailPostPayload = z.object({
53
+ header: z.custom((val) => val instanceof Uint8Array),
54
+ mail: MailWSSchema,
55
+ });
56
+ const directories = ["files", "avatars", "emoji"];
55
57
  for (const dir of directories) {
56
- if (!fs_1.default.existsSync(dir)) {
57
- fs_1.default.mkdirSync(dir);
58
+ if (!fs.existsSync(dir)) {
59
+ fs.mkdirSync(dir);
58
60
  }
59
61
  }
60
- const TokenScopes = types_1.XTypes.HTTP.TokenScopes;
61
- class Spire extends events_1.EventEmitter {
62
+ const getAppVersion = () => {
63
+ try {
64
+ const raw = fs.readFileSync(new URL("../package.json", import.meta.url), {
65
+ encoding: "utf8",
66
+ });
67
+ const pkg = JSON.parse(raw);
68
+ if (typeof pkg === "object" &&
69
+ pkg !== null &&
70
+ "version" in pkg &&
71
+ typeof pkg.version === "string") {
72
+ return pkg.version;
73
+ }
74
+ return "unknown";
75
+ }
76
+ catch {
77
+ return "unknown";
78
+ }
79
+ };
80
+ const getCommitSha = () => {
81
+ const sourceDir = path.dirname(fileURLToPath(import.meta.url));
82
+ const repoRoot = path.resolve(sourceDir, "..");
83
+ try {
84
+ const sha = execSync("git rev-parse --verify --short=12 HEAD", {
85
+ cwd: repoRoot,
86
+ encoding: "utf8",
87
+ stdio: ["ignore", "pipe", "ignore"],
88
+ timeout: 1500,
89
+ }).trim();
90
+ return sha || "unknown";
91
+ }
92
+ catch {
93
+ return "unknown";
94
+ }
95
+ };
96
+ export class Spire extends EventEmitter {
97
+ actionTokens = [];
98
+ api = express();
99
+ clients = [];
100
+ commitSha = getCommitSha();
101
+ db;
102
+ dbReady = false;
103
+ deviceChallenges = new Map();
104
+ log;
105
+ options;
106
+ queuedRequestIncrements = 0;
107
+ requestsTotal = 0;
108
+ requestsTotalLoaded = false;
109
+ server = null;
110
+ signKeys;
111
+ startedAt = new Date();
112
+ version = getAppVersion();
113
+ wss = new WebSocketServer({ noServer: true });
62
114
  constructor(SK, options) {
63
115
  super();
64
- this.clients = [];
65
- this.expWs = express_ws_1.default(express_1.default());
66
- this.api = this.expWs.app;
67
- this.wss = this.expWs.getWss();
68
- this.actionTokens = [];
69
- this.server = null;
70
- this.signKeys = tweetnacl_1.default.sign.keyPair.fromSecretKey(crypto_1.XUtils.decodeHex(SK));
71
- this.db = new Database_1.Database(options);
72
- this.log = createLogger_1.createLogger("spire", (options === null || options === void 0 ? void 0 : options.logLevel) || "error");
73
- this.init((options === null || options === void 0 ? void 0 : options.apiPort) || 16777);
116
+ this.signKeys = xSignKeyPairFromSecret(XUtils.decodeHex(SK));
117
+ // Trust a single proxy hop (nginx / cloudflare / load balancer).
118
+ // Required so `req.ip` and `express-rate-limit`'s keyGenerator see
119
+ // the real client address via X-Forwarded-For. Never use `true` —
120
+ // that lets attackers spoof the header and bypass rate limiting.
121
+ // If spire is deployed without a proxy, set this to 0 instead.
122
+ this.api.set("trust proxy", 1);
123
+ this.db = new Database(options);
124
+ this.db.on("ready", () => {
125
+ this.dbReady = true;
126
+ this.bootstrapRequestCounter().catch((err) => {
127
+ this.log.error("Failed to load persisted request counter: " + String(err));
128
+ });
129
+ });
130
+ this.log = createLogger("spire", options?.logLevel || "error");
131
+ this.init(options?.apiPort || 16777);
74
132
  this.options = options;
75
133
  }
76
- close() {
77
- var _a, _b;
78
- return __awaiter(this, void 0, void 0, function* () {
79
- this.wss.clients.forEach((ws) => {
80
- ws.terminate();
81
- });
82
- this.wss.on("close", () => {
83
- this.log.info("ws: closed.");
84
- });
85
- (_a = this.server) === null || _a === void 0 ? void 0 : _a.on("close", () => {
86
- this.log.info("http: closed.");
87
- });
88
- (_b = this.server) === null || _b === void 0 ? void 0 : _b.close();
89
- this.wss.close();
90
- yield this.db.close();
91
- return;
134
+ async close() {
135
+ this.wss.clients.forEach((ws) => {
136
+ ws.terminate();
137
+ });
138
+ this.wss.on("close", () => {
139
+ this.log.info("ws: closed.");
140
+ });
141
+ this.server?.on("close", () => {
142
+ this.log.info("http: closed.");
92
143
  });
144
+ this.server?.close();
145
+ this.wss.close();
146
+ await this.db.close();
147
+ return;
93
148
  }
94
- notify(userID, event, transmissionID, data, deviceID) {
95
- for (const client of this.clients) {
96
- if (deviceID) {
97
- if (client.getDevice().deviceID === deviceID) {
98
- const msg = {
99
- transmissionID,
100
- type: "notify",
101
- event,
102
- data,
103
- };
104
- client.send(msg);
105
- }
106
- }
107
- else {
108
- if (client.getUser().userID === userID) {
109
- const msg = {
110
- transmissionID,
111
- type: "notify",
112
- event,
113
- data,
114
- };
115
- client.send(msg);
116
- }
117
- }
149
+ async bootstrapRequestCounter() {
150
+ const persistedTotal = await this.db.getRequestsTotal();
151
+ const startupIncrements = this.queuedRequestIncrements;
152
+ this.queuedRequestIncrements = 0;
153
+ this.requestsTotal = persistedTotal + startupIncrements;
154
+ if (startupIncrements > 0) {
155
+ await this.db.incrementRequestsTotal(startupIncrements);
118
156
  }
157
+ this.requestsTotalLoaded = true;
119
158
  }
120
159
  createActionToken(scope) {
121
160
  const token = {
122
- key: uuid.v4(),
123
- time: new Date(Date.now()),
161
+ key: crypto.randomUUID(),
124
162
  scope,
163
+ time: new Date().toISOString(),
125
164
  };
126
165
  this.actionTokens.push(token);
127
166
  return token;
@@ -131,93 +170,130 @@ class Spire extends events_1.EventEmitter {
131
170
  this.actionTokens.splice(this.actionTokens.indexOf(key), 1);
132
171
  }
133
172
  }
134
- validateToken(key, scope) {
135
- this.log.info("Validating token: " + key);
136
- for (const rKey of this.actionTokens) {
137
- if (rKey.key === key) {
138
- if (rKey.scope !== scope) {
139
- continue;
140
- }
141
- const age = new Date(Date.now()).getTime() - rKey.time.getTime();
142
- this.log.info("Token found, " + age + " ms old.");
143
- if (age < exports.TOKEN_EXPIRY) {
144
- this.log.info("Token is valid.");
145
- this.deleteActionToken(rKey);
146
- return true;
147
- }
148
- else {
149
- this.log.info("Token is expired.");
150
- }
151
- }
152
- }
153
- this.log.info("Token not found.");
154
- return false;
155
- }
156
173
  init(apiPort) {
174
+ this.api.use((_req, _res, next) => {
175
+ this.requestsTotal += 1;
176
+ if (!this.requestsTotalLoaded) {
177
+ this.queuedRequestIncrements += 1;
178
+ }
179
+ else {
180
+ this.db.incrementRequestsTotal(1).catch((err) => {
181
+ this.log.warn("Failed to persist request counter increment: " +
182
+ String(err));
183
+ });
184
+ }
185
+ next();
186
+ });
157
187
  // initialize the expression app configuration with loose routes/handlers
158
- server_1.initApp(this.api, this.db, this.log, this.validateToken.bind(this), this.signKeys, this.notify.bind(this));
159
- // All the app logic strongly coupled to spire class :/
160
- this.api.ws("/socket", (ws, req) => {
161
- const jwtDetails = req.user;
162
- if (!jwtDetails) {
163
- this.log.warn("User attempted to open socket with no jwt.");
164
- const err = {
165
- type: "unauthorized",
166
- transmissionID: uuid.v4(),
167
- };
168
- const msg = crypto_1.XUtils.packMessage(err);
169
- ws.send(msg);
188
+ initApp(this.api, this.db, this.log, this.validateToken.bind(this), this.signKeys, this.notify.bind(this));
189
+ // WS auth: client sends { type: "auth", token } as first message
190
+ this.wss.on("connection", (ws) => {
191
+ this.log.info("WS connection established, waiting for auth...");
192
+ const AUTH_TIMEOUT = 10_000;
193
+ const timer = setTimeout(() => {
194
+ this.log.warn("WS auth timeout — closing.");
170
195
  ws.close();
171
- return;
172
- }
173
- this.log.info("New client initiated.");
174
- this.log.info(jwtDetails);
175
- console.log(req.user);
176
- const client = new ClientManager_1.ClientManager(ws, this.db, this.notify.bind(this), jwtDetails, this.options);
177
- client.on("fail", () => {
178
- this.log.info("Client connection is down, removing: " + client.toString());
179
- if (this.clients.includes(client)) {
180
- this.clients.splice(this.clients.indexOf(client), 1);
196
+ }, AUTH_TIMEOUT);
197
+ const onFirstMessage = (data) => {
198
+ const str = Buffer.isBuffer(data)
199
+ ? data.toString()
200
+ : data instanceof ArrayBuffer
201
+ ? Buffer.from(data).toString()
202
+ : Buffer.concat(data).toString();
203
+ clearTimeout(timer);
204
+ ws.off("message", onFirstMessage);
205
+ try {
206
+ const rawParsed = JSON.parse(str);
207
+ const authResult = wsAuthMsg.safeParse(rawParsed);
208
+ if (!authResult.success) {
209
+ throw new Error("Expected { type: 'auth', token }, got: " +
210
+ JSON.stringify(authResult.error.issues));
211
+ }
212
+ const result = jwt.verify(authResult.data.token, getJwtSecret());
213
+ const jwtResult = jwtPayload.safeParse(result);
214
+ if (!jwtResult.success) {
215
+ throw new Error("Invalid JWT payload: " +
216
+ JSON.stringify(jwtResult.error.issues));
217
+ }
218
+ const userDetails = jwtResult.data.user;
219
+ this.log.info("WS auth succeeded for " + userDetails.username);
220
+ const client = new ClientManager(ws, this.db, this.notify.bind(this), userDetails, this.options);
221
+ client.on("fail", () => {
222
+ this.log.info("Client connection is down, removing: " +
223
+ client.toString());
224
+ if (this.clients.includes(client)) {
225
+ this.clients.splice(this.clients.indexOf(client), 1);
226
+ }
227
+ this.log.info("Current authorized clients: " +
228
+ String(this.clients.length));
229
+ });
230
+ client.on("authed", () => {
231
+ this.log.info("New client authorized: " + client.toString());
232
+ this.clients.push(client);
233
+ this.log.info("Current authorized clients: " +
234
+ String(this.clients.length));
235
+ });
181
236
  }
182
- this.log.info("Current authorized clients: " + this.clients.length);
183
- });
184
- client.on("authed", () => {
185
- this.log.info("New client authorized: " + client.toString());
186
- this.clients.push(client);
187
- this.log.info("Current authorized clients: " + this.clients.length);
237
+ catch (err) {
238
+ this.log.warn("WS auth failed: " + String(err));
239
+ const errMsg = {
240
+ transmissionID: crypto.randomUUID(),
241
+ type: "unauthorized",
242
+ };
243
+ ws.send(XUtils.packMessage(errMsg));
244
+ ws.close();
245
+ }
246
+ };
247
+ ws.on("message", onFirstMessage);
248
+ ws.on("close", () => {
249
+ clearTimeout(timer);
188
250
  });
189
251
  });
190
- this.api.get("/token/:tokenType", (req, res) => __awaiter(this, void 0, void 0, function* () {
252
+ this.api.get("/token/:tokenType", (req, res, next) => {
253
+ if (getParam(req, "tokenType") !== "register") {
254
+ protect(req, res, next);
255
+ }
256
+ else {
257
+ next();
258
+ }
259
+ }, (req, res) => {
191
260
  const allowedTokens = [
192
261
  "file",
193
262
  "register",
194
263
  "avatar",
195
264
  "device",
196
265
  "invite",
266
+ "emoji",
267
+ "connect",
197
268
  ];
198
- const { tokenType } = req.params;
199
- console.log(tokenType);
269
+ const tokenType = getParam(req, "tokenType");
200
270
  if (!allowedTokens.includes(tokenType)) {
201
271
  res.sendStatus(400);
202
272
  return;
203
273
  }
204
274
  let scope;
205
275
  switch (tokenType) {
206
- case "file":
207
- scope = TokenScopes.File;
208
- break;
209
- case "register":
210
- scope = TokenScopes.Register;
211
- break;
212
276
  case "avatar":
213
277
  scope = TokenScopes.Avatar;
214
278
  break;
279
+ case "connect":
280
+ scope = TokenScopes.Connect;
281
+ break;
215
282
  case "device":
216
283
  scope = TokenScopes.Device;
217
284
  break;
285
+ case "emoji":
286
+ scope = TokenScopes.Emoji;
287
+ break;
288
+ case "file":
289
+ scope = TokenScopes.File;
290
+ break;
218
291
  case "invite":
219
292
  scope = TokenScopes.Invite;
220
293
  break;
294
+ case "register":
295
+ scope = TokenScopes.Register;
296
+ break;
221
297
  default:
222
298
  res.sendStatus(400);
223
299
  return;
@@ -228,84 +304,250 @@ class Spire extends events_1.EventEmitter {
228
304
  this.log.info("New token created: " + token.key);
229
305
  setTimeout(() => {
230
306
  this.deleteActionToken(token);
231
- }, exports.TOKEN_EXPIRY);
232
- return res.status(201).send(token);
307
+ }, TOKEN_EXPIRY);
308
+ const acceptHeader = req.get("accept")?.toLowerCase() || "";
309
+ const wantsJson = acceptHeader.includes("application/json") &&
310
+ !acceptHeader.includes("application/msgpack") &&
311
+ !acceptHeader.includes("*/*");
312
+ if (wantsJson) {
313
+ return res.json(token);
314
+ }
315
+ res.set("Content-Type", "application/msgpack");
316
+ return res.send(msgpack.encode(token));
317
+ }
318
+ catch (err) {
319
+ this.log.error(String(err));
320
+ return res.sendStatus(500);
321
+ }
322
+ });
323
+ this.api.post("/whoami", (req, res) => {
324
+ if (!req.user) {
325
+ res.sendStatus(401);
326
+ return;
327
+ }
328
+ res.send(msgpack.encode({
329
+ exp: req.exp,
330
+ token: req.bearerToken,
331
+ user: req.user,
332
+ }));
333
+ });
334
+ this.api.get("/healthz", (_req, res) => {
335
+ if (!this.dbReady) {
336
+ res.status(503).json({ dbReady: false, ok: false });
337
+ return;
338
+ }
339
+ res.json({ dbReady: true, ok: true });
340
+ });
341
+ this.api.get("/status", async (_req, res) => {
342
+ const started = Date.now();
343
+ const dbHealthy = this.dbReady ? await this.db.isHealthy() : false;
344
+ const checkDurationMs = Date.now() - started;
345
+ const ok = dbHealthy;
346
+ res.json({
347
+ checkDurationMs,
348
+ commitSha: this.commitSha,
349
+ dbHealthy,
350
+ dbReady: this.dbReady,
351
+ latencyBudgetMs: STATUS_LATENCY_BUDGET_MS,
352
+ metrics: {
353
+ requestsTotal: this.requestsTotal,
354
+ },
355
+ now: new Date(),
356
+ ok,
357
+ startedAt: this.startedAt.toISOString(),
358
+ uptimeSeconds: Math.floor(process.uptime()),
359
+ version: this.version,
360
+ withinLatencyBudget: checkDurationMs <= STATUS_LATENCY_BUDGET_MS,
361
+ });
362
+ });
363
+ this.api.post("/goodbye", protect, (req, res) => {
364
+ jwt.sign({ user: req.user }, getJwtSecret(), { expiresIn: -1 });
365
+ res.sendStatus(200);
366
+ });
367
+ // ── Device-key auth ──────────────────────────────────────────
368
+ this.api.post("/auth/device", authLimiter, async (req, res) => {
369
+ try {
370
+ const parsed = deviceAuthPayload.safeParse(req.body);
371
+ if (!parsed.success) {
372
+ return res
373
+ .status(400)
374
+ .send({ error: "deviceID and signKey required." });
375
+ }
376
+ const { deviceID, signKey } = parsed.data;
377
+ const device = await this.db.retrieveDevice(deviceID);
378
+ if (!device || device.signKey !== signKey) {
379
+ return res.status(404).send({ error: "Device not found." });
380
+ }
381
+ // Generate challenge nonce (32 bytes)
382
+ const nonce = XUtils.encodeHex(xRandomBytes(32));
383
+ const challengeID = crypto.randomUUID();
384
+ this.deviceChallenges.set(challengeID, {
385
+ deviceID,
386
+ nonce,
387
+ time: Date.now(),
388
+ });
389
+ // Clean up expired challenges
390
+ setTimeout(() => {
391
+ this.deviceChallenges.delete(challengeID);
392
+ }, DEVICE_CHALLENGE_EXPIRY);
393
+ this.log.info("Device challenge issued for " + deviceID);
394
+ return res.send(msgpack.encode({ challenge: nonce, challengeID }));
395
+ }
396
+ catch (err) {
397
+ this.log.error("Device challenge error: " + String(err));
398
+ return res.sendStatus(500);
399
+ }
400
+ });
401
+ this.api.post("/auth/device/verify", authLimiter, async (req, res) => {
402
+ try {
403
+ const parsed = deviceVerifyPayload.safeParse(req.body);
404
+ if (!parsed.success) {
405
+ return res.status(400).send({
406
+ error: "challengeID and signed required.",
407
+ });
408
+ }
409
+ const { challengeID, signed } = parsed.data;
410
+ const challenge = this.deviceChallenges.get(challengeID);
411
+ if (!challenge) {
412
+ return res.status(401).send({
413
+ error: "Challenge expired or not found.",
414
+ });
415
+ }
416
+ // Consume the challenge (single-use)
417
+ this.deviceChallenges.delete(challengeID);
418
+ // Check expiry
419
+ if (Date.now() - challenge.time > DEVICE_CHALLENGE_EXPIRY) {
420
+ return res
421
+ .status(401)
422
+ .send({ error: "Challenge expired." });
423
+ }
424
+ // Look up the device to get its public signKey
425
+ const device = await this.db.retrieveDevice(challenge.deviceID);
426
+ if (!device) {
427
+ return res.status(404).send({ error: "Device not found." });
428
+ }
429
+ // Verify the Ed25519 signature
430
+ const opened = xSignOpen(XUtils.decodeHex(signed), XUtils.decodeHex(device.signKey));
431
+ if (!opened) {
432
+ return res
433
+ .status(401)
434
+ .send({ error: "Signature verification failed." });
435
+ }
436
+ // Verify the signed content matches the challenge nonce
437
+ const signedNonce = XUtils.encodeHex(opened);
438
+ if (signedNonce !== challenge.nonce) {
439
+ return res
440
+ .status(401)
441
+ .send({ error: "Challenge mismatch." });
442
+ }
443
+ // Look up device owner
444
+ const user = await this.db.retrieveUser(device.owner);
445
+ if (!user) {
446
+ return res
447
+ .status(404)
448
+ .send({ error: "Device owner not found." });
449
+ }
450
+ // Issue short-lived JWT (1 hour, not 7 days)
451
+ const token = jwt.sign({ user: censorUser(user) }, getJwtSecret(), { expiresIn: DEVICE_AUTH_JWT_EXPIRY });
452
+ this.log.info("Device-key auth succeeded for " +
453
+ user.username +
454
+ " (device " +
455
+ device.deviceID +
456
+ ")");
457
+ return res.send(msgpack.encode({ token, user: censorUser(user) }));
233
458
  }
234
459
  catch (err) {
235
- console.error(err.toString());
460
+ this.log.error("Device verify error: " + String(err));
236
461
  return res.sendStatus(500);
237
462
  }
238
- }));
239
- this.api.post("/auth", (req, res) => __awaiter(this, void 0, void 0, function* () {
240
- const credentials = req.body;
241
- if (typeof credentials.password !== "string") {
242
- res.status(400).send("Password is required and must be a string.");
463
+ });
464
+ this.api.post("/mail", protect, async (req, res) => {
465
+ const senderDeviceDetails = req.device;
466
+ if (!senderDeviceDetails) {
467
+ res.sendStatus(401);
468
+ return;
469
+ }
470
+ const authorUserDetails = getUser(req);
471
+ const parsed = mailPostPayload.safeParse(req.body);
472
+ if (!parsed.success) {
473
+ res.status(400).json({
474
+ error: "Invalid mail payload",
475
+ issues: parsed.error.issues,
476
+ });
243
477
  return;
244
478
  }
245
- if (typeof credentials.username !== "string") {
246
- res.status(400).send("Username is required and must be a string.");
479
+ const { header, mail } = parsed.data;
480
+ await this.db.saveMail(mail, header, senderDeviceDetails.deviceID, authorUserDetails.userID);
481
+ this.log.info("Received mail for " + mail.recipient);
482
+ const recipientDeviceDetails = await this.db.retrieveDevice(mail.recipient);
483
+ if (!recipientDeviceDetails) {
484
+ res.sendStatus(400);
247
485
  return;
248
486
  }
487
+ res.sendStatus(200);
488
+ this.notify(recipientDeviceDetails.owner, "mail", crypto.randomUUID(), null, mail.recipient);
489
+ });
490
+ this.api.post("/auth", authLimiter, async (req, res) => {
491
+ const parsed = authPayload.safeParse(req.body);
492
+ if (!parsed.success) {
493
+ res.status(400).json({
494
+ error: "Invalid credentials format",
495
+ });
496
+ return;
497
+ }
498
+ const { password, username } = parsed.data;
249
499
  try {
250
- const userEntry = yield this.db.retrieveUser(credentials.username);
500
+ const userEntry = await this.db.retrieveUser(username);
251
501
  if (!userEntry) {
252
502
  res.sendStatus(404);
253
503
  this.log.warn("User does not exist.");
254
504
  return;
255
505
  }
256
- const salt = crypto_1.XUtils.decodeHex(userEntry.passwordSalt);
257
- const payloadHash = crypto_1.XUtils.encodeHex(Database_1.hashPassword(credentials.password, salt));
506
+ const salt = XUtils.decodeHex(userEntry.passwordSalt);
507
+ const payloadHash = XUtils.encodeHex(hashPassword(password, salt));
258
508
  if (payloadHash !== userEntry.passwordHash) {
259
509
  res.sendStatus(401);
260
510
  return;
261
511
  }
262
- const token = jsonwebtoken_1.default.sign({ user: utils_1.censorUser(userEntry) }, process.env.SPK, { expiresIn: exports.JWT_EXPIRY });
263
- res.cookie("auth", token);
264
- res.send({ user: utils_1.censorUser(userEntry), token });
512
+ const token = jwt.sign({ user: censorUser(userEntry) }, getJwtSecret(), { expiresIn: JWT_EXPIRY });
513
+ // just to make sure
514
+ jwt.verify(token, getJwtSecret());
515
+ res.send(msgpack.encode({ token, user: censorUser(userEntry) }));
265
516
  }
266
517
  catch (err) {
267
- console.log(err.toString());
518
+ this.log.error(String(err));
268
519
  res.sendStatus(500);
269
520
  }
270
- }));
271
- this.api.post("/register/key", (req, res) => {
272
- try {
273
- this.log.info("New regkey requested.");
274
- const regKey = this.createActionToken(TokenScopes.Register);
275
- this.log.info("New regkey created: " + regKey.key);
276
- setTimeout(() => {
277
- this.deleteActionToken(regKey);
278
- }, exports.TOKEN_EXPIRY);
279
- return res.status(201).send(regKey);
280
- }
281
- catch (err) {
282
- this.log.error(err.toString());
283
- return res.sendStatus(500);
284
- }
285
521
  });
286
- // 19 char max limit for username
287
- this.api.post("/register/new", (req, res) => __awaiter(this, void 0, void 0, function* () {
522
+ this.api.post("/register", authLimiter, async (req, res) => {
288
523
  try {
289
- const regPayload = req.body;
524
+ const regParsed = RegistrationPayloadSchema.safeParse(req.body);
525
+ if (!regParsed.success) {
526
+ res.status(400).json({
527
+ error: "Invalid registration payload",
528
+ issues: regParsed.error.issues,
529
+ });
530
+ return;
531
+ }
532
+ const regPayload = regParsed.data;
290
533
  if (!usernameRegex.test(regPayload.username)) {
291
534
  res.status(400).send({
292
535
  error: "Username must be between three and nineteen letters, digits, or underscores.",
293
536
  });
294
537
  return;
295
538
  }
296
- const regKey = tweetnacl_1.default.sign.open(crypto_1.XUtils.decodeHex(regPayload.signed), crypto_1.XUtils.decodeHex(regPayload.signKey));
539
+ const regKey = xSignOpen(XUtils.decodeHex(regPayload.signed), XUtils.decodeHex(regPayload.signKey));
297
540
  if (regKey &&
298
- this.validateToken(uuid.stringify(regKey), TokenScopes.Register)) {
299
- const [user, err] = yield this.db.createUser(regKey, regPayload);
541
+ this.validateToken(uuidStringify(regKey), TokenScopes.Register)) {
542
+ const [user, err] = await this.db.createUser(regKey, regPayload);
300
543
  if (err !== null) {
301
- switch (err.code) {
544
+ const errCode = "code" in err && typeof err.code === "string"
545
+ ? err.code
546
+ : undefined;
547
+ switch (errCode) {
302
548
  case "ER_DUP_ENTRY":
303
- const usernameConflict = err
304
- .toString()
305
- .includes("users_username_unique");
306
- const signKeyConflict = err
307
- .toString()
308
- .includes("users_signkey_unique");
549
+ const usernameConflict = String(err).includes("users_username_unique");
550
+ const signKeyConflict = String(err).includes("users_signkey_unique");
309
551
  this.log.warn("User attempted to register duplicate account.");
310
552
  if (usernameConflict) {
311
553
  res.status(400).send({
@@ -325,15 +567,19 @@ class Spire extends events_1.EventEmitter {
325
567
  break;
326
568
  default:
327
569
  this.log.info("Unsupported sql error type: " +
328
- err.code);
329
- this.log.error(err);
570
+ String(errCode));
571
+ this.log.error(String(err));
330
572
  res.sendStatus(500);
331
573
  break;
332
574
  }
333
575
  }
334
576
  else {
335
577
  this.log.info("Registration success.");
336
- res.send(utils_1.censorUser(user));
578
+ if (!user) {
579
+ res.sendStatus(500);
580
+ return;
581
+ }
582
+ res.send(msgpack.encode(censorUser(user)));
337
583
  }
338
584
  }
339
585
  else {
@@ -343,13 +589,67 @@ class Spire extends events_1.EventEmitter {
343
589
  }
344
590
  }
345
591
  catch (err) {
346
- this.log.error("error registering user: " + err.toString());
592
+ this.log.error("error registering user: " + String(err));
347
593
  res.sendStatus(500);
348
594
  }
349
- }));
595
+ });
350
596
  this.server = this.api.listen(apiPort, () => {
351
- this.log.info("API started on port " + apiPort.toString());
597
+ this.log.info("API started on port " + String(apiPort));
598
+ });
599
+ // Accept all WS upgrades — auth happens post-connection.
600
+ this.server.on("upgrade", (req, socket, head) => {
601
+ this.wss.handleUpgrade(req, socket, head, (ws) => {
602
+ this.wss.emit("connection", ws);
603
+ });
352
604
  });
353
605
  }
606
+ notify(userID, event, transmissionID, data, deviceID) {
607
+ for (const client of this.clients) {
608
+ if (deviceID) {
609
+ if (client.getDevice().deviceID === deviceID) {
610
+ const msg = {
611
+ data,
612
+ event,
613
+ transmissionID,
614
+ type: "notify",
615
+ };
616
+ client.send(msg);
617
+ }
618
+ }
619
+ else {
620
+ if (client.getUser().userID === userID) {
621
+ const msg = {
622
+ data,
623
+ event,
624
+ transmissionID,
625
+ type: "notify",
626
+ };
627
+ client.send(msg);
628
+ }
629
+ }
630
+ }
631
+ }
632
+ validateToken(key, scope) {
633
+ this.log.info("Validating token: " + key);
634
+ for (const rKey of this.actionTokens) {
635
+ if (rKey.key === key) {
636
+ if (rKey.scope !== scope) {
637
+ continue;
638
+ }
639
+ const age = Date.now() - new Date(rKey.time).getTime();
640
+ this.log.info("Token found, " + String(age) + " ms old.");
641
+ if (age < TOKEN_EXPIRY) {
642
+ this.log.info("Token is valid.");
643
+ this.deleteActionToken(rKey);
644
+ return true;
645
+ }
646
+ else {
647
+ this.log.info("Token is expired.");
648
+ }
649
+ }
650
+ }
651
+ this.log.info("Token not found.");
652
+ return false;
653
+ }
354
654
  }
355
- exports.Spire = Spire;
655
+ //# sourceMappingURL=Spire.js.map