@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
package/CLA.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Contributor License Agreement (Individual)
|
|
2
|
+
|
|
3
|
+
Thank you for your interest in contributing to this project (“**Project**”).
|
|
4
|
+
|
|
5
|
+
This Contributor License Agreement (“**Agreement**”) documents the rights granted by contributors to the Project’s copyright holders and maintainers (collectively, the “**Maintainers**”).
|
|
6
|
+
|
|
7
|
+
## Definitions
|
|
8
|
+
|
|
9
|
+
**“Contribution”** means any original work of authorship, including any modifications or additions to the Project, that you submit or have submitted to the Project in any form (for example, pull requests, issues with attached patches, or mailing-list posts).
|
|
10
|
+
|
|
11
|
+
**“Submit”** means any form of electronic or written communication sent to the Maintainers or Project repositories.
|
|
12
|
+
|
|
13
|
+
## Grant of rights
|
|
14
|
+
|
|
15
|
+
You hereby grant the Maintainers:
|
|
16
|
+
|
|
17
|
+
1. **Copyright license** — A perpetual, worldwide, non-exclusive, royalty-free, irrevocable license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute your Contributions under the **AGPL-3.0-or-later** license that applies to the Project.
|
|
18
|
+
|
|
19
|
+
2. **Relicensing for dual licensing** — The same rights as in (1), but under **other license terms** chosen by the Maintainers, solely as needed to offer **commercial or proprietary licenses** to third parties, without owing you royalties for such sublicensing.
|
|
20
|
+
|
|
21
|
+
3. **Patent license** — If your Contribution includes a patent claim that you have the right to license, you grant the Maintainers and users of the Project a perpetual, worldwide, non-exclusive, royalty-free license under those claims to make, use, sell, offer for sale, import, and otherwise dispose of the Contribution and works including it.
|
|
22
|
+
|
|
23
|
+
## Representations
|
|
24
|
+
|
|
25
|
+
You represent that:
|
|
26
|
+
|
|
27
|
+
- Each Contribution is your original creation, or you have sufficient rights to submit it under this Agreement.
|
|
28
|
+
- To your knowledge, your Contribution does not violate anyone else’s rights.
|
|
29
|
+
|
|
30
|
+
## Miscellaneous
|
|
31
|
+
|
|
32
|
+
This Agreement does not obligate the Maintainers to use or merge your Contributions.
|
|
33
|
+
|
|
34
|
+
If any part of this Agreement is held invalid, the remainder stays in effect.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
_This document is a practical starting point for open-source contribution. It is **not** legal advice. Have qualified counsel review and adapt it for your jurisdiction and entity before relying on it for government, procurement, or high-stakes decisions._
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Commercial licensing
|
|
2
|
+
|
|
3
|
+
This software is also available under **commercial license terms** from:
|
|
4
|
+
|
|
5
|
+
**Vex Heavy Industries LLC**
|
|
6
|
+
|
|
7
|
+
- **Email:** [licensing@vex.wtf](mailto:yuki@vex.wtf)
|
|
8
|
+
- **Web:** [vex.wtf](https://vex.wtf)
|
|
9
|
+
|
|
10
|
+
The default **public** license for open source use is the **GNU Affero General Public License v3.0** — see the [`LICENSE`](./LICENSE) file in this repository.
|
package/LICENSING.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Licensing
|
|
2
|
+
|
|
3
|
+
## Open source (default)
|
|
4
|
+
|
|
5
|
+
The default **public** license is the **GNU Affero General Public License v3.0** — see the full text in [`LICENSE`](./LICENSE). The SPDX identifier in `package.json` may use **AGPL-3.0-or-later**; either way, the grant is at least AGPL v3.
|
|
6
|
+
|
|
7
|
+
## Commercial licensing
|
|
8
|
+
|
|
9
|
+
Commercial licenses are offered by **Vex Heavy Industries LLC**. See [`LICENSE-COMMERCIAL`](./LICENSE-COMMERCIAL) for contact details ([vex.wtf](https://vex.wtf), **licensing@vex.wtf**).
|
|
10
|
+
|
|
11
|
+
If you need terms **other than the AGPL** (for example, a proprietary product or a deployment where AGPL obligations are not acceptable), inquire there.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
_This file summarizes how we intend licensing to work. It is not legal advice. For binding terms, rely on the written agreement or license grant you receive from the copyright holders._
|
package/README.md
CHANGED
|
@@ -58,20 +58,21 @@ Spire reads configuration from environment variables. **Docker Compose:** put th
|
|
|
58
58
|
|
|
59
59
|
### Required
|
|
60
60
|
|
|
61
|
-
| Variable | Description
|
|
62
|
-
| ------------ |
|
|
63
|
-
| `SPK` | Server private key, hex-encoded. Generate with `npm run gen-spk` (prints `SPK` and `JWT_SECRET` lines). Used for server identity signing (NaCl).
|
|
64
|
-
| `JWT_SECRET` | Hex or string used as the **HMAC secret for JWTs** — **required** and must be **separate from `SPK`**. `npm run gen-spk` emits a dedicated value; do not reuse `SPK` here.
|
|
65
|
-
| `DB_TYPE` | `
|
|
61
|
+
| Variable | Description |
|
|
62
|
+
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
63
|
+
| `SPK` | Server private key, hex-encoded. Generate with `npm run gen-spk` (prints `SPK` and `JWT_SECRET` lines). Used for server identity signing (NaCl). |
|
|
64
|
+
| `JWT_SECRET` | Hex or string used as the **HMAC secret for JWTs** — **required** and must be **separate from `SPK`**. `npm run gen-spk` emits a dedicated value; do not reuse `SPK` here. |
|
|
65
|
+
| `DB_TYPE` | `sqlite3` or `sqlite3mem`. All values use **SQLite** via `better-sqlite3` (file or `:memory:`). |
|
|
66
66
|
|
|
67
67
|
### Optional
|
|
68
68
|
|
|
69
|
-
| Variable | Default | Description
|
|
70
|
-
| -------------- | --------- |
|
|
71
|
-
| `API_PORT` | `16777` | Port for the REST API and WebSocket server (see `Spire` default in code if unset).
|
|
72
|
-
| `NODE_ENV` | _(unset)_ | Set to `production` to disable interactive `/docs` / `/async-docs`. If unset or any other value, doc viewers are mounted. `helmet()` runs in all modes.
|
|
73
|
-
| `CORS_ORIGINS` | _(empty)_ | Comma-separated allowed `Origin` values
|
|
74
|
-
| `
|
|
69
|
+
| Variable | Default | Description |
|
|
70
|
+
| -------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
71
|
+
| `API_PORT` | `16777` | Port for the REST API and WebSocket server (see `Spire` default in code if unset). |
|
|
72
|
+
| `NODE_ENV` | _(unset)_ | Set to `production` to disable interactive `/docs` / `/async-docs`. If unset or any other value, doc viewers are mounted. `helmet()` runs in all modes. |
|
|
73
|
+
| `CORS_ORIGINS` | _(empty)_ | Comma-separated allowed `Origin` values. If set, only those origins may use credentialed browser requests. If unset, Spire **reflects the request `Origin`** so self-hosted Spire and arbitrary app origins (Tauri, localhost, etc.) work without configuration — appropriate for bearer-token APIs; set an allowlist if you need to restrict which sites may call your instance from users' browsers. |
|
|
74
|
+
| `DEV_API_KEY` | _(empty)_ | When set, requests that send header `x-dev-api-key` with the same value (constant-time compare) **skip in-process rate limiters**. The same gate enables **`GET /status/process`** (404 without a valid key): a small JSON snapshot of the Spire Node process (PID, uptime, `memoryUsage`, cumulative `resourceUsage`, WebSocket client count). Dev / load-testing only — never set in production. |
|
|
75
|
+
| `CANARY` | _(unset)_ | |
|
|
75
76
|
|
|
76
77
|
### Sample `.env`
|
|
77
78
|
|
|
@@ -95,11 +96,12 @@ npm run format # prettier --write
|
|
|
95
96
|
npm run format:check
|
|
96
97
|
npm test # vitest run
|
|
97
98
|
npx type-coverage # type-coverage (≥95%)
|
|
98
|
-
npm run license:check # license allowlist gate
|
|
99
99
|
```
|
|
100
100
|
|
|
101
101
|
See [AGENTS.md](./AGENTS.md) for the release flow (changesets → publish → deploy-hook) and the rules for writing changesets.
|
|
102
102
|
|
|
103
|
+
Contributions from outside contributors should follow [CONTRIBUTING.md](./CONTRIBUTING.md) (including the [CLA](./CLA.md)).
|
|
104
|
+
|
|
103
105
|
## License
|
|
104
106
|
|
|
105
|
-
[AGPL-3.0-
|
|
107
|
+
Open source default: **[AGPL-3.0](./LICENSE)** (full text; see `package.json` for SPDX). Commercial licenses from **Vex Heavy Industries LLC**: [**LICENSE-COMMERCIAL**](./LICENSE-COMMERCIAL), [**LICENSING.md**](./LICENSING.md), [vex.wtf](https://vex.wtf).
|
package/dist/ClientManager.d.ts
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 type { Database } from "./Database.ts";
|
|
2
7
|
import type { BaseMsg, Device, User, UserRecord } from "@vex-chat/types";
|
|
3
8
|
import type WebSocket from "ws";
|
package/dist/ClientManager.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 { EventEmitter } from "events";
|
|
2
7
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
3
8
|
import { xConcat, XUtils } from "@vex-chat/crypto";
|
|
@@ -191,8 +196,7 @@ export class ClientManager extends EventEmitter {
|
|
|
191
196
|
this.send(p);
|
|
192
197
|
}
|
|
193
198
|
async pingLoop() {
|
|
194
|
-
|
|
195
|
-
while (true) {
|
|
199
|
+
while (!this.failed) {
|
|
196
200
|
this.ping();
|
|
197
201
|
await sleep(5000);
|
|
198
202
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ClientManager.js","sourceRoot":"","sources":["../src/ClientManager.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ClientManager.js","sourceRoot":"","sources":["../src/ClientManager.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAiBH,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,EAAE,UAAU,IAAI,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAE3D,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEjE,OAAO,EAAE,KAAK,IAAI,SAAS,EAAE,QAAQ,IAAI,YAAY,EAAE,MAAM,MAAM,CAAC;AAEpE,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAE7C,MAAM,CAAC,MAAM,YAAY,GAAG;IACxB,MAAM,EAAE,EAAE;IACV,MAAM,EAAE,EAAE;IACV,KAAK,EAAE,EAAE;IACT,MAAM,EAAE,EAAE;CACb,CAAC;AAEF,SAAS,WAAW;IAChB,OAAO,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC;AAC9B,CAAC;AAED,MAAM,YAAY,GAAG,IAAI,CAAC;AAE1B,MAAM,OAAO,aAAc,SAAQ,YAAY;IACnC,KAAK,GAAY,IAAI,CAAC;IACtB,MAAM,GAAY,KAAK,CAAC;IACxB,WAAW,GAAe,eAAe,EAAE,CAAC;IAC5C,IAAI,CAAY;IAChB,EAAE,CAAW;IACb,MAAM,CAAgB;IACtB,MAAM,GAAY,KAAK,CAAC;IACxB,MAAM,CAMJ;IACF,IAAI,CAAoB;IACxB,WAAW,CAAO;IAE1B,YACI,EAAa,EACb,EAAY,EACZ,MAAuE,EACvE,WAAiB;QAEjB,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,IAAI,GAAG,EAAE,CAAC;QACf,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAC/B,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QAErB,IAAI,CAAC,aAAa,EAAE,CAAC;QACrB,IAAI,CAAC,SAAS,EAAE,CAAC;IACrB,CAAC;IAEM,SAAS;QACZ,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;QACrD,CAAC;QACD,OAAO,IAAI,CAAC,MAAM,CAAC;IACvB,CAAC;IAEM,OAAO;QACV,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;YAC7B,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;QACpE,CAAC;QACD,OAAO,IAAI,CAAC,IAAI,CAAC;IACrB,CAAC;IAEM,IAAI,CAAC,GAAY,EAAE,MAAmB;QACzC,MAAM,aAAa,GAAG,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QAC/C,IAAI,CAAC;YACD,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QAClC,CAAC;QAAC,OAAO,IAAa,EAAE,CAAC;YACrB,2BAA2B;YAC3B,IAAI,CAAC,IAAI,EAAE,CAAC;QAChB,CAAC;IACL,CAAC;IAEe,QAAQ;QACpB,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YAC7B,OAAO,mBAAmB,CAAC;QAC/B,CAAC;QACD,OAAO,IAAI,CAAC,IAAI,CAAC,QAAQ,GAAG,GAAG,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC,QAAQ,GAAG,GAAG,CAAC;IACtE,CAAC;IAEO,SAAS,CAAC,cAAsB;QACpC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,IAAI,CAAC,iBAAiB,CAAC,cAAc,CAAC,CAAC;QACvC,KAAK,IAAI,CAAC,EAAE,CAAC,eAAe,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;QAC/C,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACxB,CAAC;IAEO,SAAS;QACb,IAAI,CAAC,WAAW,GAAG,IAAI,UAAU,CAAC,SAAS,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC;QAClE,MAAM,SAAS,GAAa;YACxB,SAAS,EAAE,IAAI,CAAC,WAAW;YAC3B,cAAc,EAAE,MAAM,CAAC,UAAU,EAAE;YACnC,IAAI,EAAE,WAAW;SACpB,CAAC;QACF,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACzB,CAAC;IAEO,IAAI;QACR,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YACd,OAAO;QACX,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;QAClB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACtB,CAAC;IAEO,KAAK,CAAC,aAAa,CAAC,GAAe;QACvC,MAAM,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,QAAQ,CAAC,CAAC;IACnE,CAAC;IAEO,aAAa;QACjB,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE;YACtB,UAAU,CAAC,GAAG,EAAE;gBACZ,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;oBACf,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;gBACtB,CAAC;YACL,CAAC,EAAE,YAAY,CAAC,CAAC;YACjB,KAAK,IAAI,CAAC,QAAQ,EAAE,CAAC;QACzB,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;YACvB,IAAI,CAAC,IAAI,EAAE,CAAC;QAChB,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,OAAe,EAAE,EAAE;YACxC,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;YAExC,IAAI,IAAI,GAAG,YAAY,EAAE,CAAC;gBACtB,IAAI,CAAC,OAAO,CACR,sCAAsC,EACtC,oCAAoC;oBAChC,MAAM,CAAC,IAAI,CAAC;oBACZ,qBAAqB;oBACrB,MAAM,CAAC,YAAY,CAAC,CAC3B,CAAC;gBACF,OAAO;YACX,CAAC;YAED,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;YAE7C,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;gBACZ,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,EAAE,2BAA2B,CAAC,CAAC;gBAC9D,OAAO;YACX,CAAC;YAED,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,cAAc,CAAC,EAAE,CAAC;gBACpC,IAAI,CAAC,OAAO,CACR,MAAM,CAAC,UAAU,EAAE,EACnB,sDAAsD,CACzD,CAAC;gBACF,OAAO;YACX,CAAC;YAED,QAAQ,GAAG,CAAC,IAAI,EAAE,CAAC;gBACf,KAAK,MAAM;oBACP,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;oBAC9B,MAAM;gBACV,KAAK,MAAM;oBACP,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;oBACpB,MAAM;gBACV,KAAK,SAAS;oBACV,+FAA+F;oBAC/F,KAAK,IAAI,CAAC,aAAa,CAAC,GAAiB,CAAC,CAAC;oBAC3C,MAAM;gBACV,KAAK,UAAU;oBACX,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;wBACf,IAAI,CAAC,OAAO,CACR,GAAG,CAAC,cAAc,EAClB,4BAA4B,CAC/B,CAAC;wBACF,MAAM;oBACV,CAAC;oBACD,+FAA+F;oBAC/F,KAAK,IAAI,CAAC,gBAAgB,CAAC,GAAkB,EAAE,MAAM,CAAC,CAAC;oBACvD,MAAM;gBACV,KAAK,UAAU;oBACX,+FAA+F;oBAC/F,KAAK,IAAI,CAAC,cAAc,CAAC,GAAc,CAAC,CAAC;oBACzC,MAAM;gBACV;oBACI,MAAM;YACd,CAAC;QACL,CAAC,CAAC,CAAC;IACP,CAAC;IAEO,KAAK,CAAC,gBAAgB,CAAC,GAAgB,EAAE,MAAkB;QAC/D,QAAQ,GAAG,CAAC,YAAY,EAAE,CAAC;YACvB,KAAK,MAAM;gBACP,IAAI,GAAG,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;oBAC1B,MAAM,UAAU,GAAG,YAAY,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;oBACpD,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC;wBACtB,IAAI,CAAC,OAAO,CACR,GAAG,CAAC,cAAc,EAClB,wBAAwB;4BACpB,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,CAC9C,CAAC;wBACF,OAAO;oBACX,CAAC;oBACD,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC;oBAE7B,IAAI,CAAC;wBACD,MAAM,IAAI,CAAC,EAAE,CAAC,QAAQ,CAClB,IAAI,EACJ,MAAM,EACN,IAAI,CAAC,SAAS,EAAE,CAAC,QAAQ,EACzB,IAAI,CAAC,OAAO,EAAE,CAAC,MAAM,CACxB,CAAC;wBACF,MAAM,aAAa,GAAG,MAAM,IAAI,CAAC,EAAE,CAAC,cAAc,CAC9C,IAAI,CAAC,SAAS,CACjB,CAAC;wBACF,IAAI,CAAC,aAAa,EAAE,CAAC;4BACjB,IAAI,CAAC,OAAO,CACR,GAAG,CAAC,cAAc,EAClB,6CAA6C,CAChD,CAAC;4BACF,OAAO;wBACX,CAAC;wBAED,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,cAAc,EAAE,IAAI,CAAC,CAAC;wBAC3C,IAAI,CAAC,MAAM,CACP,aAAa,CAAC,KAAK,EACnB,MAAM,EACN,GAAG,CAAC,cAAc,EAClB,IAAI,EACJ,IAAI,CAAC,SAAS,CACjB,CAAC;oBACN,CAAC;oBAAC,OAAO,GAAY,EAAE,CAAC;wBACpB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;oBAClD,CAAC;gBACL,CAAC;gBACD,MAAM;YACV;gBACI,MAAM;QACd,CAAC;IACL,CAAC;IAEO,IAAI;QACR,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;YACd,IAAI,CAAC,IAAI,EAAE,CAAC;YACZ,OAAO;QACX,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;QACrB,MAAM,CAAC,GAAG,EAAE,cAAc,EAAE,MAAM,CAAC,UAAU,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;QAChE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACjB,CAAC;IAEO,KAAK,CAAC,QAAQ;QAClB,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YAClB,IAAI,CAAC,IAAI,EAAE,CAAC;YACZ,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC;QACtB,CAAC;IACL,CAAC;IAEO,IAAI,CAAC,cAAsB;QAC/B,8BAA8B;QAC9B,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YACZ,KAAK,IAAI,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACzC,CAAC;QAED,MAAM,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;QAC3C,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACjB,CAAC;IAEO,iBAAiB,CAAC,cAAsB;QAC5C,IAAI,CAAC,IAAI,CAAC,EAAE,cAAc,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,CAAC;IACtD,CAAC;IAEO,aAAa,CAAC,KAAuB;QACzC,MAAM,GAAG,GAAG;YACR,KAAK;YACL,cAAc,EAAE,MAAM,CAAC,UAAU,EAAE;YACnC,IAAI,EAAE,SAAS;SAClB,CAAC;QACF,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACnB,CAAC;IAEO,OAAO,CAAC,cAAsB,EAAE,OAAe,EAAE,IAAc;QACnE,MAAM,KAAK,GAAW;YAClB,IAAI;YACJ,KAAK,EAAE,OAAO;YACd,cAAc;YACd,IAAI,EAAE,OAAO;SAChB,CAAC;QACF,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACrB,CAAC;IAEO,WAAW,CACf,cAAsB,EACtB,IAAa,EACb,MAAmB,EACnB,SAAkB;QAElB,MAAM,GAAG,GAAe;YACpB,IAAI;YACJ,SAAS;YACT,cAAc;YACd,IAAI,EAAE,SAAS;SAClB,CAAC;QACF,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAC3B,CAAC;IAEO,QAAQ,CAAC,MAAe;QAC5B,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC;IACxB,CAAC;IAEO,KAAK,CAAC,cAAc,CAAC,GAAY;QACrC,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;QACjE,IAAI,IAAI,EAAE,CAAC;YACP,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,EAAE,CAAC,sBAAsB,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;YACpE,IAAI,OAAO,GAAsB,IAAI,CAAC;YACtC,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;gBAC3B,MAAM,QAAQ,GAAG,SAAS,CACtB,GAAG,CAAC,MAAM,EACV,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,CACnC,CAAC;gBACF,IAAI,QAAQ,EAAE,CAAC;oBACX,OAAO,GAAG,QAAQ,CAAC;oBACnB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;gBACzB,CAAC;YACL,CAAC;YACD,IAAI,CAAC,OAAO,EAAE,CAAC;gBACX,IAAI,CAAC,aAAa,CAAC,gBAAgB,CAAC,YAAY,CAAC,CAAC;gBAClD,IAAI,CAAC,IAAI,EAAE,CAAC;gBACZ,OAAO;YACX,CAAC;YAED,IAAI,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,WAAW,EAAE,OAAO,CAAC,EAAE,CAAC;gBAC/C,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;gBACjB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;YACvC,CAAC;iBAAM,CAAC;gBACJ,IAAI,CAAC,aAAa,CAAC,gBAAgB,CAAC,YAAY,CAAC,CAAC;YACtD,CAAC;QACL,CAAC;aAAM,CAAC;YACJ,IAAI,CAAC,aAAa,CAAC,gBAAgB,CAAC,iBAAiB,CAAC,CAAC;YAEvD,IAAI,CAAC,IAAI,EAAE,CAAC;QAChB,CAAC;IACL,CAAC;CACJ;AAED,SAAS,WAAW,CAAC,GAAY,EAAE,MAAmB;IAClD,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IAClD,MAAM,IAAI,GAAG,MAAM,IAAI,WAAW,EAAE,CAAC;IACrC,OAAO,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;AAC/B,CAAC;AAED,SAAS,aAAa,CAAC,GAAW;IAC9B,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAElC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAC/B,iGAAiG;IACjG,MAAM,IAAI,GAAY,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC;IAErD,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;AACxB,CAAC"}
|
package/dist/Database.d.ts
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 type { SpireOptions } from "./Spire.ts";
|
|
2
7
|
import type { Channel, Device, DevicePayload, Emoji, FileSQL, Invite, KeyBundle, MailWS, Permission, PreKeysWS, RegistrationPayload, Server, UserRecord } from "@vex-chat/types";
|
|
3
8
|
import { EventEmitter } from "events";
|
|
@@ -7,7 +12,30 @@ export interface InternalUserRecord extends UserRecord {
|
|
|
7
12
|
}
|
|
8
13
|
export declare class Database extends EventEmitter {
|
|
9
14
|
private db;
|
|
15
|
+
/** Underlying better-sqlite3 handle (file or :memory:). */
|
|
16
|
+
private readonly rawSqlite;
|
|
17
|
+
/**
|
|
18
|
+
* In-process cache for {@link retrieveUserDeviceList} when queried for a
|
|
19
|
+
* single owner. Libvex calls GET /user/:id/devices on every outgoing
|
|
20
|
+
* `forward()`; stress runs otherwise hammer SQLite with identical reads.
|
|
21
|
+
* Cleared on device create/delete and when the DB is closed.
|
|
22
|
+
*/
|
|
23
|
+
private readonly userDeviceListCache;
|
|
24
|
+
/**
|
|
25
|
+
* Short TTL cache for {@link retrieveEmojiList} (key = server id, stored as
|
|
26
|
+
* `emojis.owner`). The noise harness fires `emoji.retrieveList` as a
|
|
27
|
+
* follow-up on ~1/25 successful ops, so this path can contend on SQLite
|
|
28
|
+
* before heavier surfaces. Invalidated on emoji create/delete.
|
|
29
|
+
*/
|
|
30
|
+
private readonly emojiListByServerCache;
|
|
31
|
+
private static readonly USER_DEVICE_LIST_CACHE_TTL_MS;
|
|
32
|
+
private static readonly EMOJI_LIST_CACHE_TTL_MS;
|
|
10
33
|
constructor(options?: SpireOptions);
|
|
34
|
+
/**
|
|
35
|
+
* Dev-only snapshot of SQLite files + pragmas (same process as Spire).
|
|
36
|
+
* Not for production callers.
|
|
37
|
+
*/
|
|
38
|
+
getDevSqliteMonitor(): Record<string, unknown>;
|
|
11
39
|
close(): Promise<void>;
|
|
12
40
|
createChannel(name: string, serverID: string): Promise<Channel>;
|
|
13
41
|
createDevice(owner: string, payload: DevicePayload): Promise<Device>;
|
|
@@ -46,7 +74,7 @@ export declare class Database extends EventEmitter {
|
|
|
46
74
|
retrieveChannels(serverID: string): Promise<Channel[]>;
|
|
47
75
|
retrieveDevice(deviceID: string): Promise<Device | null>;
|
|
48
76
|
retrieveEmoji(emojiID: string): Promise<Emoji | null>;
|
|
49
|
-
retrieveEmojiList(
|
|
77
|
+
retrieveEmojiList(serverID: string): Promise<Emoji[]>;
|
|
50
78
|
retrieveFile(fileID: string): Promise<FileSQL | null>;
|
|
51
79
|
retrieveGroupMembers(channelID: string): Promise<UserRecord[]>;
|
|
52
80
|
retrieveInvite(inviteID: string): Promise<Invite | null>;
|
package/dist/Database.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
|
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
7
|
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
8
|
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
@@ -8,6 +13,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
8
13
|
};
|
|
9
14
|
import { EventEmitter } from "events";
|
|
10
15
|
import { pbkdf2Sync } from "node:crypto";
|
|
16
|
+
import { statSync } from "node:fs";
|
|
11
17
|
import * as fs from "node:fs/promises";
|
|
12
18
|
import path from "node:path";
|
|
13
19
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -84,6 +90,24 @@ const pubkeyRegex = /[0-9a-f]{64}/;
|
|
|
84
90
|
const PBKDF2_ITERATIONS = 1000;
|
|
85
91
|
export class Database extends EventEmitter {
|
|
86
92
|
db;
|
|
93
|
+
/** Underlying better-sqlite3 handle (file or :memory:). */
|
|
94
|
+
rawSqlite;
|
|
95
|
+
/**
|
|
96
|
+
* In-process cache for {@link retrieveUserDeviceList} when queried for a
|
|
97
|
+
* single owner. Libvex calls GET /user/:id/devices on every outgoing
|
|
98
|
+
* `forward()`; stress runs otherwise hammer SQLite with identical reads.
|
|
99
|
+
* Cleared on device create/delete and when the DB is closed.
|
|
100
|
+
*/
|
|
101
|
+
userDeviceListCache = new Map();
|
|
102
|
+
/**
|
|
103
|
+
* Short TTL cache for {@link retrieveEmojiList} (key = server id, stored as
|
|
104
|
+
* `emojis.owner`). The noise harness fires `emoji.retrieveList` as a
|
|
105
|
+
* follow-up on ~1/25 successful ops, so this path can contend on SQLite
|
|
106
|
+
* before heavier surfaces. Invalidated on emoji create/delete.
|
|
107
|
+
*/
|
|
108
|
+
emojiListByServerCache = new Map();
|
|
109
|
+
static USER_DEVICE_LIST_CACHE_TTL_MS = 10_000;
|
|
110
|
+
static EMOJI_LIST_CACHE_TTL_MS = 10_000;
|
|
87
111
|
constructor(options) {
|
|
88
112
|
super();
|
|
89
113
|
const dbType = options?.dbType || "sqlite3";
|
|
@@ -100,19 +124,78 @@ export class Database extends EventEmitter {
|
|
|
100
124
|
filename = "spire.sqlite";
|
|
101
125
|
break;
|
|
102
126
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
127
|
+
this.rawSqlite = new BetterSqlite3(filename);
|
|
128
|
+
this.rawSqlite.pragma("journal_mode = WAL");
|
|
129
|
+
this.rawSqlite.pragma("synchronous = NORMAL");
|
|
130
|
+
this.rawSqlite.pragma("busy_timeout = 5000");
|
|
131
|
+
this.rawSqlite.pragma("cache_size = -64000");
|
|
132
|
+
this.rawSqlite.pragma("temp_store = memory");
|
|
133
|
+
this.rawSqlite.pragma("foreign_keys = ON");
|
|
110
134
|
this.db = new Kysely({
|
|
111
|
-
dialect: new SqliteDialect({ database:
|
|
135
|
+
dialect: new SqliteDialect({ database: this.rawSqlite }),
|
|
112
136
|
});
|
|
113
137
|
void this.init();
|
|
114
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Dev-only snapshot of SQLite files + pragmas (same process as Spire).
|
|
141
|
+
* Not for production callers.
|
|
142
|
+
*/
|
|
143
|
+
getDevSqliteMonitor() {
|
|
144
|
+
const openedAs = this.rawSqlite.name;
|
|
145
|
+
const absPath = openedAs === ":memory:"
|
|
146
|
+
? ":memory:"
|
|
147
|
+
: path.isAbsolute(openedAs)
|
|
148
|
+
? openedAs
|
|
149
|
+
: path.resolve(process.cwd(), openedAs);
|
|
150
|
+
const journalMode = this.rawSqlite.pragma("journal_mode", {
|
|
151
|
+
simple: true,
|
|
152
|
+
});
|
|
153
|
+
const synchronous = this.rawSqlite.pragma("synchronous", {
|
|
154
|
+
simple: true,
|
|
155
|
+
});
|
|
156
|
+
const busyTimeout = this.rawSqlite.pragma("busy_timeout", {
|
|
157
|
+
simple: true,
|
|
158
|
+
});
|
|
159
|
+
const cacheSize = this.rawSqlite.pragma("cache_size", { simple: true });
|
|
160
|
+
const pageCount = this.rawSqlite.pragma("page_count", { simple: true });
|
|
161
|
+
const pageSize = this.rawSqlite.pragma("page_size", { simple: true });
|
|
162
|
+
const freelistCount = this.rawSqlite.pragma("freelist_count", {
|
|
163
|
+
simple: true,
|
|
164
|
+
});
|
|
165
|
+
const out = {
|
|
166
|
+
absPath,
|
|
167
|
+
busyTimeout,
|
|
168
|
+
cacheSize,
|
|
169
|
+
freelistCount,
|
|
170
|
+
journalMode,
|
|
171
|
+
openedAs,
|
|
172
|
+
pageCount,
|
|
173
|
+
pageSize,
|
|
174
|
+
synchronous,
|
|
175
|
+
};
|
|
176
|
+
if (openedAs !== ":memory:") {
|
|
177
|
+
const filePaths = {
|
|
178
|
+
main: absPath,
|
|
179
|
+
shm: `${absPath}-shm`,
|
|
180
|
+
wal: `${absPath}-wal`,
|
|
181
|
+
};
|
|
182
|
+
out["filePaths"] = filePaths;
|
|
183
|
+
const sizes = {};
|
|
184
|
+
for (const [label, fp] of Object.entries(filePaths)) {
|
|
185
|
+
try {
|
|
186
|
+
sizes[label] = statSync(fp).size;
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
sizes[label] = 0;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
out["fileBytes"] = sizes;
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
115
196
|
async close() {
|
|
197
|
+
this.userDeviceListCache.clear();
|
|
198
|
+
this.emojiListByServerCache.clear();
|
|
116
199
|
await this.db.destroy();
|
|
117
200
|
}
|
|
118
201
|
async createChannel(name, serverID) {
|
|
@@ -134,6 +217,7 @@ export class Database extends EventEmitter {
|
|
|
134
217
|
signKey: payload.signKey,
|
|
135
218
|
};
|
|
136
219
|
await this.db.insertInto("devices").values(device).execute();
|
|
220
|
+
this.userDeviceListCache.delete(owner);
|
|
137
221
|
const medPreKeys = {
|
|
138
222
|
deviceID: device.deviceID,
|
|
139
223
|
index: payload.preKeyIndex,
|
|
@@ -147,6 +231,7 @@ export class Database extends EventEmitter {
|
|
|
147
231
|
}
|
|
148
232
|
async createEmoji(emoji) {
|
|
149
233
|
await this.db.insertInto("emojis").values(emoji).execute();
|
|
234
|
+
this.emojiListByServerCache.delete(emoji.owner);
|
|
150
235
|
}
|
|
151
236
|
async createFile(file) {
|
|
152
237
|
await this.db.insertInto("files").values(file).execute();
|
|
@@ -162,27 +247,29 @@ export class Database extends EventEmitter {
|
|
|
162
247
|
return invite;
|
|
163
248
|
}
|
|
164
249
|
async createPermission(userID, resourceType, resourceID, powerLevel) {
|
|
165
|
-
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
250
|
+
// Atomic check-then-insert inside a transaction so two concurrent
|
|
251
|
+
// callers cannot both find "no existing" and insert duplicates.
|
|
252
|
+
return this.db.transaction().execute(async (trx) => {
|
|
253
|
+
const checkPermission = await trx
|
|
254
|
+
.selectFrom("permissions")
|
|
255
|
+
.selectAll()
|
|
256
|
+
.where("userID", "=", userID)
|
|
257
|
+
.where("resourceID", "=", resourceID)
|
|
258
|
+
.execute();
|
|
259
|
+
const existing = checkPermission[0];
|
|
260
|
+
if (existing) {
|
|
261
|
+
return existing;
|
|
262
|
+
}
|
|
263
|
+
const permission = {
|
|
264
|
+
permissionID: crypto.randomUUID(),
|
|
265
|
+
powerLevel,
|
|
266
|
+
resourceID,
|
|
267
|
+
resourceType,
|
|
268
|
+
userID,
|
|
269
|
+
};
|
|
270
|
+
await trx.insertInto("permissions").values(permission).execute();
|
|
271
|
+
return permission;
|
|
272
|
+
});
|
|
186
273
|
}
|
|
187
274
|
async createServer(name, ownerID) {
|
|
188
275
|
// create the server
|
|
@@ -241,6 +328,11 @@ export class Database extends EventEmitter {
|
|
|
241
328
|
.execute();
|
|
242
329
|
}
|
|
243
330
|
async deleteDevice(deviceID) {
|
|
331
|
+
const ownerRow = await this.db
|
|
332
|
+
.selectFrom("devices")
|
|
333
|
+
.select("owner")
|
|
334
|
+
.where("deviceID", "=", deviceID)
|
|
335
|
+
.executeTakeFirst();
|
|
244
336
|
await this.db
|
|
245
337
|
.deleteFrom("preKeys")
|
|
246
338
|
.where("deviceID", "=", deviceID)
|
|
@@ -254,12 +346,19 @@ export class Database extends EventEmitter {
|
|
|
254
346
|
.set({ deleted: 1 })
|
|
255
347
|
.where("deviceID", "=", deviceID)
|
|
256
348
|
.execute();
|
|
349
|
+
if (ownerRow?.owner) {
|
|
350
|
+
this.userDeviceListCache.delete(ownerRow.owner);
|
|
351
|
+
}
|
|
257
352
|
}
|
|
258
353
|
async deleteEmoji(emojiID) {
|
|
354
|
+
const existing = await this.retrieveEmoji(emojiID);
|
|
259
355
|
await this.db
|
|
260
356
|
.deleteFrom("emojis")
|
|
261
357
|
.where("emojiID", "=", emojiID)
|
|
262
358
|
.execute();
|
|
359
|
+
if (existing) {
|
|
360
|
+
this.emojiListByServerCache.delete(existing.owner);
|
|
361
|
+
}
|
|
263
362
|
}
|
|
264
363
|
async deleteInvite(inviteID) {
|
|
265
364
|
await this.db
|
|
@@ -315,30 +414,32 @@ export class Database extends EventEmitter {
|
|
|
315
414
|
return keyBundle;
|
|
316
415
|
}
|
|
317
416
|
async getOTK(deviceID) {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
417
|
+
// Atomic select-then-delete inside a transaction so two concurrent
|
|
418
|
+
// callers cannot dispense the same one-time key.
|
|
419
|
+
return this.db.transaction().execute(async (trx) => {
|
|
420
|
+
const rows = await trx
|
|
421
|
+
.selectFrom("oneTimeKeys")
|
|
422
|
+
.selectAll()
|
|
423
|
+
.where("deviceID", "=", deviceID)
|
|
424
|
+
.orderBy("index")
|
|
425
|
+
.limit(1)
|
|
426
|
+
.execute();
|
|
427
|
+
const otkInfo = rows[0];
|
|
428
|
+
if (!otkInfo) {
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
await trx
|
|
432
|
+
.deleteFrom("oneTimeKeys")
|
|
433
|
+
.where("deviceID", "=", deviceID)
|
|
434
|
+
.where("index", "=", otkInfo.index)
|
|
435
|
+
.execute();
|
|
436
|
+
return {
|
|
437
|
+
deviceID: otkInfo.deviceID,
|
|
438
|
+
index: otkInfo.index,
|
|
439
|
+
publicKey: XUtils.decodeHex(otkInfo.publicKey),
|
|
440
|
+
signature: XUtils.decodeHex(otkInfo.signature),
|
|
441
|
+
};
|
|
442
|
+
});
|
|
342
443
|
}
|
|
343
444
|
async getOTKCount(deviceID) {
|
|
344
445
|
const result = await this.db
|
|
@@ -491,12 +592,22 @@ export class Database extends EventEmitter {
|
|
|
491
592
|
.execute();
|
|
492
593
|
return rows[0] ?? null;
|
|
493
594
|
}
|
|
494
|
-
async retrieveEmojiList(
|
|
495
|
-
|
|
595
|
+
async retrieveEmojiList(serverID) {
|
|
596
|
+
const now = Date.now();
|
|
597
|
+
const hit = this.emojiListByServerCache.get(serverID);
|
|
598
|
+
if (hit && hit.until > now) {
|
|
599
|
+
return hit.rows.map((r) => ({ ...r }));
|
|
600
|
+
}
|
|
601
|
+
const rows = await this.db
|
|
496
602
|
.selectFrom("emojis")
|
|
497
603
|
.selectAll()
|
|
498
|
-
.where("owner", "=",
|
|
604
|
+
.where("owner", "=", serverID)
|
|
499
605
|
.execute();
|
|
606
|
+
this.emojiListByServerCache.set(serverID, {
|
|
607
|
+
rows: rows.map((r) => ({ ...r })),
|
|
608
|
+
until: now + Database.EMOJI_LIST_CACHE_TTL_MS,
|
|
609
|
+
});
|
|
610
|
+
return rows.map((r) => ({ ...r }));
|
|
500
611
|
}
|
|
501
612
|
async retrieveFile(fileID) {
|
|
502
613
|
const file = await this.db
|
|
@@ -608,14 +719,25 @@ export class Database extends EventEmitter {
|
|
|
608
719
|
.selectAll()
|
|
609
720
|
.where("serverID", "=", serverID)
|
|
610
721
|
.execute();
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
722
|
+
const nowMs = Date.now();
|
|
723
|
+
const expiredIds = [];
|
|
724
|
+
const valid = [];
|
|
725
|
+
for (const row of rows) {
|
|
726
|
+
const expMs = new Date(row.expiration).getTime();
|
|
727
|
+
if (!Number.isFinite(expMs) || expMs <= nowMs) {
|
|
728
|
+
expiredIds.push(row.inviteID);
|
|
729
|
+
continue;
|
|
616
730
|
}
|
|
617
|
-
|
|
618
|
-
}
|
|
731
|
+
valid.push(row);
|
|
732
|
+
}
|
|
733
|
+
if (expiredIds.length > 0) {
|
|
734
|
+
await this.db
|
|
735
|
+
.deleteFrom("invites")
|
|
736
|
+
.where("serverID", "=", serverID)
|
|
737
|
+
.where("inviteID", "in", expiredIds)
|
|
738
|
+
.execute();
|
|
739
|
+
}
|
|
740
|
+
return valid;
|
|
619
741
|
}
|
|
620
742
|
async retrieveServers(userID) {
|
|
621
743
|
const serverPerms = await this.retrievePermissions(userID, "server");
|
|
@@ -651,13 +773,33 @@ export class Database extends EventEmitter {
|
|
|
651
773
|
return row ? toUserRecord(row) : null;
|
|
652
774
|
}
|
|
653
775
|
async retrieveUserDeviceList(userIDs) {
|
|
776
|
+
const now = Date.now();
|
|
777
|
+
if (userIDs.length === 1) {
|
|
778
|
+
const owner = userIDs[0];
|
|
779
|
+
if (typeof owner === "string") {
|
|
780
|
+
const hit = this.userDeviceListCache.get(owner);
|
|
781
|
+
if (hit && hit.until > now) {
|
|
782
|
+
return hit.devices.map((d) => ({ ...d }));
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
654
786
|
const rows = await this.db
|
|
655
787
|
.selectFrom("devices")
|
|
656
788
|
.selectAll()
|
|
657
789
|
.where("owner", "in", userIDs)
|
|
658
790
|
.where("deleted", "=", 0)
|
|
659
791
|
.execute();
|
|
660
|
-
|
|
792
|
+
const devices = rows.map(toDevice);
|
|
793
|
+
if (userIDs.length === 1) {
|
|
794
|
+
const owner = userIDs[0];
|
|
795
|
+
if (typeof owner === "string") {
|
|
796
|
+
this.userDeviceListCache.set(owner, {
|
|
797
|
+
devices: devices.map((d) => ({ ...d })),
|
|
798
|
+
until: now + Database.USER_DEVICE_LIST_CACHE_TTL_MS,
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
return devices;
|
|
661
803
|
}
|
|
662
804
|
async retrieveUsers() {
|
|
663
805
|
const rows = await this.db.selectFrom("users").selectAll().execute();
|