@vex-chat/spire 1.0.3 → 1.1.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.
- package/README.md +44 -36
- package/dist/ClientManager.js +2 -2
- package/dist/ClientManager.js.map +1 -1
- package/dist/Database.d.ts +14 -16
- package/dist/Database.js +73 -101
- package/dist/Database.js.map +1 -1
- package/dist/Spire.d.ts +13 -1
- package/dist/Spire.js +89 -10
- package/dist/Spire.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/run.js +24 -6
- package/dist/run.js.map +1 -1
- package/dist/server/index.js +20 -13
- package/dist/server/index.js.map +1 -1
- package/dist/server/user.d.ts +1 -1
- package/dist/server/user.js +238 -11
- package/dist/server/user.js.map +1 -1
- package/dist/spireListenPort.d.ts +14 -0
- package/dist/spireListenPort.js +22 -0
- package/dist/spireListenPort.js.map +1 -0
- package/dist/utils/spireXSignOpenAsync.d.ts +9 -0
- package/dist/utils/spireXSignOpenAsync.js +16 -0
- package/dist/utils/spireXSignOpenAsync.js.map +1 -0
- package/package.json +110 -119
- package/src/ClientManager.ts +2 -2
- package/src/Database.ts +84 -116
- package/src/Spire.ts +116 -13
- package/src/__tests__/Database.spec.ts +2 -1
- package/src/__tests__/spireListenPort.spec.ts +22 -0
- package/src/index.ts +4 -0
- package/src/run.ts +28 -6
- package/src/server/index.ts +25 -14
- package/src/server/user.ts +313 -10
- package/src/spireListenPort.ts +25 -0
- package/src/utils/spireXSignOpenAsync.ts +20 -0
package/src/Spire.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { ActionToken, BaseMsg, NotifyMsg, User } from "@vex-chat/types";
|
|
8
|
-
import type { Server } from "http";
|
|
8
|
+
import type { IncomingMessage, Server } from "http";
|
|
9
9
|
|
|
10
10
|
import { EventEmitter } from "events";
|
|
11
11
|
import { execSync } from "node:child_process";
|
|
@@ -19,9 +19,10 @@ import express from "express";
|
|
|
19
19
|
import { XUtils } from "@vex-chat/crypto";
|
|
20
20
|
import {
|
|
21
21
|
type KeyPair,
|
|
22
|
+
setCryptoProfile,
|
|
22
23
|
xRandomBytes,
|
|
23
24
|
xSignKeyPairFromSecret,
|
|
24
|
-
|
|
25
|
+
xSignKeyPairFromSecretAsync,
|
|
25
26
|
} from "@vex-chat/crypto";
|
|
26
27
|
import {
|
|
27
28
|
MailWSSchema,
|
|
@@ -31,6 +32,7 @@ import {
|
|
|
31
32
|
} from "@vex-chat/types";
|
|
32
33
|
|
|
33
34
|
import jwt from "jsonwebtoken";
|
|
35
|
+
import morgan from "morgan";
|
|
34
36
|
import { stringify as uuidStringify } from "uuid";
|
|
35
37
|
import { WebSocketServer } from "ws";
|
|
36
38
|
import { z } from "zod/v4";
|
|
@@ -40,13 +42,15 @@ import { Database, hashPasswordArgon2, verifyPassword } from "./Database.ts";
|
|
|
40
42
|
import { initApp, protect } from "./server/index.ts";
|
|
41
43
|
import { authLimiter, devApiKeySkipsRateLimits } from "./server/rateLimit.ts";
|
|
42
44
|
import { censorUser, getParam, getUser } from "./server/utils.ts";
|
|
45
|
+
import { resolveSpireListenPort } from "./spireListenPort.ts";
|
|
43
46
|
import { getJwtSecret } from "./utils/jwtSecret.ts";
|
|
44
47
|
import { msgpack } from "./utils/msgpack.ts";
|
|
48
|
+
import { spireXSignOpenAsync } from "./utils/spireXSignOpenAsync.ts";
|
|
45
49
|
|
|
46
50
|
// expiry of regkeys = 24hr
|
|
47
51
|
export const TOKEN_EXPIRY = 1000 * 60 * 10;
|
|
48
52
|
export const JWT_EXPIRY = "7d";
|
|
49
|
-
export const DEVICE_AUTH_JWT_EXPIRY = "
|
|
53
|
+
export const DEVICE_AUTH_JWT_EXPIRY = "7d";
|
|
50
54
|
const DEVICE_CHALLENGE_EXPIRY = 1000 * 60; // 60 seconds
|
|
51
55
|
|
|
52
56
|
// 3-19 chars long
|
|
@@ -132,15 +136,40 @@ const getCommitSha = (): string => {
|
|
|
132
136
|
};
|
|
133
137
|
|
|
134
138
|
export interface SpireOptions {
|
|
139
|
+
/**
|
|
140
|
+
* TCP port for the HTTP/WS server. If omitted, `run.ts` + `resolveSpireListenPort`
|
|
141
|
+
* use the default (16777 for all profiles; crypto mode is in `GET /status`). Env: `API_PORT`.
|
|
142
|
+
*/
|
|
135
143
|
apiPort?: number;
|
|
144
|
+
/** Default `tweetnacl`. For `fips`, use `Spire.createAsync` (FIPS key load is async). */
|
|
145
|
+
cryptoProfile?: "fips" | "tweetnacl";
|
|
136
146
|
dbType?: "mysql" | "sqlite3" | "sqlite3mem" | "sqlite";
|
|
137
147
|
}
|
|
138
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Masks identifying material in the access-log URL: hyphenated UUIDs, and
|
|
151
|
+
* `/device/...` path segments that hold public-key material (32B ed25519 hex, or
|
|
152
|
+
* a longer P-256 SPKI hex in FIPS) — not the same pattern as a UUID.
|
|
153
|
+
*/
|
|
154
|
+
function redactAccessLogUrl(url: string): string {
|
|
155
|
+
let s = url.replace(
|
|
156
|
+
/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi,
|
|
157
|
+
"[uuid]",
|
|
158
|
+
);
|
|
159
|
+
// Device id is a hex public key, not a UUID: strip the segment after /device/
|
|
160
|
+
s = s.replace(/(\/device\/)([0-9a-fA-F]{16,})(?=[/?#]|$)/g, "$1[device]");
|
|
161
|
+
return s;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** FIPS: sign key loaded inside `Spire.createAsync` before the constructor runs. */
|
|
165
|
+
let pendingFipsKeyPair: KeyPair | null = null;
|
|
166
|
+
|
|
139
167
|
export class Spire extends EventEmitter {
|
|
140
168
|
private actionTokens: ActionToken[] = [];
|
|
141
169
|
private api = express();
|
|
142
170
|
private clients: ClientManager[] = [];
|
|
143
171
|
private readonly commitSha = getCommitSha();
|
|
172
|
+
private readonly cryptoProfile: "fips" | "tweetnacl";
|
|
144
173
|
private db: Database;
|
|
145
174
|
private dbReady = false;
|
|
146
175
|
private deviceChallenges = new Map<
|
|
@@ -148,13 +177,13 @@ export class Spire extends EventEmitter {
|
|
|
148
177
|
{ deviceID: string; nonce: string; time: number }
|
|
149
178
|
>();
|
|
150
179
|
private queuedRequestIncrements = 0;
|
|
180
|
+
|
|
151
181
|
private requestsTotal = 0;
|
|
152
182
|
|
|
153
183
|
private requestsTotalLoaded = false;
|
|
154
|
-
|
|
155
184
|
private server: null | Server = null;
|
|
156
|
-
private signKeys: KeyPair;
|
|
157
185
|
|
|
186
|
+
private signKeys: KeyPair;
|
|
158
187
|
private readonly startedAt = new Date();
|
|
159
188
|
private readonly version = getAppVersion();
|
|
160
189
|
private wss: WebSocketServer = new WebSocketServer({
|
|
@@ -164,7 +193,19 @@ export class Spire extends EventEmitter {
|
|
|
164
193
|
|
|
165
194
|
constructor(SK: string, options?: SpireOptions) {
|
|
166
195
|
super();
|
|
167
|
-
this.
|
|
196
|
+
this.cryptoProfile = options?.cryptoProfile ?? "tweetnacl";
|
|
197
|
+
if (pendingFipsKeyPair) {
|
|
198
|
+
this.signKeys = pendingFipsKeyPair;
|
|
199
|
+
pendingFipsKeyPair = null;
|
|
200
|
+
} else {
|
|
201
|
+
if (this.cryptoProfile === "fips") {
|
|
202
|
+
throw new Error(
|
|
203
|
+
'FIPS: use `await Spire.createAsync(secretKeyHex, { ...options, cryptoProfile: "fips" })` instead of `new Spire()`.',
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
setCryptoProfile(this.cryptoProfile);
|
|
207
|
+
this.signKeys = xSignKeyPairFromSecret(XUtils.decodeHex(SK));
|
|
208
|
+
}
|
|
168
209
|
|
|
169
210
|
// Trust a single proxy hop (nginx / cloudflare / load balancer).
|
|
170
211
|
// Required so `req.ip` and `express-rate-limit`'s keyGenerator see
|
|
@@ -181,7 +222,34 @@ export class Spire extends EventEmitter {
|
|
|
181
222
|
});
|
|
182
223
|
});
|
|
183
224
|
|
|
184
|
-
this.init(options?.apiPort
|
|
225
|
+
this.init(resolveSpireListenPort(options?.apiPort));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Construct Spire when the crypto profile is `fips` (async sign-key derivation).
|
|
230
|
+
* For `tweetnacl` (default) you can use `new Spire()` directly.
|
|
231
|
+
*/
|
|
232
|
+
public static async createAsync(
|
|
233
|
+
secretKeyHex: string,
|
|
234
|
+
options?: SpireOptions,
|
|
235
|
+
): Promise<Spire> {
|
|
236
|
+
if ((options?.cryptoProfile ?? "tweetnacl") !== "fips") {
|
|
237
|
+
return new Spire(secretKeyHex, options);
|
|
238
|
+
}
|
|
239
|
+
if (typeof globalThis.crypto.subtle !== "object") {
|
|
240
|
+
throw new Error(
|
|
241
|
+
"FIPS: Spire requires `globalThis.crypto.subtle` (e.g. Node 20+ with global Web Crypto).",
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
setCryptoProfile("fips");
|
|
245
|
+
try {
|
|
246
|
+
pendingFipsKeyPair = await xSignKeyPairFromSecretAsync(
|
|
247
|
+
XUtils.decodeHex(secretKeyHex),
|
|
248
|
+
);
|
|
249
|
+
return new Spire(secretKeyHex, options);
|
|
250
|
+
} finally {
|
|
251
|
+
pendingFipsKeyPair = null;
|
|
252
|
+
}
|
|
185
253
|
}
|
|
186
254
|
|
|
187
255
|
public async close(): Promise<void> {
|
|
@@ -232,6 +300,21 @@ export class Spire extends EventEmitter {
|
|
|
232
300
|
}
|
|
233
301
|
|
|
234
302
|
private init(apiPort: number): void {
|
|
303
|
+
// Request traces (UUIDs and device public-key path segments redacted
|
|
304
|
+
// in the `url` token). Enabled in all envs, including production.
|
|
305
|
+
const accessFlag = process.env["SPIRE_HTTP_ACCESS_LOG"];
|
|
306
|
+
const accessLogEnabled =
|
|
307
|
+
accessFlag !== "0" &&
|
|
308
|
+
accessFlag !== "false" &&
|
|
309
|
+
accessFlag !== "off";
|
|
310
|
+
if (accessLogEnabled) {
|
|
311
|
+
morgan.token("url", (req: IncomingMessage) => {
|
|
312
|
+
const r = req as IncomingMessage & { originalUrl?: string };
|
|
313
|
+
return redactAccessLogUrl(r.originalUrl ?? r.url ?? "");
|
|
314
|
+
});
|
|
315
|
+
this.api.use(morgan("dev"));
|
|
316
|
+
}
|
|
317
|
+
|
|
235
318
|
this.api.use((_req, _res, next) => {
|
|
236
319
|
this.requestsTotal += 1;
|
|
237
320
|
|
|
@@ -432,12 +515,16 @@ export class Spire extends EventEmitter {
|
|
|
432
515
|
res.json({ dbReady: true, ok: true });
|
|
433
516
|
});
|
|
434
517
|
|
|
435
|
-
this.api.get("/status", async (
|
|
518
|
+
this.api.get("/status", async (req, res) => {
|
|
436
519
|
const started = Date.now();
|
|
437
520
|
const dbHealthy = this.dbReady ? await this.db.isHealthy() : false;
|
|
438
521
|
const checkDurationMs = Date.now() - started;
|
|
439
522
|
|
|
440
523
|
const ok = dbHealthy;
|
|
524
|
+
if (!devApiKeySkipsRateLimits(req)) {
|
|
525
|
+
res.json({ cryptoProfile: this.cryptoProfile, ok });
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
441
528
|
const canaryEnv = process.env["CANARY"]?.trim().toLowerCase();
|
|
442
529
|
res.json({
|
|
443
530
|
canary:
|
|
@@ -445,6 +532,7 @@ export class Spire extends EventEmitter {
|
|
|
445
532
|
canaryEnv === "true" ||
|
|
446
533
|
canaryEnv === "yes",
|
|
447
534
|
checkDurationMs,
|
|
535
|
+
cryptoProfile: this.cryptoProfile,
|
|
448
536
|
now: new Date(),
|
|
449
537
|
ok,
|
|
450
538
|
version: this.version,
|
|
@@ -592,7 +680,7 @@ export class Spire extends EventEmitter {
|
|
|
592
680
|
}
|
|
593
681
|
|
|
594
682
|
// Verify the Ed25519 signature
|
|
595
|
-
const opened =
|
|
683
|
+
const opened = await spireXSignOpenAsync(
|
|
596
684
|
XUtils.decodeHex(signed),
|
|
597
685
|
XUtils.decodeHex(device.signKey),
|
|
598
686
|
);
|
|
@@ -743,13 +831,14 @@ export class Spire extends EventEmitter {
|
|
|
743
831
|
return;
|
|
744
832
|
}
|
|
745
833
|
|
|
746
|
-
const regKey =
|
|
834
|
+
const regKey = await spireXSignOpenAsync(
|
|
747
835
|
XUtils.decodeHex(regPayload.signed),
|
|
748
836
|
XUtils.decodeHex(regPayload.signKey),
|
|
749
837
|
);
|
|
750
838
|
|
|
751
839
|
if (
|
|
752
840
|
regKey &&
|
|
841
|
+
regKey.length === 16 &&
|
|
753
842
|
this.validateToken(
|
|
754
843
|
uuidStringify(regKey),
|
|
755
844
|
TokenScopes.Register,
|
|
@@ -800,14 +889,28 @@ export class Spire extends EventEmitter {
|
|
|
800
889
|
}
|
|
801
890
|
res.send(msgpack.encode(censorUser(user)));
|
|
802
891
|
}
|
|
892
|
+
} else if (regKey && regKey.length !== 16) {
|
|
893
|
+
res.status(400).send({
|
|
894
|
+
error: "Invalid registration token payload.",
|
|
895
|
+
});
|
|
803
896
|
} else {
|
|
804
897
|
res.status(400).send({
|
|
805
898
|
error: "Invalid or no token supplied.",
|
|
806
899
|
});
|
|
807
900
|
}
|
|
808
|
-
} catch (
|
|
809
|
-
|
|
810
|
-
|
|
901
|
+
} catch (err: unknown) {
|
|
902
|
+
const requestId = crypto.randomUUID();
|
|
903
|
+
const message =
|
|
904
|
+
err instanceof Error ? err.message : String(err);
|
|
905
|
+
console.error(
|
|
906
|
+
`[spire] /register failed requestId=${requestId} profile=${this.cryptoProfile} message=${message}`,
|
|
907
|
+
);
|
|
908
|
+
if (err instanceof Error && err.stack) {
|
|
909
|
+
console.error(err.stack);
|
|
910
|
+
}
|
|
911
|
+
res.status(500).json({
|
|
912
|
+
error: `Registration failed. requestId=${requestId}`,
|
|
913
|
+
});
|
|
811
914
|
}
|
|
812
915
|
});
|
|
813
916
|
|
|
@@ -71,7 +71,8 @@ describe("Database", () => {
|
|
|
71
71
|
it("takes a userId and one time key, adds a keyId and saves it to oneTimeKey table", async () => {
|
|
72
72
|
expect.assertions(1);
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
// uuid@14 overloads: mock the string return path (v4 with no `buf` argument).
|
|
75
|
+
vi.mocked(uuid.v4 as () => string).mockReturnValueOnce(keyID);
|
|
75
76
|
|
|
76
77
|
const provider = new Database(options);
|
|
77
78
|
await new Promise<void>((resolve, reject) => {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2020-2026 Vex Heavy Industries LLC
|
|
3
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
4
|
+
* Commercial licenses available at vex.wtf
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, it } from "vitest";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_SPIRE_API_PORT,
|
|
11
|
+
resolveSpireListenPort,
|
|
12
|
+
} from "../spireListenPort.ts";
|
|
13
|
+
|
|
14
|
+
describe("resolveSpireListenPort", () => {
|
|
15
|
+
it("uses default for tweetnacl and fips when no explicit port", () => {
|
|
16
|
+
expect(resolveSpireListenPort(undefined)).toBe(DEFAULT_SPIRE_API_PORT);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("honors explicit api port", () => {
|
|
20
|
+
expect(resolveSpireListenPort(9000)).toBe(9000);
|
|
21
|
+
});
|
|
22
|
+
});
|
package/src/index.ts
CHANGED
package/src/run.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type { SpireOptions } from "./Spire.ts";
|
|
|
9
9
|
import { Spire } from "./Spire.ts";
|
|
10
10
|
import { loadEnv } from "./utils/loadEnv.ts";
|
|
11
11
|
|
|
12
|
-
function main() {
|
|
12
|
+
async function main() {
|
|
13
13
|
// load the environment variables — loadEnv() exits if required vars are missing
|
|
14
14
|
loadEnv();
|
|
15
15
|
|
|
@@ -18,13 +18,32 @@ function main() {
|
|
|
18
18
|
throw new Error("SPK must be set (loadEnv should have caught this).");
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
const
|
|
21
|
+
const rawPort = process.env["API_PORT"]?.trim() ?? "";
|
|
22
|
+
const apiPort =
|
|
23
|
+
rawPort.length > 0 ? Number.parseInt(rawPort, 10) : undefined;
|
|
24
|
+
if (apiPort !== undefined) {
|
|
25
|
+
if (!Number.isFinite(apiPort) || apiPort < 1 || apiPort > 65_535) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
`API_PORT must be 1-65535; got ${JSON.stringify(process.env["API_PORT"])}.`,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
22
31
|
const dbType = parseDbType(process.env["DB_TYPE"]);
|
|
32
|
+
const fips =
|
|
33
|
+
process.env["SPIRE_FIPS"] === "1" ||
|
|
34
|
+
process.env["SPIRE_FIPS"] === "true";
|
|
23
35
|
|
|
24
|
-
|
|
25
|
-
...(apiPort !== undefined ? { apiPort
|
|
36
|
+
const options: SpireOptions = {
|
|
37
|
+
...(apiPort !== undefined ? { apiPort } : {}),
|
|
26
38
|
...(dbType !== undefined ? { dbType } : {}),
|
|
27
|
-
|
|
39
|
+
...(fips ? { cryptoProfile: "fips" } : {}),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
if (fips) {
|
|
43
|
+
await Spire.createAsync(spk, options);
|
|
44
|
+
} else {
|
|
45
|
+
new Spire(spk, options);
|
|
46
|
+
}
|
|
28
47
|
}
|
|
29
48
|
|
|
30
49
|
function parseDbType(value: string | undefined): SpireOptions["dbType"] {
|
|
@@ -39,4 +58,7 @@ function parseDbType(value: string | undefined): SpireOptions["dbType"] {
|
|
|
39
58
|
}
|
|
40
59
|
}
|
|
41
60
|
|
|
42
|
-
main()
|
|
61
|
+
void main().catch((err: unknown) => {
|
|
62
|
+
console.error(err);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
});
|
package/src/server/index.ts
CHANGED
|
@@ -12,8 +12,7 @@ import * as fsp from "node:fs/promises";
|
|
|
12
12
|
|
|
13
13
|
import express from "express";
|
|
14
14
|
|
|
15
|
-
import { XUtils } from "@vex-chat/crypto";
|
|
16
|
-
import { type KeyPair, xSignOpen } from "@vex-chat/crypto";
|
|
15
|
+
import { type KeyPair, XUtils } from "@vex-chat/crypto";
|
|
17
16
|
import { PreKeysWSSchema, TokenScopes, UserSchema } from "@vex-chat/types";
|
|
18
17
|
|
|
19
18
|
import cors from "cors";
|
|
@@ -29,6 +28,7 @@ import { POWER_LEVELS } from "../ClientManager.ts";
|
|
|
29
28
|
import { JWT_EXPIRY } from "../Spire.ts";
|
|
30
29
|
import { getJwtSecret } from "../utils/jwtSecret.ts";
|
|
31
30
|
import { msgpack } from "../utils/msgpack.ts";
|
|
31
|
+
import { spireXSignOpenAsync } from "../utils/spireXSignOpenAsync.ts";
|
|
32
32
|
|
|
33
33
|
import { getAvatarRouter } from "./avatar.ts";
|
|
34
34
|
import { errorHandler } from "./errors.ts";
|
|
@@ -181,7 +181,7 @@ export const initApp = (
|
|
|
181
181
|
) => void,
|
|
182
182
|
) => {
|
|
183
183
|
// INIT ROUTERS
|
|
184
|
-
const userRouter = getUserRouter(db, tokenValidator);
|
|
184
|
+
const userRouter = getUserRouter(db, tokenValidator, notify);
|
|
185
185
|
const fileRouter = getFileRouter(db);
|
|
186
186
|
const avatarRouter = getAvatarRouter();
|
|
187
187
|
const inviteRouter = getInviteRouter(db, tokenValidator, notify);
|
|
@@ -198,16 +198,10 @@ export const initApp = (
|
|
|
198
198
|
type: "application/msgpack",
|
|
199
199
|
}),
|
|
200
200
|
);
|
|
201
|
-
api.use(helmet());
|
|
202
|
-
api.use(msgpackParser);
|
|
203
|
-
api.use(checkAuth);
|
|
204
|
-
api.use(checkDevice);
|
|
205
201
|
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
// usual bearer-API pattern. Set `CORS_ORIGINS` to a comma-separated allowlist
|
|
210
|
-
// when an operator wants to restrict which frontends may call the API.
|
|
202
|
+
// CORS before helmet/auth so browser preflight (OPTIONS) and PATCH get
|
|
203
|
+
// Access-Control-* headers. Node clients ignore CORS; browsers do not.
|
|
204
|
+
// Set `CORS_ORIGINS` to a comma-separated allowlist to restrict frontends.
|
|
211
205
|
const corsRaw = process.env["CORS_ORIGINS"];
|
|
212
206
|
const corsOrigins = corsRaw
|
|
213
207
|
? corsRaw
|
|
@@ -218,6 +212,15 @@ export const initApp = (
|
|
|
218
212
|
api.use(
|
|
219
213
|
cors({
|
|
220
214
|
credentials: true,
|
|
215
|
+
methods: [
|
|
216
|
+
"GET",
|
|
217
|
+
"HEAD",
|
|
218
|
+
"POST",
|
|
219
|
+
"PUT",
|
|
220
|
+
"PATCH",
|
|
221
|
+
"DELETE",
|
|
222
|
+
"OPTIONS",
|
|
223
|
+
],
|
|
221
224
|
origin:
|
|
222
225
|
corsOrigins.length > 0
|
|
223
226
|
? corsOrigins
|
|
@@ -225,6 +228,11 @@ export const initApp = (
|
|
|
225
228
|
}),
|
|
226
229
|
);
|
|
227
230
|
|
|
231
|
+
api.use(helmet());
|
|
232
|
+
api.use(msgpackParser);
|
|
233
|
+
api.use(checkAuth);
|
|
234
|
+
api.use(checkDevice);
|
|
235
|
+
|
|
228
236
|
api.get("/server/:id", protect, async (req, res) => {
|
|
229
237
|
const server = await db.retrieveServer(getParam(req, "id"));
|
|
230
238
|
|
|
@@ -576,7 +584,10 @@ export const initApp = (
|
|
|
576
584
|
return;
|
|
577
585
|
}
|
|
578
586
|
|
|
579
|
-
const regKey =
|
|
587
|
+
const regKey = await spireXSignOpenAsync(
|
|
588
|
+
signed,
|
|
589
|
+
XUtils.decodeHex(device.signKey),
|
|
590
|
+
);
|
|
580
591
|
if (
|
|
581
592
|
regKey &&
|
|
582
593
|
tokenValidator(uuidStringify(regKey), TokenScopes.Connect)
|
|
@@ -628,7 +639,7 @@ export const initApp = (
|
|
|
628
639
|
return;
|
|
629
640
|
}
|
|
630
641
|
|
|
631
|
-
const message =
|
|
642
|
+
const message = await spireXSignOpenAsync(
|
|
632
643
|
otk.signature,
|
|
633
644
|
XUtils.decodeHex(device.signKey),
|
|
634
645
|
);
|