@vex-chat/spire 1.0.1 → 1.0.3
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/CLA.md +38 -0
- package/LICENSE-COMMERCIAL +10 -0
- package/LICENSING.md +15 -0
- package/README.md +15 -13
- package/dist/ClientManager.d.ts +5 -0
- package/dist/ClientManager.js +6 -2
- package/dist/ClientManager.js.map +1 -1
- package/dist/Database.d.ts +29 -1
- package/dist/Database.js +206 -64
- package/dist/Database.js.map +1 -1
- package/dist/Spire.d.ts +5 -0
- package/dist/Spire.js +86 -19
- package/dist/Spire.js.map +1 -1
- package/dist/db/schema.d.ts +5 -0
- package/dist/db/schema.js +5 -0
- package/dist/db/schema.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/middleware/validate.d.ts +5 -0
- package/dist/middleware/validate.js +5 -0
- package/dist/middleware/validate.js.map +1 -1
- package/dist/migrations/2026-04-06_initial-schema.d.ts +5 -0
- package/dist/migrations/2026-04-06_initial-schema.js +5 -0
- package/dist/migrations/2026-04-06_initial-schema.js.map +1 -1
- package/dist/migrations/2026-04-14_argon2id-password-hashing.d.ts +5 -0
- package/dist/migrations/2026-04-14_argon2id-password-hashing.js +5 -0
- package/dist/migrations/2026-04-14_argon2id-password-hashing.js.map +1 -1
- package/dist/run.d.ts +5 -0
- package/dist/run.js +5 -0
- package/dist/run.js.map +1 -1
- package/dist/server/avatar.d.ts +5 -0
- package/dist/server/avatar.js +5 -0
- package/dist/server/avatar.js.map +1 -1
- package/dist/server/errors.d.ts +5 -0
- package/dist/server/errors.js +5 -0
- package/dist/server/errors.js.map +1 -1
- package/dist/server/file.d.ts +5 -0
- package/dist/server/file.js +5 -0
- package/dist/server/file.js.map +1 -1
- package/dist/server/index.d.ts +5 -0
- package/dist/server/index.js +20 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/invite.d.ts +5 -0
- package/dist/server/invite.js +5 -0
- package/dist/server/invite.js.map +1 -1
- package/dist/server/openapi.d.ts +5 -0
- package/dist/server/openapi.js +5 -0
- package/dist/server/openapi.js.map +1 -1
- package/dist/server/permissions.d.ts +5 -0
- package/dist/server/permissions.js +5 -0
- package/dist/server/permissions.js.map +1 -1
- package/dist/server/rateLimit.d.ts +15 -0
- package/dist/server/rateLimit.js +56 -0
- package/dist/server/rateLimit.js.map +1 -1
- package/dist/server/user.d.ts +5 -0
- package/dist/server/user.js +12 -3
- package/dist/server/user.js.map +1 -1
- package/dist/server/utils.d.ts +5 -0
- package/dist/server/utils.js +5 -0
- package/dist/server/utils.js.map +1 -1
- package/dist/types/express.d.ts +5 -0
- package/dist/types/express.js +5 -0
- package/dist/types/express.js.map +1 -1
- package/dist/utils/createUint8UUID.d.ts +5 -0
- package/dist/utils/createUint8UUID.js +5 -0
- package/dist/utils/createUint8UUID.js.map +1 -1
- package/dist/utils/jwtSecret.d.ts +5 -0
- package/dist/utils/jwtSecret.js +5 -0
- package/dist/utils/jwtSecret.js.map +1 -1
- package/dist/utils/loadEnv.d.ts +5 -0
- package/dist/utils/loadEnv.js +5 -0
- package/dist/utils/loadEnv.js.map +1 -1
- package/dist/utils/msgpack.d.ts +5 -0
- package/dist/utils/msgpack.js +5 -0
- package/dist/utils/msgpack.js.map +1 -1
- package/package.json +18 -8
- package/src/ClientManager.ts +7 -2
- package/src/Database.ts +231 -68
- package/src/Spire.ts +91 -23
- package/src/__tests__/Database.spec.ts +6 -0
- package/src/ambient-modules.d.ts +6 -0
- package/src/db/schema.ts +6 -0
- package/src/index.ts +6 -0
- package/src/middleware/validate.ts +6 -0
- package/src/migrations/2026-04-06_initial-schema.ts +6 -0
- package/src/migrations/2026-04-14_argon2id-password-hashing.ts +6 -0
- package/src/run.ts +6 -0
- package/src/server/avatar.ts +6 -0
- package/src/server/errors.ts +6 -0
- package/src/server/file.ts +6 -0
- package/src/server/index.ts +22 -2
- package/src/server/invite.ts +6 -0
- package/src/server/openapi.ts +6 -0
- package/src/server/permissions.ts +6 -0
- package/src/server/rateLimit.ts +43 -3
- package/src/server/user.ts +13 -3
- package/src/server/utils.ts +6 -0
- package/src/types/express.ts +6 -0
- package/src/utils/createUint8UUID.ts +6 -0
- package/src/utils/jwtSecret.ts +6 -0
- package/src/utils/loadEnv.ts +6 -0
- package/src/utils/msgpack.ts +6 -0
- package/dist/__tests__/Database.spec.d.ts +0 -1
- package/dist/__tests__/Database.spec.js +0 -122
- package/dist/__tests__/Database.spec.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jwtSecret.js","sourceRoot":"","sources":["../../src/utils/jwtSecret.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,UAAU,YAAY;IACxB,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IACzC,IAAI,CAAC,MAAM,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CACX,mEAAmE,CACtE,CAAC;IACN,CAAC;IACD,OAAO,MAAM,CAAC;AAClB,CAAC"}
|
|
1
|
+
{"version":3,"file":"jwtSecret.js","sourceRoot":"","sources":["../../src/utils/jwtSecret.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;;GAKG;AACH,MAAM,UAAU,YAAY;IACxB,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IACzC,IAAI,CAAC,MAAM,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CACX,mEAAmE,CACtE,CAAC;IACN,CAAC;IACD,OAAO,MAAM,CAAC;AAClB,CAAC"}
|
package/dist/utils/loadEnv.d.ts
CHANGED
package/dist/utils/loadEnv.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
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
|
+
*/
|
|
1
6
|
import { config } from "dotenv";
|
|
2
7
|
/* Populate process.env with vars from .env and verify required vars are present. */
|
|
3
8
|
export function loadEnv() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loadEnv.js","sourceRoot":"","sources":["../../src/utils/loadEnv.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,oFAAoF;AACpF,MAAM,UAAU,OAAO;IACnB,MAAM,EAAE,CAAC;IACT,MAAM,eAAe,GAAa,CAAC,SAAS,EAAE,YAAY,EAAE,KAAK,CAAC,CAAC;IACnE,KAAK,MAAM,QAAQ,IAAI,eAAe,EAAE,CAAC;QACrC,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,SAAS,EAAE,CAAC;YACtC,OAAO,CAAC,MAAM,CAAC,KAAK,CAChB,kCAAkC,QAAQ,4CAA4C,CACzF,CAAC;YACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACpB,CAAC;IACL,CAAC;AACL,CAAC"}
|
|
1
|
+
{"version":3,"file":"loadEnv.js","sourceRoot":"","sources":["../../src/utils/loadEnv.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,oFAAoF;AACpF,MAAM,UAAU,OAAO;IACnB,MAAM,EAAE,CAAC;IACT,MAAM,eAAe,GAAa,CAAC,SAAS,EAAE,YAAY,EAAE,KAAK,CAAC,CAAC;IACnE,KAAK,MAAM,QAAQ,IAAI,eAAe,EAAE,CAAC;QACrC,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,SAAS,EAAE,CAAC;YACtC,OAAO,CAAC,MAAM,CAAC,KAAK,CAChB,kCAAkC,QAAQ,4CAA4C,CACzF,CAAC;YACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACpB,CAAC;IACL,CAAC;AACL,CAAC"}
|
package/dist/utils/msgpack.d.ts
CHANGED
package/dist/utils/msgpack.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
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
|
+
*/
|
|
1
6
|
import { Packr } from "msgpackr";
|
|
2
7
|
// Standard msgpack encoder/decoder configured for broad compatibility.
|
|
3
8
|
export const msgpack = new Packr({ moreTypes: false, useRecords: false });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"msgpack.js","sourceRoot":"","sources":["../../src/utils/msgpack.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEjC,uEAAuE;AACvE,MAAM,CAAC,MAAM,OAAO,GAAG,IAAI,KAAK,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC"}
|
|
1
|
+
{"version":3,"file":"msgpack.js","sourceRoot":"","sources":["../../src/utils/msgpack.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEjC,uEAAuE;AACvE,MAAM,CAAC,MAAM,OAAO,GAAG,IAAI,KAAK,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vex-chat/spire",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Vex server implementation in NodeJS.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
7
|
"dist",
|
|
8
8
|
"src",
|
|
9
|
-
"LICENSE"
|
|
9
|
+
"LICENSE",
|
|
10
|
+
"LICENSE-COMMERCIAL",
|
|
11
|
+
"LICENSING.md",
|
|
12
|
+
"CLA.md"
|
|
10
13
|
],
|
|
11
14
|
"author": "Extra <extrahash@protonmail.com>",
|
|
12
15
|
"license": "AGPL-3.0-or-later",
|
|
@@ -23,16 +26,20 @@
|
|
|
23
26
|
"preinstall": "npx only-allow npm",
|
|
24
27
|
"start": "node --experimental-strip-types src/run.ts",
|
|
25
28
|
"restart": "pm2 restart spire && pm2 logs spire",
|
|
26
|
-
"build": "tsc",
|
|
29
|
+
"build": "tsc && node scripts/clean-dist-tests.mjs",
|
|
27
30
|
"format": "prettier --write .",
|
|
28
31
|
"format:check": "prettier --check .",
|
|
29
|
-
"lint": "eslint src/",
|
|
30
|
-
"lint:fix": "eslint src/ --fix",
|
|
32
|
+
"lint": "eslint src/ scripts/stress/",
|
|
33
|
+
"lint:fix": "eslint src/ scripts/stress/ --fix",
|
|
31
34
|
"prepare": "husky || true",
|
|
32
35
|
"test": "vitest run",
|
|
36
|
+
"stress:web": "node --experimental-strip-types scripts/stress/spire-stress.ts",
|
|
37
|
+
"stress:cli": "node --experimental-strip-types scripts/stress/stress-cli-sweep.ts",
|
|
38
|
+
"stress:repo-manifest": "node --experimental-strip-types scripts/stress/stress-repo-manifest.ts",
|
|
39
|
+
"stress:docs-pack": "node --experimental-strip-types scripts/stress/stress-docs-pack.ts",
|
|
40
|
+
"stress:llm-triage": "node --experimental-strip-types scripts/stress/stress-llm-triage.ts",
|
|
33
41
|
"gen-spk": "node scripts/gen-spk.js",
|
|
34
42
|
"docs": "node ./scripts/generate-docs.js",
|
|
35
|
-
"license:check": "node scripts/fix-license-checker.mjs && npx @onebeyond/license-checker scan --allowOnly MIT MIT-0 ISC BSD-2-Clause BSD-3-Clause Apache-2.0 0BSD MPL-2.0 CC0-1.0 CC-BY-3.0 CC-BY-4.0 Unlicense BlueOak-1.0.0 Python-2.0 Zlib PSF-2.0 Artistic-2.0 AGPL-3.0-or-later WTFPL \"(MIT AND CC-BY-3.0)\" --ignoreRootPackageLicense --disableReport",
|
|
36
43
|
"service:status-monitor": "node ./services/status-monitor/index.js",
|
|
37
44
|
"service:status-monitor:clear-history": "node ./services/status-monitor/clear-history.js",
|
|
38
45
|
"service:deploy-hook": "node ./services/deploy-hook/index.js",
|
|
@@ -40,7 +47,7 @@
|
|
|
40
47
|
},
|
|
41
48
|
"devDependencies": {
|
|
42
49
|
"@changesets/cli": "2.30.0",
|
|
43
|
-
"@
|
|
50
|
+
"@socketsecurity/cli": "1.1.84",
|
|
44
51
|
"@types/better-sqlite3": "7.6.13",
|
|
45
52
|
"@types/cors": "2.8.19",
|
|
46
53
|
"@types/express": "5.0.6",
|
|
@@ -50,13 +57,16 @@
|
|
|
50
57
|
"@types/node": "25.6.0",
|
|
51
58
|
"@types/uuid": "10.0.0",
|
|
52
59
|
"@types/ws": "8.18.1",
|
|
60
|
+
"@vex-chat/libvex": "5.2.0",
|
|
53
61
|
"@vitest/eslint-plugin": "1.6.15",
|
|
62
|
+
"axios": "1.15.0",
|
|
54
63
|
"eslint": "10.2.0",
|
|
55
64
|
"eslint-config-prettier": "10.1.8",
|
|
56
65
|
"eslint-plugin-n": "17.24.0",
|
|
57
66
|
"eslint-plugin-perfectionist": "5.8.0",
|
|
58
67
|
"husky": "9.1.7",
|
|
59
68
|
"lint-staged": "16.4.0",
|
|
69
|
+
"npm-package-json-lint": "10.2.1",
|
|
60
70
|
"prettier": "3.8.2",
|
|
61
71
|
"type-coverage": "2.29.7",
|
|
62
72
|
"typedoc": "0.28.18",
|
|
@@ -86,7 +96,7 @@
|
|
|
86
96
|
"dependencies": {
|
|
87
97
|
"@asyncapi/web-component": "2.6.5",
|
|
88
98
|
"@scalar/express-api-reference": "0.9.7",
|
|
89
|
-
"@vex-chat/crypto": "2.0.
|
|
99
|
+
"@vex-chat/crypto": "2.0.1",
|
|
90
100
|
"@vex-chat/types": "2.0.0",
|
|
91
101
|
"argon2": "0.44.0",
|
|
92
102
|
"better-sqlite3": "11.10.0",
|
package/src/ClientManager.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
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
|
+
|
|
1
7
|
import type { Database } from "./Database.ts";
|
|
2
8
|
import type {
|
|
3
9
|
BaseMsg,
|
|
@@ -271,8 +277,7 @@ export class ClientManager extends EventEmitter {
|
|
|
271
277
|
}
|
|
272
278
|
|
|
273
279
|
private async pingLoop() {
|
|
274
|
-
|
|
275
|
-
while (true) {
|
|
280
|
+
while (!this.failed) {
|
|
276
281
|
this.ping();
|
|
277
282
|
await sleep(5000);
|
|
278
283
|
}
|
package/src/Database.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
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
|
+
|
|
1
7
|
import type { ServerDatabase } from "./db/schema.ts";
|
|
2
8
|
import type { SpireOptions } from "./Spire.ts";
|
|
3
9
|
import type {
|
|
@@ -21,6 +27,7 @@ import type { Migration, MigrationProvider } from "kysely";
|
|
|
21
27
|
|
|
22
28
|
import { EventEmitter } from "events";
|
|
23
29
|
import { pbkdf2Sync } from "node:crypto";
|
|
30
|
+
import { statSync } from "node:fs";
|
|
24
31
|
import * as fs from "node:fs/promises";
|
|
25
32
|
import path from "node:path";
|
|
26
33
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -119,6 +126,34 @@ export interface InternalUserRecord extends UserRecord {
|
|
|
119
126
|
export class Database extends EventEmitter {
|
|
120
127
|
private db: Kysely<ServerDatabase>;
|
|
121
128
|
|
|
129
|
+
/** Underlying better-sqlite3 handle (file or :memory:). */
|
|
130
|
+
private readonly rawSqlite: InstanceType<typeof BetterSqlite3>;
|
|
131
|
+
|
|
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
|
+
>();
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Short TTL cache for {@link retrieveEmojiList} (key = server id, stored as
|
|
145
|
+
* `emojis.owner`). The noise harness fires `emoji.retrieveList` as a
|
|
146
|
+
* follow-up on ~1/25 successful ops, so this path can contend on SQLite
|
|
147
|
+
* before heavier surfaces. Invalidated on emoji create/delete.
|
|
148
|
+
*/
|
|
149
|
+
private readonly emojiListByServerCache = new Map<
|
|
150
|
+
string,
|
|
151
|
+
{ rows: Emoji[]; until: number }
|
|
152
|
+
>();
|
|
153
|
+
|
|
154
|
+
private static readonly USER_DEVICE_LIST_CACHE_TTL_MS = 10_000;
|
|
155
|
+
private static readonly EMOJI_LIST_CACHE_TTL_MS = 10_000;
|
|
156
|
+
|
|
122
157
|
constructor(options?: SpireOptions) {
|
|
123
158
|
super();
|
|
124
159
|
|
|
@@ -138,22 +173,86 @@ export class Database extends EventEmitter {
|
|
|
138
173
|
break;
|
|
139
174
|
}
|
|
140
175
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
176
|
+
this.rawSqlite = new BetterSqlite3(filename);
|
|
177
|
+
this.rawSqlite.pragma("journal_mode = WAL");
|
|
178
|
+
this.rawSqlite.pragma("synchronous = NORMAL");
|
|
179
|
+
this.rawSqlite.pragma("busy_timeout = 5000");
|
|
180
|
+
this.rawSqlite.pragma("cache_size = -64000");
|
|
181
|
+
this.rawSqlite.pragma("temp_store = memory");
|
|
182
|
+
this.rawSqlite.pragma("foreign_keys = ON");
|
|
148
183
|
|
|
149
184
|
this.db = new Kysely<ServerDatabase>({
|
|
150
|
-
dialect: new SqliteDialect({ database:
|
|
185
|
+
dialect: new SqliteDialect({ database: this.rawSqlite }),
|
|
151
186
|
});
|
|
152
187
|
|
|
153
188
|
void this.init();
|
|
154
189
|
}
|
|
155
190
|
|
|
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
|
+
|
|
156
253
|
public async close(): Promise<void> {
|
|
254
|
+
this.userDeviceListCache.clear();
|
|
255
|
+
this.emojiListByServerCache.clear();
|
|
157
256
|
await this.db.destroy();
|
|
158
257
|
}
|
|
159
258
|
|
|
@@ -184,6 +283,7 @@ export class Database extends EventEmitter {
|
|
|
184
283
|
};
|
|
185
284
|
|
|
186
285
|
await this.db.insertInto("devices").values(device).execute();
|
|
286
|
+
this.userDeviceListCache.delete(owner);
|
|
187
287
|
|
|
188
288
|
const medPreKeys = {
|
|
189
289
|
deviceID: device.deviceID,
|
|
@@ -201,6 +301,7 @@ export class Database extends EventEmitter {
|
|
|
201
301
|
|
|
202
302
|
public async createEmoji(emoji: Emoji): Promise<void> {
|
|
203
303
|
await this.db.insertInto("emojis").values(emoji).execute();
|
|
304
|
+
this.emojiListByServerCache.delete(emoji.owner);
|
|
204
305
|
}
|
|
205
306
|
|
|
206
307
|
public async createFile(file: FileSQL): Promise<void> {
|
|
@@ -230,30 +331,31 @@ export class Database extends EventEmitter {
|
|
|
230
331
|
resourceID: string,
|
|
231
332
|
powerLevel: number,
|
|
232
333
|
): Promise<Permission> {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
334
|
+
// Atomic check-then-insert inside a transaction so two concurrent
|
|
335
|
+
// callers cannot both find "no existing" and insert duplicates.
|
|
336
|
+
return this.db.transaction().execute(async (trx) => {
|
|
337
|
+
const checkPermission = await trx
|
|
338
|
+
.selectFrom("permissions")
|
|
339
|
+
.selectAll()
|
|
340
|
+
.where("userID", "=", userID)
|
|
341
|
+
.where("resourceID", "=", resourceID)
|
|
342
|
+
.execute();
|
|
343
|
+
const existing = checkPermission[0];
|
|
344
|
+
if (existing) {
|
|
345
|
+
return existing;
|
|
346
|
+
}
|
|
246
347
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
348
|
+
const permission: Permission = {
|
|
349
|
+
permissionID: crypto.randomUUID(),
|
|
350
|
+
powerLevel,
|
|
351
|
+
resourceID,
|
|
352
|
+
resourceType,
|
|
353
|
+
userID,
|
|
354
|
+
};
|
|
254
355
|
|
|
255
|
-
|
|
256
|
-
|
|
356
|
+
await trx.insertInto("permissions").values(permission).execute();
|
|
357
|
+
return permission;
|
|
358
|
+
});
|
|
257
359
|
}
|
|
258
360
|
|
|
259
361
|
public async createServer(name: string, ownerID: string): Promise<Server> {
|
|
@@ -321,6 +423,12 @@ export class Database extends EventEmitter {
|
|
|
321
423
|
}
|
|
322
424
|
|
|
323
425
|
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
|
+
|
|
324
432
|
await this.db
|
|
325
433
|
.deleteFrom("preKeys")
|
|
326
434
|
.where("deviceID", "=", deviceID)
|
|
@@ -336,13 +444,21 @@ export class Database extends EventEmitter {
|
|
|
336
444
|
.set({ deleted: 1 })
|
|
337
445
|
.where("deviceID", "=", deviceID)
|
|
338
446
|
.execute();
|
|
447
|
+
|
|
448
|
+
if (ownerRow?.owner) {
|
|
449
|
+
this.userDeviceListCache.delete(ownerRow.owner);
|
|
450
|
+
}
|
|
339
451
|
}
|
|
340
452
|
|
|
341
453
|
public async deleteEmoji(emojiID: string): Promise<void> {
|
|
454
|
+
const existing = await this.retrieveEmoji(emojiID);
|
|
342
455
|
await this.db
|
|
343
456
|
.deleteFrom("emojis")
|
|
344
457
|
.where("emojiID", "=", emojiID)
|
|
345
458
|
.execute();
|
|
459
|
+
if (existing) {
|
|
460
|
+
this.emojiListByServerCache.delete(existing.owner);
|
|
461
|
+
}
|
|
346
462
|
}
|
|
347
463
|
|
|
348
464
|
public async deleteInvite(inviteID: string): Promise<void> {
|
|
@@ -405,31 +521,34 @@ export class Database extends EventEmitter {
|
|
|
405
521
|
}
|
|
406
522
|
|
|
407
523
|
public async getOTK(deviceID: string): Promise<null | PreKeysWS> {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
publicKey: XUtils.decodeHex(otkInfo.publicKey),
|
|
423
|
-
signature: XUtils.decodeHex(otkInfo.signature),
|
|
424
|
-
};
|
|
524
|
+
// Atomic select-then-delete inside a transaction so two concurrent
|
|
525
|
+
// callers cannot dispense the same one-time key.
|
|
526
|
+
return this.db.transaction().execute(async (trx) => {
|
|
527
|
+
const rows: PreKeysSQL[] = await trx
|
|
528
|
+
.selectFrom("oneTimeKeys")
|
|
529
|
+
.selectAll()
|
|
530
|
+
.where("deviceID", "=", deviceID)
|
|
531
|
+
.orderBy("index")
|
|
532
|
+
.limit(1)
|
|
533
|
+
.execute();
|
|
534
|
+
const otkInfo = rows[0];
|
|
535
|
+
if (!otkInfo) {
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
425
538
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
539
|
+
await trx
|
|
540
|
+
.deleteFrom("oneTimeKeys")
|
|
541
|
+
.where("deviceID", "=", deviceID)
|
|
542
|
+
.where("index", "=", otkInfo.index)
|
|
543
|
+
.execute();
|
|
544
|
+
|
|
545
|
+
return {
|
|
546
|
+
deviceID: otkInfo.deviceID,
|
|
547
|
+
index: otkInfo.index,
|
|
548
|
+
publicKey: XUtils.decodeHex(otkInfo.publicKey),
|
|
549
|
+
signature: XUtils.decodeHex(otkInfo.signature),
|
|
550
|
+
};
|
|
551
|
+
});
|
|
433
552
|
}
|
|
434
553
|
|
|
435
554
|
public async getOTKCount(deviceID: string): Promise<number> {
|
|
@@ -605,12 +724,25 @@ export class Database extends EventEmitter {
|
|
|
605
724
|
return rows[0] ?? null;
|
|
606
725
|
}
|
|
607
726
|
|
|
608
|
-
public async retrieveEmojiList(
|
|
609
|
-
|
|
727
|
+
public async retrieveEmojiList(serverID: string): Promise<Emoji[]> {
|
|
728
|
+
const now = Date.now();
|
|
729
|
+
const hit = this.emojiListByServerCache.get(serverID);
|
|
730
|
+
if (hit && hit.until > now) {
|
|
731
|
+
return hit.rows.map((r) => ({ ...r }));
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const rows: Emoji[] = await this.db
|
|
610
735
|
.selectFrom("emojis")
|
|
611
736
|
.selectAll()
|
|
612
|
-
.where("owner", "=",
|
|
737
|
+
.where("owner", "=", serverID)
|
|
613
738
|
.execute();
|
|
739
|
+
|
|
740
|
+
this.emojiListByServerCache.set(serverID, {
|
|
741
|
+
rows: rows.map((r) => ({ ...r })),
|
|
742
|
+
until: now + Database.EMOJI_LIST_CACHE_TTL_MS,
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
return rows.map((r) => ({ ...r }));
|
|
614
746
|
}
|
|
615
747
|
|
|
616
748
|
public async retrieveFile(fileID: string): Promise<FileSQL | null> {
|
|
@@ -752,17 +884,25 @@ export class Database extends EventEmitter {
|
|
|
752
884
|
.where("serverID", "=", serverID)
|
|
753
885
|
.execute();
|
|
754
886
|
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
if (!
|
|
761
|
-
|
|
887
|
+
const nowMs = Date.now();
|
|
888
|
+
const expiredIds: string[] = [];
|
|
889
|
+
const valid: Invite[] = [];
|
|
890
|
+
for (const row of rows) {
|
|
891
|
+
const expMs = new Date(row.expiration).getTime();
|
|
892
|
+
if (!Number.isFinite(expMs) || expMs <= nowMs) {
|
|
893
|
+
expiredIds.push(row.inviteID);
|
|
894
|
+
continue;
|
|
762
895
|
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
896
|
+
valid.push(row);
|
|
897
|
+
}
|
|
898
|
+
if (expiredIds.length > 0) {
|
|
899
|
+
await this.db
|
|
900
|
+
.deleteFrom("invites")
|
|
901
|
+
.where("serverID", "=", serverID)
|
|
902
|
+
.where("inviteID", "in", expiredIds)
|
|
903
|
+
.execute();
|
|
904
|
+
}
|
|
905
|
+
return valid;
|
|
766
906
|
}
|
|
767
907
|
|
|
768
908
|
public async retrieveServers(userID: string): Promise<Server[]> {
|
|
@@ -803,13 +943,36 @@ export class Database extends EventEmitter {
|
|
|
803
943
|
}
|
|
804
944
|
|
|
805
945
|
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
|
+
|
|
806
957
|
const rows = await this.db
|
|
807
958
|
.selectFrom("devices")
|
|
808
959
|
.selectAll()
|
|
809
960
|
.where("owner", "in", userIDs)
|
|
810
961
|
.where("deleted", "=", 0)
|
|
811
962
|
.execute();
|
|
812
|
-
|
|
963
|
+
const devices = rows.map(toDevice);
|
|
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;
|
|
813
976
|
}
|
|
814
977
|
|
|
815
978
|
public async retrieveUsers(): Promise<InternalUserRecord[]> {
|