@vex-chat/spire 1.0.4 → 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 +43 -35
- 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 +83 -20
- 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 +4 -4
- 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 -121
- package/src/ClientManager.ts +2 -2
- package/src/Database.ts +84 -116
- package/src/Spire.ts +108 -26
- 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 +8 -5
- package/src/server/user.ts +313 -10
- package/src/spireListenPort.ts +25 -0
- package/src/utils/spireXSignOpenAsync.ts +20 -0
package/package.json
CHANGED
|
@@ -1,122 +1,111 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
"
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
"
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
"
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
"
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
"kysely": "0.28.16",
|
|
113
|
-
"morgan": "1.10.1",
|
|
114
|
-
"msgpackr": "1.11.8",
|
|
115
|
-
"multer": "2.1.1",
|
|
116
|
-
"parse-duration": "2.1.6",
|
|
117
|
-
"tweetnacl": "1.0.3",
|
|
118
|
-
"uuid": "8.3.2",
|
|
119
|
-
"ws": "8.20.0",
|
|
120
|
-
"zod": "4.3.6"
|
|
121
|
-
}
|
|
122
|
-
}
|
|
2
|
+
"name": "@vex-chat/spire",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Vex server implementation in NodeJS.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist",
|
|
8
|
+
"src",
|
|
9
|
+
"LICENSE",
|
|
10
|
+
"LICENSE-COMMERCIAL",
|
|
11
|
+
"LICENSING.md",
|
|
12
|
+
"CLA.md"
|
|
13
|
+
],
|
|
14
|
+
"author": "Extra <extrahash@protonmail.com>",
|
|
15
|
+
"license": "AGPL-3.0-or-later",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/vex-protocol/protocol.git",
|
|
19
|
+
"directory": "apps/spire"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/vex-protocol/protocol/issues"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/vex-protocol/protocol/tree/master/apps/spire#readme",
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=24.0.0"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public",
|
|
30
|
+
"registry": "https://registry.npmjs.org/"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@changesets/cli": "2.30.0",
|
|
34
|
+
"@socketsecurity/cli": "1.1.84",
|
|
35
|
+
"@types/better-sqlite3": "7.6.13",
|
|
36
|
+
"@types/cors": "2.8.19",
|
|
37
|
+
"@types/express": "5.0.6",
|
|
38
|
+
"@types/express-serve-static-core": "5.1.1",
|
|
39
|
+
"@types/jsonwebtoken": "9.0.10",
|
|
40
|
+
"@types/morgan": "1.9.10",
|
|
41
|
+
"@types/multer": "2.1.0",
|
|
42
|
+
"@types/node": "^25.6.0",
|
|
43
|
+
"@types/ws": "8.18.1",
|
|
44
|
+
"@vitest/eslint-plugin": "1.6.15",
|
|
45
|
+
"axios": "1.15.0",
|
|
46
|
+
"eslint": "10.2.0",
|
|
47
|
+
"eslint-config-prettier": "10.1.8",
|
|
48
|
+
"eslint-plugin-n": "17.24.0",
|
|
49
|
+
"eslint-plugin-perfectionist": "5.8.0",
|
|
50
|
+
"husky": "9.1.7",
|
|
51
|
+
"lint-staged": "16.4.0",
|
|
52
|
+
"prettier": "3.8.2",
|
|
53
|
+
"type-coverage": "2.29.7",
|
|
54
|
+
"typedoc": "0.28.18",
|
|
55
|
+
"typescript": "6.0.2",
|
|
56
|
+
"typescript-eslint": "8.58.1",
|
|
57
|
+
"vitest": "4.1.4",
|
|
58
|
+
"@vex-chat/eslint-config": "0.0.1",
|
|
59
|
+
"@vex-chat/libvex": "^5.5.1"
|
|
60
|
+
},
|
|
61
|
+
"typeCoverage": {
|
|
62
|
+
"atLeast": 95,
|
|
63
|
+
"ignoreCatch": true
|
|
64
|
+
},
|
|
65
|
+
"dependencies": {
|
|
66
|
+
"@asyncapi/web-component": "2.6.5",
|
|
67
|
+
"@scalar/express-api-reference": "0.9.7",
|
|
68
|
+
"argon2": "0.44.0",
|
|
69
|
+
"better-sqlite3": "11.10.0",
|
|
70
|
+
"cors": "2.8.6",
|
|
71
|
+
"dotenv": "17.4.1",
|
|
72
|
+
"express": "5.2.1",
|
|
73
|
+
"express-rate-limit": "8.3.2",
|
|
74
|
+
"file-type": "22.0.1",
|
|
75
|
+
"helmet": "8.1.0",
|
|
76
|
+
"jsonwebtoken": "9.0.3",
|
|
77
|
+
"kysely": "^0.28.16",
|
|
78
|
+
"morgan": "1.10.1",
|
|
79
|
+
"msgpackr": "^1.11.9",
|
|
80
|
+
"multer": "2.1.1",
|
|
81
|
+
"parse-duration": "2.1.6",
|
|
82
|
+
"tweetnacl": "1.0.3",
|
|
83
|
+
"uuid": "^14.0.0",
|
|
84
|
+
"ws": "8.20.0",
|
|
85
|
+
"zod": "^4.3.6",
|
|
86
|
+
"@vex-chat/crypto": "^2.1.2",
|
|
87
|
+
"@vex-chat/types": "^2.0.0"
|
|
88
|
+
},
|
|
89
|
+
"scripts": {
|
|
90
|
+
"rebuild:sqlite": "pnpm rebuild better-sqlite3",
|
|
91
|
+
"start": "node --experimental-strip-types src/run.ts",
|
|
92
|
+
"restart": "pm2 restart spire && pm2 logs spire",
|
|
93
|
+
"build": "tsc && node scripts/clean-dist-tests.mjs",
|
|
94
|
+
"format": "prettier --write .",
|
|
95
|
+
"format:check": "prettier --check .",
|
|
96
|
+
"lint": "eslint src/ scripts/stress/",
|
|
97
|
+
"lint:fix": "eslint src/ scripts/stress/ --fix",
|
|
98
|
+
"test": "vitest run",
|
|
99
|
+
"stress:web": "node --experimental-strip-types scripts/stress/spire-stress.ts",
|
|
100
|
+
"stress:cli": "node --experimental-strip-types scripts/stress/stress-cli-sweep.ts",
|
|
101
|
+
"stress:repo-manifest": "node --experimental-strip-types scripts/stress/stress-repo-manifest.ts",
|
|
102
|
+
"stress:docs-pack": "node --experimental-strip-types scripts/stress/stress-docs-pack.ts",
|
|
103
|
+
"stress:llm-triage": "node --experimental-strip-types scripts/stress/stress-llm-triage.ts",
|
|
104
|
+
"gen-spk": "node scripts/gen-spk.js",
|
|
105
|
+
"gen-spk-fips": "node scripts/gen-spk-fips.js",
|
|
106
|
+
"docs": "node ./scripts/generate-docs.js",
|
|
107
|
+
"service:status-monitor": "node ./services/status-monitor/index.js",
|
|
108
|
+
"service:status-monitor:clear-history": "node ./services/status-monitor/clear-history.js",
|
|
109
|
+
"monitor": "node ./services/status-monitor/index.js"
|
|
110
|
+
}
|
|
111
|
+
}
|
package/src/ClientManager.ts
CHANGED
|
@@ -23,7 +23,6 @@ import { EventEmitter } from "events";
|
|
|
23
23
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
24
24
|
|
|
25
25
|
import { xConcat, XUtils } from "@vex-chat/crypto";
|
|
26
|
-
import { xSignOpen } from "@vex-chat/crypto";
|
|
27
26
|
import { MailWSSchema, SocketAuthErrors } from "@vex-chat/types";
|
|
28
27
|
|
|
29
28
|
import { parse as uuidParse, validate as uuidValidate } from "uuid";
|
|
@@ -31,6 +30,7 @@ import { parse as uuidParse, validate as uuidValidate } from "uuid";
|
|
|
31
30
|
import { TOKEN_EXPIRY } from "./Spire.ts";
|
|
32
31
|
import { createUint8UUID } from "./utils/createUint8UUID.ts";
|
|
33
32
|
import { msgpack } from "./utils/msgpack.ts";
|
|
33
|
+
import { spireXSignOpenAsync } from "./utils/spireXSignOpenAsync.ts";
|
|
34
34
|
|
|
35
35
|
export const POWER_LEVELS = {
|
|
36
36
|
CREATE: 50,
|
|
@@ -341,7 +341,7 @@ export class ClientManager extends EventEmitter {
|
|
|
341
341
|
const devices = await this.db.retrieveUserDeviceList([user.userID]);
|
|
342
342
|
let message: null | Uint8Array = null;
|
|
343
343
|
for (const device of devices) {
|
|
344
|
-
const verified =
|
|
344
|
+
const verified = await spireXSignOpenAsync(
|
|
345
345
|
msg.signed,
|
|
346
346
|
XUtils.decodeHex(device.signKey),
|
|
347
347
|
);
|
package/src/Database.ts
CHANGED
|
@@ -32,7 +32,11 @@ import * as fs from "node:fs/promises";
|
|
|
32
32
|
import path from "node:path";
|
|
33
33
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
34
34
|
|
|
35
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
fipsEcdhRawPublicKeyFromEcdsaSpkiAsync,
|
|
37
|
+
getCryptoProfile,
|
|
38
|
+
XUtils,
|
|
39
|
+
} from "@vex-chat/crypto";
|
|
36
40
|
import { MailType } from "@vex-chat/types";
|
|
37
41
|
|
|
38
42
|
import argon2 from "argon2";
|
|
@@ -124,21 +128,9 @@ export interface InternalUserRecord extends UserRecord {
|
|
|
124
128
|
}
|
|
125
129
|
|
|
126
130
|
export class Database extends EventEmitter {
|
|
127
|
-
private
|
|
128
|
-
|
|
129
|
-
/** Underlying better-sqlite3 handle (file or :memory:). */
|
|
130
|
-
private readonly rawSqlite: InstanceType<typeof BetterSqlite3>;
|
|
131
|
+
private static readonly EMOJI_LIST_CACHE_TTL_MS = 10_000;
|
|
131
132
|
|
|
132
|
-
|
|
133
|
-
* In-process cache for {@link retrieveUserDeviceList} when queried for a
|
|
134
|
-
* single owner. Libvex calls GET /user/:id/devices on every outgoing
|
|
135
|
-
* `forward()`; stress runs otherwise hammer SQLite with identical reads.
|
|
136
|
-
* Cleared on device create/delete and when the DB is closed.
|
|
137
|
-
*/
|
|
138
|
-
private readonly userDeviceListCache = new Map<
|
|
139
|
-
string,
|
|
140
|
-
{ devices: Device[]; until: number }
|
|
141
|
-
>();
|
|
133
|
+
private db: Kysely<ServerDatabase>;
|
|
142
134
|
|
|
143
135
|
/**
|
|
144
136
|
* Short TTL cache for {@link retrieveEmojiList} (key = server id, stored as
|
|
@@ -151,8 +143,8 @@ export class Database extends EventEmitter {
|
|
|
151
143
|
{ rows: Emoji[]; until: number }
|
|
152
144
|
>();
|
|
153
145
|
|
|
154
|
-
|
|
155
|
-
private
|
|
146
|
+
/** Underlying better-sqlite3 handle (file or :memory:). */
|
|
147
|
+
private readonly rawSqlite: InstanceType<typeof BetterSqlite3>;
|
|
156
148
|
|
|
157
149
|
constructor(options?: SpireOptions) {
|
|
158
150
|
super();
|
|
@@ -188,70 +180,7 @@ export class Database extends EventEmitter {
|
|
|
188
180
|
void this.init();
|
|
189
181
|
}
|
|
190
182
|
|
|
191
|
-
/**
|
|
192
|
-
* Dev-only snapshot of SQLite files + pragmas (same process as Spire).
|
|
193
|
-
* Not for production callers.
|
|
194
|
-
*/
|
|
195
|
-
public getDevSqliteMonitor(): Record<string, unknown> {
|
|
196
|
-
const openedAs = this.rawSqlite.name;
|
|
197
|
-
const absPath =
|
|
198
|
-
openedAs === ":memory:"
|
|
199
|
-
? ":memory:"
|
|
200
|
-
: path.isAbsolute(openedAs)
|
|
201
|
-
? openedAs
|
|
202
|
-
: path.resolve(process.cwd(), openedAs);
|
|
203
|
-
|
|
204
|
-
const journalMode = this.rawSqlite.pragma("journal_mode", {
|
|
205
|
-
simple: true,
|
|
206
|
-
});
|
|
207
|
-
const synchronous = this.rawSqlite.pragma("synchronous", {
|
|
208
|
-
simple: true,
|
|
209
|
-
});
|
|
210
|
-
const busyTimeout = this.rawSqlite.pragma("busy_timeout", {
|
|
211
|
-
simple: true,
|
|
212
|
-
});
|
|
213
|
-
const cacheSize = this.rawSqlite.pragma("cache_size", { simple: true });
|
|
214
|
-
const pageCount = this.rawSqlite.pragma("page_count", { simple: true });
|
|
215
|
-
const pageSize = this.rawSqlite.pragma("page_size", { simple: true });
|
|
216
|
-
const freelistCount = this.rawSqlite.pragma("freelist_count", {
|
|
217
|
-
simple: true,
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
const out: Record<string, unknown> = {
|
|
221
|
-
absPath,
|
|
222
|
-
busyTimeout,
|
|
223
|
-
cacheSize,
|
|
224
|
-
freelistCount,
|
|
225
|
-
journalMode,
|
|
226
|
-
openedAs,
|
|
227
|
-
pageCount,
|
|
228
|
-
pageSize,
|
|
229
|
-
synchronous,
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
if (openedAs !== ":memory:") {
|
|
233
|
-
const filePaths = {
|
|
234
|
-
main: absPath,
|
|
235
|
-
shm: `${absPath}-shm`,
|
|
236
|
-
wal: `${absPath}-wal`,
|
|
237
|
-
};
|
|
238
|
-
out["filePaths"] = filePaths;
|
|
239
|
-
const sizes: Record<string, number> = {};
|
|
240
|
-
for (const [label, fp] of Object.entries(filePaths)) {
|
|
241
|
-
try {
|
|
242
|
-
sizes[label] = statSync(fp).size;
|
|
243
|
-
} catch {
|
|
244
|
-
sizes[label] = 0;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
out["fileBytes"] = sizes;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return out;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
183
|
public async close(): Promise<void> {
|
|
254
|
-
this.userDeviceListCache.clear();
|
|
255
184
|
this.emojiListByServerCache.clear();
|
|
256
185
|
await this.db.destroy();
|
|
257
186
|
}
|
|
@@ -283,7 +212,6 @@ export class Database extends EventEmitter {
|
|
|
283
212
|
};
|
|
284
213
|
|
|
285
214
|
await this.db.insertInto("devices").values(device).execute();
|
|
286
|
-
this.userDeviceListCache.delete(owner);
|
|
287
215
|
|
|
288
216
|
const medPreKeys = {
|
|
289
217
|
deviceID: device.deviceID,
|
|
@@ -423,12 +351,6 @@ export class Database extends EventEmitter {
|
|
|
423
351
|
}
|
|
424
352
|
|
|
425
353
|
public async deleteDevice(deviceID: string): Promise<void> {
|
|
426
|
-
const ownerRow = await this.db
|
|
427
|
-
.selectFrom("devices")
|
|
428
|
-
.select("owner")
|
|
429
|
-
.where("deviceID", "=", deviceID)
|
|
430
|
-
.executeTakeFirst();
|
|
431
|
-
|
|
432
354
|
await this.db
|
|
433
355
|
.deleteFrom("preKeys")
|
|
434
356
|
.where("deviceID", "=", deviceID)
|
|
@@ -444,10 +366,6 @@ export class Database extends EventEmitter {
|
|
|
444
366
|
.set({ deleted: 1 })
|
|
445
367
|
.where("deviceID", "=", deviceID)
|
|
446
368
|
.execute();
|
|
447
|
-
|
|
448
|
-
if (ownerRow?.owner) {
|
|
449
|
-
this.userDeviceListCache.delete(ownerRow.owner);
|
|
450
|
-
}
|
|
451
369
|
}
|
|
452
370
|
|
|
453
371
|
public async deleteEmoji(emojiID: string): Promise<void> {
|
|
@@ -502,6 +420,68 @@ export class Database extends EventEmitter {
|
|
|
502
420
|
.execute();
|
|
503
421
|
}
|
|
504
422
|
|
|
423
|
+
/**
|
|
424
|
+
* Dev-only snapshot of SQLite files + pragmas (same process as Spire).
|
|
425
|
+
* Not for production callers.
|
|
426
|
+
*/
|
|
427
|
+
public getDevSqliteMonitor(): Record<string, unknown> {
|
|
428
|
+
const openedAs = this.rawSqlite.name;
|
|
429
|
+
const absPath =
|
|
430
|
+
openedAs === ":memory:"
|
|
431
|
+
? ":memory:"
|
|
432
|
+
: path.isAbsolute(openedAs)
|
|
433
|
+
? openedAs
|
|
434
|
+
: path.resolve(process.cwd(), openedAs);
|
|
435
|
+
|
|
436
|
+
const journalMode = this.rawSqlite.pragma("journal_mode", {
|
|
437
|
+
simple: true,
|
|
438
|
+
});
|
|
439
|
+
const synchronous = this.rawSqlite.pragma("synchronous", {
|
|
440
|
+
simple: true,
|
|
441
|
+
});
|
|
442
|
+
const busyTimeout = this.rawSqlite.pragma("busy_timeout", {
|
|
443
|
+
simple: true,
|
|
444
|
+
});
|
|
445
|
+
const cacheSize = this.rawSqlite.pragma("cache_size", { simple: true });
|
|
446
|
+
const pageCount = this.rawSqlite.pragma("page_count", { simple: true });
|
|
447
|
+
const pageSize = this.rawSqlite.pragma("page_size", { simple: true });
|
|
448
|
+
const freelistCount = this.rawSqlite.pragma("freelist_count", {
|
|
449
|
+
simple: true,
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
const out: Record<string, unknown> = {
|
|
453
|
+
absPath,
|
|
454
|
+
busyTimeout,
|
|
455
|
+
cacheSize,
|
|
456
|
+
freelistCount,
|
|
457
|
+
journalMode,
|
|
458
|
+
openedAs,
|
|
459
|
+
pageCount,
|
|
460
|
+
pageSize,
|
|
461
|
+
synchronous,
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
if (openedAs !== ":memory:") {
|
|
465
|
+
const filePaths = {
|
|
466
|
+
main: absPath,
|
|
467
|
+
shm: `${absPath}-shm`,
|
|
468
|
+
wal: `${absPath}-wal`,
|
|
469
|
+
};
|
|
470
|
+
out["filePaths"] = filePaths;
|
|
471
|
+
const sizes: Record<string, number> = {};
|
|
472
|
+
for (const [label, fp] of Object.entries(filePaths)) {
|
|
473
|
+
try {
|
|
474
|
+
sizes[label] = statSync(fp).size;
|
|
475
|
+
} catch {
|
|
476
|
+
sizes[label] = 0;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
out["fileBytes"] = sizes;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return out;
|
|
483
|
+
}
|
|
484
|
+
|
|
505
485
|
public async getKeyBundle(deviceID: string): Promise<KeyBundle | null> {
|
|
506
486
|
const device = await this.retrieveDevice(deviceID);
|
|
507
487
|
if (!device) {
|
|
@@ -512,10 +492,15 @@ export class Database extends EventEmitter {
|
|
|
512
492
|
if (!preKey) {
|
|
513
493
|
throw new Error("Failed to get prekey.");
|
|
514
494
|
}
|
|
495
|
+
const signKeyBytes = XUtils.decodeHex(device.signKey);
|
|
496
|
+
const signKey =
|
|
497
|
+
getCryptoProfile() === "fips"
|
|
498
|
+
? await fipsEcdhRawPublicKeyFromEcdsaSpkiAsync(signKeyBytes)
|
|
499
|
+
: signKeyBytes;
|
|
515
500
|
const keyBundle: KeyBundle = {
|
|
516
501
|
otk,
|
|
517
502
|
preKey,
|
|
518
|
-
signKey
|
|
503
|
+
signKey,
|
|
519
504
|
};
|
|
520
505
|
return keyBundle;
|
|
521
506
|
}
|
|
@@ -942,37 +927,20 @@ export class Database extends EventEmitter {
|
|
|
942
927
|
return row ? toUserRecord(row) : null;
|
|
943
928
|
}
|
|
944
929
|
|
|
930
|
+
/**
|
|
931
|
+
* All devices for the given user ID(s), **no in-process cache** — a prior
|
|
932
|
+
* 10s TTL was removed: concurrent GET + POST could end with a stale
|
|
933
|
+
* snapshot overwriting the map after a new device was inserted (automation
|
|
934
|
+
* and `POST /user/:id/devices` must see a fresh list on the next read).
|
|
935
|
+
*/
|
|
945
936
|
public async retrieveUserDeviceList(userIDs: string[]): Promise<Device[]> {
|
|
946
|
-
const now = Date.now();
|
|
947
|
-
if (userIDs.length === 1) {
|
|
948
|
-
const owner = userIDs[0];
|
|
949
|
-
if (typeof owner === "string") {
|
|
950
|
-
const hit = this.userDeviceListCache.get(owner);
|
|
951
|
-
if (hit && hit.until > now) {
|
|
952
|
-
return hit.devices.map((d) => ({ ...d }));
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
|
|
957
937
|
const rows = await this.db
|
|
958
938
|
.selectFrom("devices")
|
|
959
939
|
.selectAll()
|
|
960
940
|
.where("owner", "in", userIDs)
|
|
961
941
|
.where("deleted", "=", 0)
|
|
962
942
|
.execute();
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
if (userIDs.length === 1) {
|
|
966
|
-
const owner = userIDs[0];
|
|
967
|
-
if (typeof owner === "string") {
|
|
968
|
-
this.userDeviceListCache.set(owner, {
|
|
969
|
-
devices: devices.map((d) => ({ ...d })),
|
|
970
|
-
until: now + Database.USER_DEVICE_LIST_CACHE_TTL_MS,
|
|
971
|
-
});
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
return devices;
|
|
943
|
+
return rows.map(toDevice);
|
|
976
944
|
}
|
|
977
945
|
|
|
978
946
|
public async retrieveUsers(): Promise<InternalUserRecord[]> {
|